index.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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-warning">
  37. <div class="card-body text-center">
  38. <h6>Actives</h6>
  39. <h2 id="kpiActiveShards">–</h2>
  40. </div>
  41. </div>
  42. </div>
  43. <div class="col-md-2">
  44. <div class="card card-kpi text-bg-success">
  45. <div class="card-body text-center">
  46. <h6>Players</h6>
  47. <h2 id="kpiTotalPlayers">–</h2>
  48. </div>
  49. </div>
  50. </div>
  51. <div class="col-md-2">
  52. <div class="card card-kpi text-bg-dark">
  53. <div class="card-body text-center">
  54. <h6>Events</h6>
  55. <h2 id="kpiTotalEvents">–</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. <div class="row">
  120. <div class="col-md-6">
  121. <h6>Events over time (Snapshots)</h6>
  122. <canvas id="snapshotChart"></canvas>
  123. </div>
  124. <div class="col-md-6">
  125. <h6>Events over time (Events)</h6>
  126. <canvas id="eventsChart"></canvas>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. </div>
  133. <!-- ===================================================== -->
  134. <!-- TAB: PLAYERS -->
  135. <!-- ===================================================== -->
  136. <div class="tab-pane fade" id="tabPlayers">
  137. <div class="d-flex justify-content-between align-items-center mb-2">
  138. <h2 class="section-title mb-0">Players (Global)</h2>
  139. <button
  140. class="btn btn-sm btn-outline-secondary"
  141. onclick="loadPlayersGlobal()">
  142. 🔄 Refresh
  143. </button>
  144. </div>
  145. <table class="table table-striped">
  146. <thead class="table-dark">
  147. <tr>
  148. <th>Player</th>
  149. <th>Global Score</th>
  150. <th>Shards</th>
  151. <th>Actions</th>
  152. </tr>
  153. </thead>
  154. <tbody id="tablePlayers"></tbody>
  155. </table>
  156. </div>
  157. <!-- ===================================================== -->
  158. <!-- TAB: RANKINGS -->
  159. <!-- ===================================================== -->
  160. <div class="tab-pane fade" id="tabRankings">
  161. <div class="d-flex justify-content-between align-items-center mb-2">
  162. <h2 class="section-title mb-0">Rankings by</h2>
  163. <button class="btn btn-sm btn-outline-secondary" onclick="refreshRankings()">🔄 Refresh</button>
  164. </div>
  165. <ul class="nav nav-pills mb-3">
  166. <li class="nav-item">
  167. <button class="nav-link active" data-bs-toggle="pill" data-bs-target="#rankGlobal">Global</button>
  168. </li>
  169. <li class="nav-item">
  170. <button class="nav-link" data-bs-toggle="pill" data-bs-target="#rankShard">By Shard</button>
  171. </li>
  172. </ul>
  173. <div class="tab-content">
  174. <div class="tab-pane fade show active" id="rankGlobal">
  175. Ranking global de jugadores (agregación multi‑shard)
  176. <table class="table table-sm table-striped">
  177. <thead class="table-dark">
  178. <tr>
  179. <th>#</th>
  180. <th>Player</th>
  181. <th>Score</th>
  182. </tr>
  183. </thead>
  184. <tbody id="tableRankingGlobal"></tbody>
  185. </table>
  186. </div>
  187. <div class="tab-pane fade" id="rankShard">
  188. Ranking por shard usando <code>shard_player_stats</code>
  189. <table class="table table-sm table-striped">
  190. <thead class="table-dark">
  191. <tr>
  192. <th>#</th>
  193. <th>Player</th>
  194. <th>Score</th>
  195. </tr>
  196. </thead>
  197. <tbody id="tableRankingShard"></tbody>
  198. </table>
  199. </div>
  200. </div>
  201. </div>
  202. <!-- ===================================================== -->
  203. <!-- TAB: EVENTS -->
  204. <!-- ===================================================== -->
  205. <div class="tab-pane fade" id="tabEvents">
  206. <h2 class="section-title">Event Explorer</h2>
  207. <div class="alert alert-info mb-2">
  208. Vista paginada y filtrable de <code>master_events</code>
  209. </div>
  210. <div class="d-flex gap-2 mb-2">
  211. <input type="text" id="filterShard" class="form-control form-control-sm" placeholder="Shard ID">
  212. <input type="text" id="filterPlayer" class="form-control form-control-sm" placeholder="Player ID">
  213. <select id="filterAction" class="form-select form-select-sm">
  214. <option value="">All actions</option>
  215. <option value="EXPLORE">EXPLORE</option>
  216. <option value="GATHER">GATHER</option>
  217. <option value="FIGHT">FIGHT</option>
  218. <option value="REST">REST</option>
  219. </select>
  220. <button class="btn btn-sm btn-outline-primary" onclick="loadEvents(0)">
  221. Apply
  222. </button>
  223. </div>
  224. <table class="table table-sm table-striped table-hover">
  225. <thead class="table-dark">
  226. <tr>
  227. <th>Time</th>
  228. <th>Shard</th>
  229. <th>Player</th>
  230. <th>Action</th>
  231. <th>Δ Score</th>
  232. </tr>
  233. </thead>
  234. <tbody id="tableEvents"></tbody>
  235. </table>
  236. <div class="d-flex justify-content-between align-items-center">
  237. <button class="btn btn-sm btn-outline-secondary" onclick="prevEventsPage()">
  238. ◀ Prev
  239. </button>
  240. <span id="eventsPageInfo"></span>
  241. <button class="btn btn-sm btn-outline-secondary" onclick="nextEventsPage()">
  242. Next ▶
  243. </button>
  244. </div>
  245. </div>
  246. </div>
  247. </div>
  248. <script
  249. src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
  250. integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
  251. crossorigin="anonymous">
  252. </script>
  253. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  254. <script>
  255. const API_BASE = "http://localhost:8010";
  256. const REFRESH_INTERVAL_MS = 3000;
  257. let selectedShardId = null;
  258. let snapshotChart = null;
  259. let eventsChart = null;
  260. let eventsPage = 0;
  261. let eventsTotalPages = 0;
  262. async function refreshShardsTab() {
  263. try {
  264. await loadDashboardSummary();
  265. await loadShardsOverview();
  266. } catch (err) {
  267. console.error("Error actualizando Shards Overview", err);
  268. }
  269. }
  270. /* FUNCIONES PARA ACTUALIZAR LA PESTAÑA SHARDS */
  271. async function loadDashboardSummary() {
  272. const response = await fetch(`${API_BASE}/master/dashboard/summary`);
  273. const summary = await response.json();
  274. document.getElementById("kpiTotalShards").textContent = summary.totalShards;
  275. document.getElementById("kpiTotalPlayers").textContent = summary.totalPlayers;
  276. document.getElementById("kpiTotalEvents").textContent = summary.totalEvents;
  277. document.getElementById("kpiActiveShards").textContent = summary.activeShards;
  278. }
  279. async function loadShardsOverview() {
  280. const response = await fetch(`${API_BASE}/master/shards`);
  281. const shards = await response.json();
  282. const tbody = document.getElementById("tableShards");
  283. tbody.innerHTML = "";
  284. shards.forEach(shard => {
  285. const tr = document.createElement("tr");
  286. tr.innerHTML = `
  287. <td>${shard.shardId}</td>
  288. <td>${shard.name ?? "—"}</td>
  289. <td>${shard.location ?? "—"}</td>
  290. <td>
  291. <span class="badge ${statusBadge(shard.status)}">
  292. ${shard.status}
  293. </span>
  294. </td>
  295. <td>
  296. <span class="badge bg-${heartbeatColor(shard.lastHeartbeat)}">
  297. ${formatHeartbeat(shard.lastHeartbeat)}
  298. </span>
  299. </td>
  300. <td>${shard.totalEvents}</td>
  301. <td>${shard.totalPlayers}</td>
  302. `;
  303. if (shard.shardId === selectedShardId) {
  304. tr.classList.add("shard-selected");
  305. loadShardDetail();
  306. }
  307. tr.addEventListener("click", () => {
  308. selectedShardId = shard.shardId;
  309. console.log(selectedShardId);
  310. loadShardDetail();
  311. loadShardsOverview();
  312. });
  313. tr.style.cursor = "pointer";
  314. tbody.appendChild(tr);
  315. });
  316. }
  317. async function loadShardPlayers() {
  318. if (!selectedShardId) return;
  319. const res = await fetch(
  320. `${API_BASE}/master/shards/${selectedShardId}/players`
  321. );
  322. const players = await res.json();
  323. const tbody = document.getElementById("shardPlayers");
  324. tbody.innerHTML = "";
  325. players.forEach(p => {
  326. const tr = document.createElement("tr");
  327. tr.innerHTML = `
  328. <td>${p.playerId}</td>
  329. <td>${p.score}</td>
  330. <td>${p.totalActions}</td>
  331. `;
  332. tbody.appendChild(tr);
  333. });
  334. }
  335. async function loadShardSnapshots() {
  336. if (!selectedShardId) return;
  337. const res = await fetch(
  338. `${API_BASE}/master/shards/${selectedShardId}/snapshots`
  339. );
  340. const snapshots = await res.json();
  341. const labels = snapshots.map(s =>
  342. new Date(s.timestamp).toLocaleTimeString()
  343. );
  344. const data = snapshots.map(s => s.totalEvents);
  345. if (snapshotChart) {
  346. snapshotChart.destroy();
  347. }
  348. snapshotChart = new Chart(
  349. document.getElementById("snapshotChart"),
  350. {
  351. type: "line",
  352. data: {
  353. labels,
  354. datasets: [{
  355. label: "Total Events",
  356. data,
  357. borderColor: "blue",
  358. tension: 0.2
  359. }]
  360. }
  361. }
  362. );
  363. }
  364. async function loadShardEventsChart() {
  365. if (!selectedShardId) return;
  366. const res = await fetch(
  367. `${API_BASE}/master/shards/${selectedShardId}/events/timeseries`
  368. );
  369. const series = await res.json();
  370. const labels = series.map(p =>
  371. new Date(p.timestamp).toLocaleTimeString()
  372. );
  373. const data = series.map(p => p.totalEvents);
  374. if (eventsChart) eventsChart.destroy();
  375. eventsChart = new Chart(
  376. document.getElementById("eventsChart"),
  377. {
  378. type: "line",
  379. data: {
  380. labels,
  381. datasets: [{
  382. label: "Total Events (Events table)",
  383. data,
  384. borderColor: "red",
  385. tension: 0.2
  386. }]
  387. }
  388. }
  389. );
  390. }
  391. async function loadShardDetail() {
  392. await loadShardPlayers();
  393. await loadShardSnapshots();
  394. await loadShardEventsChart();
  395. }
  396. /* FUNCIONES PARA ACTUALIZAR LA PESTAÑA PLAYERS */
  397. async function loadPlayersGlobal() {
  398. const res = await fetch(`${API_BASE}/master/players`);
  399. const players = await res.json();
  400. const tbody = document.getElementById("tablePlayers");
  401. tbody.innerHTML = "";
  402. players.forEach(p => {
  403. const tr = document.createElement("tr");
  404. tr.innerHTML = `
  405. <td>${p.playerId}</td>
  406. <td>${p.globalScore}</td>
  407. <td>${p.shardsPlayed}</td>
  408. <td>${p.totalActions}</td>
  409. `;
  410. tbody.appendChild(tr);
  411. });
  412. }
  413. /* FUNCIONES PARA ACTUALIZAR PESTAÑA RANKINGS */
  414. async function loadRankingGlobal() {
  415. const res = await fetch(`${API_BASE}/master/rankings/global`);
  416. const ranking = await res.json();
  417. const tbody = document.getElementById("tableRankingGlobal");
  418. tbody.innerHTML = "";
  419. ranking.forEach((p, index) => {
  420. const tr = document.createElement("tr");
  421. tr.innerHTML = `
  422. <td>${index + 1}</td>
  423. <td>${p.playerId}</td>
  424. <td>${p.score}</td>
  425. `;
  426. tbody.appendChild(tr);
  427. });
  428. }
  429. async function loadRankingShard() {
  430. if (!selectedShardId) return;
  431. const res = await fetch(
  432. `${API_BASE}/master/rankings/shards/${selectedShardId}`
  433. );
  434. const ranking = await res.json();
  435. const tbody = document.getElementById("tableRankingShard");
  436. tbody.innerHTML = "";
  437. ranking.forEach((p, index) => {
  438. const tr = document.createElement("tr");
  439. tr.innerHTML = `
  440. <td>${index + 1}</td>
  441. <td>${p.playerId}</td>
  442. <td>${p.score}</td>
  443. `;
  444. tbody.appendChild(tr);
  445. });
  446. }
  447. function refreshRankings(){
  448. loadRankingGlobal();
  449. loadRankingShard();
  450. }
  451. /* FUNCIONES PARA ACTUALIZAR LA PESTAÑA EVENTS */
  452. async function loadEvents(page) {
  453. const shard = document.getElementById("filterShard").value;
  454. const player = document.getElementById("filterPlayer").value;
  455. const action = document.getElementById("filterAction").value;
  456. const params = new URLSearchParams({
  457. page: page,
  458. size: 15,
  459. shard: shard,
  460. player: player,
  461. action: action
  462. });
  463. const res = await fetch(
  464. `${API_BASE}/master/events?${params}`
  465. );
  466. const data = await res.json();
  467. eventsPage = data.number;
  468. eventsTotalPages = data.totalPages;
  469. const tbody = document.getElementById("tableEvents");
  470. tbody.innerHTML = "";
  471. data.content.forEach(e => {
  472. const tr = document.createElement("tr");
  473. tr.innerHTML = `
  474. <td>${new Date(e.timestamp).toLocaleTimeString()}</td>
  475. <td>${e.shardId}</td>
  476. <td>${e.playerId}</td>
  477. <td>${e.action}</td>
  478. <td>${e.pointsDelta}</td>
  479. `;
  480. tbody.appendChild(tr);
  481. });
  482. document.getElementById("eventsPageInfo").textContent =
  483. `Page ${eventsPage + 1} / ${eventsTotalPages}`;
  484. }
  485. function nextEventsPage() {
  486. if (eventsPage + 1 < eventsTotalPages) {
  487. loadEvents(eventsPage + 1);
  488. }
  489. }
  490. function prevEventsPage() {
  491. if (eventsPage > 0) {
  492. loadEvents(eventsPage - 1);
  493. }
  494. }
  495. /* FUNCIONES AUXILIARES */
  496. function heartbeatColor(lastHeartbeat) {
  497. if (!lastHeartbeat) return "secondary";
  498. const diffSeconds =
  499. (Date.now() - new Date(lastHeartbeat).getTime()) / 1000;
  500. if (diffSeconds < 15) return "success";
  501. if (diffSeconds < 60) return "warning";
  502. return "danger";
  503. }
  504. function formatHeartbeat(lastHeartbeat) {
  505. if (!lastHeartbeat) return "no heartbeat";
  506. return new Date(lastHeartbeat).toLocaleTimeString();
  507. }
  508. function statusBadge(status) {
  509. switch (status) {
  510. case "STARTED": return "bg-success";
  511. case "STOPPED": return "bg-danger";
  512. default: return "bg-secondary";
  513. }
  514. }
  515. // Arranque inicial
  516. refreshShardsTab();
  517. // Refresco automático
  518. setInterval(refreshShardsTab, REFRESH_INTERVAL_MS);
  519. document.querySelector('button[data-bs-target="#tabPlayers"]')
  520. .addEventListener("shown.bs.tab", () => {
  521. loadPlayersGlobal();
  522. });
  523. document.querySelector('button[data-bs-target="#tabRankings"]')
  524. .addEventListener("shown.bs.tab", () => {
  525. refreshRankings();
  526. });
  527. document.querySelector('button[data-bs-target="#tabEvents"]')
  528. .addEventListener("shown.bs.tab", () => {
  529. loadEvents(0);
  530. });
  531. </script>
  532. </body>