index.html 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <!DOCTYPE html>
  2. <html lang="es">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>Shard Master - Control Panel</title>
  6. <!-- Bootstrap CSS -->
  7. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
  8. <style>
  9. body { background-color: #f5f6f8; }
  10. .section-title { margin-top: 2rem; }
  11. .heartbeat-badge { font-size: 0.8rem; }
  12. .card-kpi { min-height: 90px; padding: 0.5rem; }
  13. .card-kpi h6 { font-size: 0.85rem; margin-bottom: 0.25rem; }
  14. .card-kpi h2 { font-size: 1.4rem; margin: 0; }
  15. </style>
  16. </head>
  17. <body>
  18. <div class="container my-4">
  19. <h1 class="mb-4">Shard Master – Control Panel</h1>
  20. <!-- ===================================================== -->
  21. <!-- KPI ROW (COMPACT) -->
  22. <!-- ===================================================== -->
  23. <div class="row mb-3">
  24. <div class="col-md-2">
  25. <div class="card card-kpi text-bg-primary">
  26. <div class="card-body text-center">
  27. <h6>Shards</h6>
  28. <h2 id="kpiTotalShards">–</h2>
  29. </div>
  30. </div>
  31. </div>
  32. <div class="col-md-2">
  33. <div class="card card-kpi text-bg-success">
  34. <div class="card-body text-center">
  35. <h6>Players</h6>
  36. <h2 id="kpiTotalPlayers">–</h2>
  37. </div>
  38. </div>
  39. </div>
  40. <div class="col-md-2">
  41. <div class="card card-kpi text-bg-dark">
  42. <div class="card-body text-center">
  43. <h6>Events</h6>
  44. <h2 id="kpiTotalEvents">–</h2>
  45. </div>
  46. </div>
  47. </div>
  48. <div class="col-md-2">
  49. <div class="card card-kpi text-bg-warning">
  50. <div class="card-body text-center">
  51. <h6>Active</h6>
  52. <h2 id="kpiActiveShards">–</h2>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- ===================================================== -->
  58. <!-- MAIN NAV TABS -->
  59. <!-- ===================================================== -->
  60. <ul class="nav nav-tabs" role="tablist">
  61. <li class="nav-item">
  62. <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabShards">Shards</button>
  63. </li>
  64. <li class="nav-item">
  65. <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabPlayers">Players</button>
  66. </li>
  67. <li class="nav-item">
  68. <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabRankings">Rankings</button>
  69. </li>
  70. <li class="nav-item">
  71. <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabEvents">Events</button>
  72. </li>
  73. </ul>
  74. <div class="tab-content border border-top-0 p-3 bg-white">
  75. <!-- ===================================================== -->
  76. <!-- TAB: SHARDS -->
  77. <!-- ===================================================== -->
  78. <div class="tab-pane fade show active" id="tabShards">
  79. <h2 class="section-title">Shards Overview</h2>
  80. <table class="table table-striped table-hover">
  81. <thead class="table-dark">
  82. <tr>
  83. <th>Shard</th>
  84. <th>Name</th>
  85. <th>Location</th>
  86. <th>Status</th>
  87. <th>Heartbeat</th>
  88. <th>Events</th>
  89. <th>Players</th>
  90. </tr>
  91. </thead>
  92. <tbody id="tableShards"></tbody>
  93. </table>
  94. <h3 class="section-title">Shard Detail</h3>
  95. <div class="row">
  96. <div class="col-md-6">
  97. <h6>Top Players (Shard)</h6>
  98. <table class="table table-sm">
  99. <thead><tr><th>Player</th><th>Score</th><th>Actions</th></tr></thead>
  100. <tbody id="shardPlayers"></tbody>
  101. </table>
  102. </div>
  103. <div class="col-md-6">
  104. <h6>Activity Snapshot</h6>
  105. <div class="alert alert-light">
  106. Gráfico temporal basado en <code>shard_world_snapshot</code>
  107. </div>
  108. </div>
  109. </div>
  110. </div>
  111. <!-- ===================================================== -->
  112. <!-- TAB: PLAYERS -->
  113. <!-- ===================================================== -->
  114. <div class="tab-pane fade" id="tabPlayers">
  115. <h2 class="section-title">Players (Global)</h2>
  116. <table class="table table-striped">
  117. <thead class="table-dark">
  118. <tr>
  119. <th>Player</th>
  120. <th>Global Score</th>
  121. <th>Shards</th>
  122. <th>Actions</th>
  123. </tr>
  124. </thead>
  125. <tbody id="tablePlayers"></tbody>
  126. </table>
  127. </div>
  128. <!-- ===================================================== -->
  129. <!-- TAB: RANKINGS -->
  130. <!-- ===================================================== -->
  131. <div class="tab-pane fade" id="tabRankings">
  132. <ul class="nav nav-pills mb-3">
  133. <li class="nav-item">
  134. <button class="nav-link active" data-bs-toggle="pill" data-bs-target="#rankGlobal">Global</button>
  135. </li>
  136. <li class="nav-item">
  137. <button class="nav-link" data-bs-toggle="pill" data-bs-target="#rankShard">By Shard</button>
  138. </li>
  139. </ul>
  140. <div class="tab-content">
  141. <div class="tab-pane fade show active" id="rankGlobal">
  142. Ranking global de jugadores (agregación multi‑shard)
  143. </div>
  144. <div class="tab-pane fade" id="rankShard">
  145. Ranking por shard usando <code>shard_player_stats</code>
  146. </div>
  147. </div>
  148. </div>
  149. <!-- ===================================================== -->
  150. <!-- TAB: EVENTS -->
  151. <!-- ===================================================== -->
  152. <div class="tab-pane fade" id="tabEvents">
  153. <h2 class="section-title">Event Explorer</h2>
  154. <div class="alert alert-info mb-2">
  155. Vista paginada y filtrable de <code>master_events</code>
  156. </div>
  157. <table class="table table-sm table-hover">
  158. <thead>
  159. <tr>
  160. <th>Time</th>
  161. <th>Shard</th>
  162. <th>Player</th>
  163. <th>Action</th>
  164. <th>Δ</th>
  165. <th>Winner</th>
  166. </tr>
  167. </thead>
  168. <tbody id="tableEvents"></tbody>
  169. </table>
  170. </div>
  171. </div>
  172. </div>
  173. <script
  174. src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
  175. integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
  176. crossorigin="anonymous">
  177. </script>
  178. <script>
  179. const API_BASE = "http://localhost:8010";
  180. const REFRESH_INTERVAL_MS = 1000;
  181. async function refreshShardsTab() {
  182. try {
  183. await loadDashboardSummary();
  184. await loadShardsOverview();
  185. } catch (err) {
  186. console.error("Error actualizando Shards Overview", err);
  187. }
  188. }
  189. async function loadDashboardSummary() {
  190. const response = await fetch(`${API_BASE}/master/dashboard/summary`);
  191. const summary = await response.json();
  192. document.getElementById("kpiTotalShards").textContent = summary.totalShards;
  193. document.getElementById("kpiTotalPlayers").textContent = summary.totalPlayers;
  194. document.getElementById("kpiTotalEvents").textContent = summary.totalEvents;
  195. document.getElementById("kpiActiveShards").textContent = summary.activeShards;
  196. }
  197. async function loadShardsOverview() {
  198. const response = await fetch(`${API_BASE}/master/shards`);
  199. const shards = await response.json();
  200. const tbody = document.getElementById("tableShards");
  201. tbody.innerHTML = "";
  202. shards.forEach(shard => {
  203. const tr = document.createElement("tr");
  204. tr.innerHTML = `
  205. <td>${shard.shardId}</td>
  206. <td>${shard.name ?? "—"}</td>
  207. <td>${shard.location ?? "—"}</td>
  208. <td>
  209. <span class="badge ${statusBadge(shard.status)}">
  210. ${shard.status}
  211. </span>
  212. </td>
  213. <td>
  214. <span class="badge bg-${heartbeatColor(shard.lastHeartbeat)}">
  215. ${formatHeartbeat(shard.lastHeartbeat)}
  216. </span>
  217. </td>
  218. <td>${shard.totalEvents}</td>
  219. <td>${shard.totalPlayers}</td>
  220. `;
  221. tbody.appendChild(tr);
  222. });
  223. }
  224. function heartbeatColor(lastHeartbeat) {
  225. if (!lastHeartbeat) return "secondary";
  226. const diffSeconds =
  227. (Date.now() - new Date(lastHeartbeat).getTime()) / 1000;
  228. if (diffSeconds < 15) return "success";
  229. if (diffSeconds < 60) return "warning";
  230. return "danger";
  231. }
  232. function formatHeartbeat(lastHeartbeat) {
  233. if (!lastHeartbeat) return "no heartbeat";
  234. return new Date(lastHeartbeat).toLocaleTimeString();
  235. }
  236. function statusBadge(status) {
  237. switch (status) {
  238. case "STARTED": return "bg-success";
  239. case "STOPPED": return "bg-danger";
  240. default: return "bg-secondary";
  241. }
  242. }
  243. // Arranque inicial
  244. refreshShardsTab();
  245. // Refresco automático
  246. setInterval(refreshShardsTab, REFRESH_INTERVAL_MS);
  247. </script>
  248. </body>