| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655 |
- <!DOCTYPE html>
- <html lang="es">
- <head>
- <meta charset="UTF-8" />
- <title>Shard Master - Control Panel</title>
- <!-- Bootstrap CSS -->
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
- <style>
- body { background-color: #f5f6f8; }
- .section-title { margin-top: 2rem; }
- .heartbeat-badge { font-size: 0.8rem; }
- .card-kpi { min-height: 90px; padding: 0.5rem; }
- .card-kpi h6 { font-size: 0.85rem; margin-bottom: 0.25rem; }
- .card-kpi h2 { font-size: 1.4rem; margin: 0; }
- .shard-selected {background-color: #4b48ff !important;}
- .table.table-hover tbody tr.shard-selected > * {background-color: #4b48ff !important;}
- .shard-table-scroll {max-height: 320px;overflow-y: auto;overflow-x: hidden;}
-
- </style>
- </head>
- <body>
- <div class="container my-4">
- <h1 class="mb-4">Shard Master – Control Panel</h1>
- <!-- ===================================================== -->
- <!-- KPI ROW (COMPACT) -->
- <!-- ===================================================== -->
- <div class="row mb-3">
- <div class="col-md-2">
- <div class="card card-kpi text-bg-primary">
- <div class="card-body text-center">
- <h6>Shards</h6>
- <h2 id="kpiTotalShards">–</h2>
- </div>
- </div>
- </div>
- <div class="col-md-2">
- <div class="card card-kpi text-bg-warning">
- <div class="card-body text-center">
- <h6>Actives</h6>
- <h2 id="kpiActiveShards">–</h2>
- </div>
- </div>
- </div>
- <div class="col-md-2">
- <div class="card card-kpi text-bg-success">
- <div class="card-body text-center">
- <h6>Players</h6>
- <h2 id="kpiTotalPlayers">–</h2>
- </div>
- </div>
- </div>
- <div class="col-md-2">
- <div class="card card-kpi text-bg-dark">
- <div class="card-body text-center">
- <h6>Events</h6>
- <h2 id="kpiTotalEvents">–</h2>
- </div>
- </div>
- </div>
- </div>
- <!-- ===================================================== -->
- <!-- MAIN NAV TABS -->
- <!-- ===================================================== -->
- <ul class="nav nav-tabs" role="tablist">
- <li class="nav-item">
- <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabShards">Shards</button>
- </li>
- <li class="nav-item">
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabPlayers">Players</button>
- </li>
- <li class="nav-item">
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabRankings">Rankings</button>
- </li>
- <li class="nav-item">
- <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabEvents">Events</button>
- </li>
- </ul>
- <div class="tab-content border border-top-0 p-3 bg-white">
- <!-- ===================================================== -->
- <!-- TAB: SHARDS -->
- <!-- ===================================================== -->
- <div class="tab-pane fade show active" id="tabShards">
- <h2 class="section-title">Shards Overview</h2>
- <div class="shard-table-scroll">
- <table class="table table-striped table-hover mb-0">
- <thead class="table-dark">
- <tr>
- <th>Shard</th>
- <th>Name</th>
- <th>Location</th>
- <th>Status</th>
- <th>Heartbeat</th>
- <th>Events</th>
- <th>Players</th>
- </tr>
- </thead>
- <tbody id="tableShards"></tbody>
- </table>
- </div>
-
- <div class="d-flex justify-content-between align-items-center">
- <h3 class="section-title">Shard Detail</h3>
- <button
- class="btn btn-sm btn-outline-secondary"
- onclick="loadShardDetail()"
- title="Refresh shard details">
- 🔄 Refresh
- </button>
- </div>
- <div class="row">
- <div class="col-md-6">
- <h6>Top Players (Shard)</h6>
- <table class="table table-sm">
- <thead><tr><th>Player</th><th>Score</th><th>Actions</th></tr></thead>
- <tbody id="shardPlayers"></tbody>
- </table>
- </div>
- <div class="col-md-6">
- <h6>Activity Snapshot</h6>
- <div class="alert alert-light">
- <div class="row">
- <div class="col-md-6">
- <h6>Events over time (Snapshots)</h6>
- <canvas id="snapshotChart"></canvas>
- </div>
- <div class="col-md-6">
- <h6>Events over time (Events)</h6>
- <canvas id="eventsChart"></canvas>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- ===================================================== -->
- <!-- TAB: PLAYERS -->
- <!-- ===================================================== -->
- <div class="tab-pane fade" id="tabPlayers">
- <div class="d-flex justify-content-between align-items-center mb-2">
- <h2 class="section-title mb-0">Players (Global)</h2>
- <button
- class="btn btn-sm btn-outline-secondary"
- onclick="loadPlayersGlobal()">
- 🔄 Refresh
- </button>
- </div>
- <table class="table table-striped">
- <thead class="table-dark">
- <tr>
- <th>Player</th>
- <th>Global Score</th>
- <th>Shards</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody id="tablePlayers"></tbody>
- </table>
- </div>
- <!-- ===================================================== -->
- <!-- TAB: RANKINGS -->
- <!-- ===================================================== -->
- <div class="tab-pane fade" id="tabRankings">
- <div class="d-flex justify-content-between align-items-center mb-2">
- <h2 class="section-title mb-0">Rankings by</h2>
- <button class="btn btn-sm btn-outline-secondary" onclick="refreshRankings()">🔄 Refresh</button>
- </div>
- <ul class="nav nav-pills mb-3">
- <li class="nav-item">
- <button class="nav-link active" data-bs-toggle="pill" data-bs-target="#rankGlobal">Global</button>
- </li>
- <li class="nav-item">
- <button class="nav-link" data-bs-toggle="pill" data-bs-target="#rankShard">By Shard</button>
- </li>
- </ul>
- <div class="tab-content">
- <div class="tab-pane fade show active" id="rankGlobal">
- Ranking global de jugadores (agregación multi‑shard)
- <table class="table table-sm table-striped">
- <thead class="table-dark">
- <tr>
- <th>#</th>
- <th>Player</th>
- <th>Score</th>
- </tr>
- </thead>
- <tbody id="tableRankingGlobal"></tbody>
- </table>
-
- </div>
- <div class="tab-pane fade" id="rankShard">
- Ranking por shard usando <code>shard_player_stats</code>
- <table class="table table-sm table-striped">
- <thead class="table-dark">
- <tr>
- <th>#</th>
- <th>Player</th>
- <th>Score</th>
- </tr>
- </thead>
- <tbody id="tableRankingShard"></tbody>
- </table>
- </div>
- </div>
- </div>
- <!-- ===================================================== -->
- <!-- TAB: EVENTS -->
- <!-- ===================================================== -->
- <div class="tab-pane fade" id="tabEvents">
- <h2 class="section-title">Event Explorer</h2>
- <div class="alert alert-info mb-2">
- Vista paginada y filtrable de <code>master_events</code>
- </div>
- <div class="d-flex gap-2 mb-2">
- <input type="text" id="filterShard" class="form-control form-control-sm" placeholder="Shard ID">
- <input type="text" id="filterPlayer" class="form-control form-control-sm" placeholder="Player ID">
- <select id="filterAction" class="form-select form-select-sm">
- <option value="">All actions</option>
- <option value="EXPLORE">EXPLORE</option>
- <option value="GATHER">GATHER</option>
- <option value="FIGHT">FIGHT</option>
- <option value="REST">REST</option>
- </select>
- <button class="btn btn-sm btn-outline-primary" onclick="loadEvents(0)">
- Apply
- </button>
- </div>
- <table class="table table-sm table-striped table-hover">
- <thead class="table-dark">
- <tr>
- <th>Time</th>
- <th>Shard</th>
- <th>Player</th>
- <th>Action</th>
- <th>Δ Score</th>
- </tr>
- </thead>
- <tbody id="tableEvents"></tbody>
- </table>
- <div class="d-flex justify-content-between align-items-center">
- <button class="btn btn-sm btn-outline-secondary" onclick="prevEventsPage()">
- ◀ Prev
- </button>
- <span id="eventsPageInfo"></span>
- <button class="btn btn-sm btn-outline-secondary" onclick="nextEventsPage()">
- Next ▶
- </button>
- </div>
- </div>
- </div>
- </div>
- <script
- src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
- integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
- crossorigin="anonymous">
- </script>
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
- <script>
- const API_BASE = "http://localhost:8010";
- const REFRESH_INTERVAL_MS = 3000;
- let selectedShardId = null;
- let snapshotChart = null;
- let eventsChart = null;
- let eventsPage = 0;
- let eventsTotalPages = 0;
- async function refreshShardsTab() {
- try {
- await loadDashboardSummary();
- await loadShardsOverview();
- } catch (err) {
- console.error("Error actualizando Shards Overview", err);
- }
- }
- /* FUNCIONES PARA ACTUALIZAR LA PESTAÑA SHARDS */
- async function loadDashboardSummary() {
- const response = await fetch(`${API_BASE}/master/dashboard/summary`);
- const summary = await response.json();
- document.getElementById("kpiTotalShards").textContent = summary.totalShards;
- document.getElementById("kpiTotalPlayers").textContent = summary.totalPlayers;
- document.getElementById("kpiTotalEvents").textContent = summary.totalEvents;
- document.getElementById("kpiActiveShards").textContent = summary.activeShards;
- }
- async function loadShardsOverview() {
- const response = await fetch(`${API_BASE}/master/shards`);
- const shards = await response.json();
- const tbody = document.getElementById("tableShards");
- tbody.innerHTML = "";
- shards.forEach(shard => {
- const tr = document.createElement("tr");
- tr.innerHTML = `
- <td>${shard.shardId}</td>
- <td>${shard.name ?? "—"}</td>
- <td>${shard.location ?? "—"}</td>
- <td>
- <span class="badge ${statusBadge(shard.status)}">
- ${shard.status}
- </span>
- </td>
- <td>
- <span class="badge bg-${heartbeatColor(shard.lastHeartbeat)}">
- ${formatHeartbeat(shard.lastHeartbeat)}
- </span>
- </td>
- <td>${shard.totalEvents}</td>
- <td>${shard.totalPlayers}</td>
- `;
- if (shard.shardId === selectedShardId) {
- tr.classList.add("shard-selected");
- loadShardDetail();
- }
- tr.addEventListener("click", () => {
- selectedShardId = shard.shardId;
- console.log(selectedShardId);
- loadShardDetail();
- loadShardsOverview();
- });
- tr.style.cursor = "pointer";
- tbody.appendChild(tr);
- });
- }
- async function loadShardPlayers() {
- if (!selectedShardId) return;
- const res = await fetch(
- `${API_BASE}/master/shards/${selectedShardId}/players`
- );
- const players = await res.json();
- const tbody = document.getElementById("shardPlayers");
- tbody.innerHTML = "";
- players.forEach(p => {
- const tr = document.createElement("tr");
- tr.innerHTML = `
- <td>${p.playerId}</td>
- <td>${p.score}</td>
- <td>${p.totalActions}</td>
- `;
- tbody.appendChild(tr);
- });
- }
- async function loadShardSnapshots() {
- if (!selectedShardId) return;
- const res = await fetch(
- `${API_BASE}/master/shards/${selectedShardId}/snapshots`
- );
- const snapshots = await res.json();
- const labels = snapshots.map(s =>
- new Date(s.timestamp).toLocaleTimeString()
- );
- const data = snapshots.map(s => s.totalEvents);
- if (snapshotChart) {
- snapshotChart.destroy();
- }
- snapshotChart = new Chart(
- document.getElementById("snapshotChart"),
- {
- type: "line",
- data: {
- labels,
- datasets: [{
- label: "Total Events",
- data,
- borderColor: "blue",
- tension: 0.2
- }]
- }
- }
- );
- }
- async function loadShardEventsChart() {
- if (!selectedShardId) return;
- const res = await fetch(
- `${API_BASE}/master/shards/${selectedShardId}/events/timeseries`
- );
- const series = await res.json();
- const labels = series.map(p =>
- new Date(p.timestamp).toLocaleTimeString()
- );
- const data = series.map(p => p.totalEvents);
- if (eventsChart) eventsChart.destroy();
- eventsChart = new Chart(
- document.getElementById("eventsChart"),
- {
- type: "line",
- data: {
- labels,
- datasets: [{
- label: "Total Events (Events table)",
- data,
- borderColor: "red",
- tension: 0.2
- }]
- }
- }
- );
- }
- async function loadShardDetail() {
- await loadShardPlayers();
- await loadShardSnapshots();
- await loadShardEventsChart();
- }
- /* FUNCIONES PARA ACTUALIZAR LA PESTAÑA PLAYERS */
- async function loadPlayersGlobal() {
- const res = await fetch(`${API_BASE}/master/players`);
- const players = await res.json();
- const tbody = document.getElementById("tablePlayers");
- tbody.innerHTML = "";
- players.forEach(p => {
- const tr = document.createElement("tr");
- tr.innerHTML = `
- <td>${p.playerId}</td>
- <td>${p.globalScore}</td>
- <td>${p.shardsPlayed}</td>
- <td>${p.totalActions}</td>
- `;
- tbody.appendChild(tr);
- });
- }
- /* FUNCIONES PARA ACTUALIZAR PESTAÑA RANKINGS */
- async function loadRankingGlobal() {
- const res = await fetch(`${API_BASE}/master/rankings/global`);
- const ranking = await res.json();
- const tbody = document.getElementById("tableRankingGlobal");
- tbody.innerHTML = "";
- ranking.forEach((p, index) => {
- const tr = document.createElement("tr");
- tr.innerHTML = `
- <td>${index + 1}</td>
- <td>${p.playerId}</td>
- <td>${p.score}</td>
- `;
- tbody.appendChild(tr);
- });
- }
- async function loadRankingShard() {
- if (!selectedShardId) return;
- const res = await fetch(
- `${API_BASE}/master/rankings/shards/${selectedShardId}`
- );
- const ranking = await res.json();
- const tbody = document.getElementById("tableRankingShard");
- tbody.innerHTML = "";
- ranking.forEach((p, index) => {
- const tr = document.createElement("tr");
- tr.innerHTML = `
- <td>${index + 1}</td>
- <td>${p.playerId}</td>
- <td>${p.score}</td>
- `;
- tbody.appendChild(tr);
- });
- }
- function refreshRankings(){
- loadRankingGlobal();
- loadRankingShard();
- }
- /* FUNCIONES PARA ACTUALIZAR LA PESTAÑA EVENTS */
- async function loadEvents(page) {
- const shard = document.getElementById("filterShard").value;
- const player = document.getElementById("filterPlayer").value;
- const action = document.getElementById("filterAction").value;
- const params = new URLSearchParams({
- page: page,
- size: 15,
- shard: shard,
- player: player,
- action: action
- });
- const res = await fetch(
- `${API_BASE}/master/events?${params}`
- );
- const data = await res.json();
- eventsPage = data.number;
- eventsTotalPages = data.totalPages;
- const tbody = document.getElementById("tableEvents");
- tbody.innerHTML = "";
- data.content.forEach(e => {
- const tr = document.createElement("tr");
- tr.innerHTML = `
- <td>${new Date(e.timestamp).toLocaleTimeString()}</td>
- <td>${e.shardId}</td>
- <td>${e.playerId}</td>
- <td>${e.action}</td>
- <td>${e.pointsDelta}</td>
- `;
- tbody.appendChild(tr);
- });
- document.getElementById("eventsPageInfo").textContent =
- `Page ${eventsPage + 1} / ${eventsTotalPages}`;
- }
- function nextEventsPage() {
- if (eventsPage + 1 < eventsTotalPages) {
- loadEvents(eventsPage + 1);
- }
- }
- function prevEventsPage() {
- if (eventsPage > 0) {
- loadEvents(eventsPage - 1);
- }
- }
- /* FUNCIONES AUXILIARES */
- function heartbeatColor(lastHeartbeat) {
- if (!lastHeartbeat) return "secondary";
- const diffSeconds =
- (Date.now() - new Date(lastHeartbeat).getTime()) / 1000;
- if (diffSeconds < 15) return "success";
- if (diffSeconds < 60) return "warning";
- return "danger";
- }
- function formatHeartbeat(lastHeartbeat) {
- if (!lastHeartbeat) return "no heartbeat";
- return new Date(lastHeartbeat).toLocaleTimeString();
- }
- function statusBadge(status) {
- switch (status) {
- case "STARTED": return "bg-success";
- case "STOPPED": return "bg-danger";
- default: return "bg-secondary";
- }
- }
- // Arranque inicial
- refreshShardsTab();
- // Refresco automático
- setInterval(refreshShardsTab, REFRESH_INTERVAL_MS);
- document.querySelector('button[data-bs-target="#tabPlayers"]')
- .addEventListener("shown.bs.tab", () => {
- loadPlayersGlobal();
- });
-
- document.querySelector('button[data-bs-target="#tabRankings"]')
- .addEventListener("shown.bs.tab", () => {
- refreshRankings();
- });
- document.querySelector('button[data-bs-target="#tabEvents"]')
- .addEventListener("shown.bs.tab", () => {
- loadEvents(0);
- });
- </script>
- </body>
|