index.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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. .shard-selected {background-color: #4b48ff !important;}
  16. .table.table-hover tbody tr.shard-selected > * {background-color: #4b48ff !important;}
  17. .shard-table-scroll {max-height: 320px;overflow-y: auto;overflow-x: hidden;}
  18. </style>
  19. </head>
  20. <body>
  21. <div class="container my-4">
  22. <h1 class="mb-4">Shard Master – Control Panel</h1>
  23. <!-- ===================================================== -->
  24. <!-- KPI ROW (COMPACT) -->
  25. <!-- ===================================================== -->
  26. <div class="row mb-3">
  27. <div class="col-md-2">
  28. <div class="card card-kpi text-bg-primary">
  29. <div class="card-body text-center">
  30. <h6>Shards</h6>
  31. <h2 id="kpiTotalShards">–</h2>
  32. </div>
  33. </div>
  34. </div>
  35. <div class="col-md-2">
  36. <div class="card card-kpi text-bg-success">
  37. <div class="card-body text-center">
  38. <h6>Players</h6>
  39. <h2 id="kpiTotalPlayers">–</h2>
  40. </div>
  41. </div>
  42. </div>
  43. <div class="col-md-2">
  44. <div class="card card-kpi text-bg-dark">
  45. <div class="card-body text-center">
  46. <h6>Events</h6>
  47. <h2 id="kpiTotalEvents">–</h2>
  48. </div>
  49. </div>
  50. </div>
  51. <div class="col-md-2">
  52. <div class="card card-kpi text-bg-warning">
  53. <div class="card-body text-center">
  54. <h6>Active</h6>
  55. <h2 id="kpiActiveShards">–</h2>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. <!-- ===================================================== -->
  61. <!-- MAIN NAV TABS -->
  62. <!-- ===================================================== -->
  63. <ul class="nav nav-tabs" role="tablist">
  64. <li class="nav-item">
  65. <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabShards">Shards</button>
  66. </li>
  67. <li class="nav-item">
  68. <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabPlayers">Players</button>
  69. </li>
  70. <li class="nav-item">
  71. <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabRankings">Rankings</button>
  72. </li>
  73. <li class="nav-item">
  74. <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabEvents">Events</button>
  75. </li>
  76. </ul>
  77. <div class="tab-content border border-top-0 p-3 bg-white">
  78. <!-- ===================================================== -->
  79. <!-- TAB: SHARDS -->
  80. <!-- ===================================================== -->
  81. <div class="tab-pane fade show active" id="tabShards">
  82. <h2 class="section-title">Shards Overview</h2>
  83. <div class="shard-table-scroll">
  84. <table class="table table-striped table-hover mb-0">
  85. <thead class="table-dark">
  86. <tr>
  87. <th>Shard</th>
  88. <th>Name</th>
  89. <th>Location</th>
  90. <th>Status</th>
  91. <th>Heartbeat</th>
  92. <th>Events</th>
  93. <th>Players</th>
  94. </tr>
  95. </thead>
  96. <tbody id="tableShards"></tbody>
  97. </table>
  98. </div>
  99. <div class="d-flex justify-content-between align-items-center">
  100. <h3 class="section-title">Shard Detail</h3>
  101. <button
  102. class="btn btn-sm btn-outline-secondary"
  103. onclick="loadShardDetail()"
  104. title="Refresh shard details">
  105. 🔄 Refresh
  106. </button>
  107. </div>
  108. <div class="row">
  109. <div class="col-md-6">
  110. <h6>Top Players (Shard)</h6>
  111. <table class="table table-sm">
  112. <thead><tr><th>Player</th><th>Score</th><th>Actions</th></tr></thead>
  113. <tbody id="shardPlayers"></tbody>
  114. </table>
  115. </div>
  116. <div class="col-md-6">
  117. <h6>Activity Snapshot</h6>
  118. <div class="alert alert-light">
  119. <canvas id="snapshotChart"></canvas>
  120. </div>
  121. </div>
  122. </div>
  123. </div>
  124. <!-- ===================================================== -->
  125. <!-- TAB: PLAYERS -->
  126. <!-- ===================================================== -->
  127. <div class="tab-pane fade" id="tabPlayers">
  128. <h2 class="section-title">Players (Global)</h2>
  129. <table class="table table-striped">
  130. <thead class="table-dark">
  131. <tr>
  132. <th>Player</th>
  133. <th>Global Score</th>
  134. <th>Shards</th>
  135. <th>Actions</th>
  136. </tr>
  137. </thead>
  138. <tbody id="tablePlayers"></tbody>
  139. </table>
  140. </div>
  141. <!-- ===================================================== -->
  142. <!-- TAB: RANKINGS -->
  143. <!-- ===================================================== -->
  144. <div class="tab-pane fade" id="tabRankings">
  145. <ul class="nav nav-pills mb-3">
  146. <li class="nav-item">
  147. <button class="nav-link active" data-bs-toggle="pill" data-bs-target="#rankGlobal">Global</button>
  148. </li>
  149. <li class="nav-item">
  150. <button class="nav-link" data-bs-toggle="pill" data-bs-target="#rankShard">By Shard</button>
  151. </li>
  152. </ul>
  153. <div class="tab-content">
  154. <div class="tab-pane fade show active" id="rankGlobal">
  155. Ranking global de jugadores (agregación multi‑shard)
  156. </div>
  157. <div class="tab-pane fade" id="rankShard">
  158. Ranking por shard usando <code>shard_player_stats</code>
  159. </div>
  160. </div>
  161. </div>
  162. <!-- ===================================================== -->
  163. <!-- TAB: EVENTS -->
  164. <!-- ===================================================== -->
  165. <div class="tab-pane fade" id="tabEvents">
  166. <h2 class="section-title">Event Explorer</h2>
  167. <div class="alert alert-info mb-2">
  168. Vista paginada y filtrable de <code>master_events</code>
  169. </div>
  170. <table class="table table-sm table-hover">
  171. <thead>
  172. <tr>
  173. <th>Time</th>
  174. <th>Shard</th>
  175. <th>Player</th>
  176. <th>Action</th>
  177. <th>Δ</th>
  178. <th>Winner</th>
  179. </tr>
  180. </thead>
  181. <tbody id="tableEvents"></tbody>
  182. </table>
  183. </div>
  184. </div>
  185. </div>
  186. <script
  187. src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
  188. integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
  189. crossorigin="anonymous">
  190. </script>
  191. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  192. <script>
  193. const API_BASE = "http://localhost:8010";
  194. const REFRESH_INTERVAL_MS = 3000;
  195. let selectedShardId = null;
  196. let snapshotChart = null;
  197. async function refreshShardsTab() {
  198. try {
  199. await loadDashboardSummary();
  200. await loadShardsOverview();
  201. } catch (err) {
  202. console.error("Error actualizando Shards Overview", err);
  203. }
  204. }
  205. async function loadDashboardSummary() {
  206. const response = await fetch(`${API_BASE}/master/dashboard/summary`);
  207. const summary = await response.json();
  208. document.getElementById("kpiTotalShards").textContent = summary.totalShards;
  209. document.getElementById("kpiTotalPlayers").textContent = summary.totalPlayers;
  210. document.getElementById("kpiTotalEvents").textContent = summary.totalEvents;
  211. document.getElementById("kpiActiveShards").textContent = summary.activeShards;
  212. }
  213. async function loadShardsOverview() {
  214. const response = await fetch(`${API_BASE}/master/shards`);
  215. const shards = await response.json();
  216. const tbody = document.getElementById("tableShards");
  217. tbody.innerHTML = "";
  218. shards.forEach(shard => {
  219. const tr = document.createElement("tr");
  220. tr.innerHTML = `
  221. <td>${shard.shardId}</td>
  222. <td>${shard.name ?? "—"}</td>
  223. <td>${shard.location ?? "—"}</td>
  224. <td>
  225. <span class="badge ${statusBadge(shard.status)}">
  226. ${shard.status}
  227. </span>
  228. </td>
  229. <td>
  230. <span class="badge bg-${heartbeatColor(shard.lastHeartbeat)}">
  231. ${formatHeartbeat(shard.lastHeartbeat)}
  232. </span>
  233. </td>
  234. <td>${shard.totalEvents}</td>
  235. <td>${shard.totalPlayers}</td>
  236. `;
  237. if (shard.shardId === selectedShardId) {
  238. tr.classList.add("shard-selected");
  239. }
  240. tr.addEventListener("click", () => {
  241. selectedShardId = shard.shardId;
  242. console.log(selectedShardId);
  243. loadShardDetail();
  244. loadShardsOverview();
  245. });
  246. tr.style.cursor = "pointer";
  247. tbody.appendChild(tr);
  248. });
  249. }
  250. async function loadShardPlayers() {
  251. if (!selectedShardId) return;
  252. const res = await fetch(
  253. `${API_BASE}/master/shards/${selectedShardId}/players`
  254. );
  255. const players = await res.json();
  256. const tbody = document.getElementById("shardPlayers");
  257. tbody.innerHTML = "";
  258. players.forEach(p => {
  259. const tr = document.createElement("tr");
  260. tr.innerHTML = `
  261. <td>${p.playerId}</td>
  262. <td>${p.score}</td>
  263. <td>${p.totalActions}</td>
  264. `;
  265. tbody.appendChild(tr);
  266. });
  267. }
  268. async function loadShardSnapshots() {
  269. if (!selectedShardId) return;
  270. const res = await fetch(
  271. `${API_BASE}/master/shards/${selectedShardId}/snapshots`
  272. );
  273. const snapshots = await res.json();
  274. const labels = snapshots.map(s =>
  275. new Date(s.timestamp).toLocaleTimeString()
  276. );
  277. const data = snapshots.map(s => s.totalEvents);
  278. if (snapshotChart) {
  279. snapshotChart.destroy();
  280. }
  281. snapshotChart = new Chart(
  282. document.getElementById("snapshotChart"),
  283. {
  284. type: "line",
  285. data: {
  286. labels,
  287. datasets: [{
  288. label: "Total Events",
  289. data,
  290. borderColor: "blue",
  291. tension: 0.2
  292. }]
  293. }
  294. }
  295. );
  296. }
  297. async function loadShardDetail() {
  298. await loadShardPlayers();
  299. await loadShardSnapshots();
  300. }
  301. /* FUNCIONES AUXILIARES */
  302. function heartbeatColor(lastHeartbeat) {
  303. if (!lastHeartbeat) return "secondary";
  304. const diffSeconds =
  305. (Date.now() - new Date(lastHeartbeat).getTime()) / 1000;
  306. if (diffSeconds < 15) return "success";
  307. if (diffSeconds < 60) return "warning";
  308. return "danger";
  309. }
  310. function formatHeartbeat(lastHeartbeat) {
  311. if (!lastHeartbeat) return "no heartbeat";
  312. return new Date(lastHeartbeat).toLocaleTimeString();
  313. }
  314. function statusBadge(status) {
  315. switch (status) {
  316. case "STARTED": return "bg-success";
  317. case "STOPPED": return "bg-danger";
  318. default: return "bg-secondary";
  319. }
  320. }
  321. // Arranque inicial
  322. refreshShardsTab();
  323. // Refresco automático
  324. setInterval(refreshShardsTab, REFRESH_INTERVAL_MS);
  325. </script>
  326. </body>