|
|
@@ -1,226 +1,320 @@
|
|
|
<!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; }
|
|
|
- </style>
|
|
|
+ <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; }
|
|
|
+ </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>
|
|
|
+ <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-success">
|
|
|
- <div class="card-body text-center">
|
|
|
- <h6>Players</h6>
|
|
|
- <h2 id="kpiTotalPlayers">–</h2>
|
|
|
+ </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 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 class="col-md-2">
|
|
|
+ <div class="card card-kpi text-bg-warning">
|
|
|
+ <div class="card-body text-center">
|
|
|
+ <h6>Active</h6>
|
|
|
+ <h2 id="kpiActiveShards">–</h2>
|
|
|
</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="col-md-2">
|
|
|
- <div class="card card-kpi text-bg-warning">
|
|
|
- <div class="card-body text-center">
|
|
|
- <h6>Active</h6>
|
|
|
- <h2 id="kpiActiveShards">–</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>
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <!-- 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">
|
|
|
+ <div class="tab-content border border-top-0 p-3 bg-white">
|
|
|
|
|
|
|
|
|
- <!-- ===================================================== -->
|
|
|
- <!-- TAB: SHARDS -->
|
|
|
- <!-- ===================================================== -->
|
|
|
- <div class="tab-pane fade show active" id="tabShards">
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <!-- TAB: SHARDS -->
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <div class="tab-pane fade show active" id="tabShards">
|
|
|
|
|
|
|
|
|
- <h2 class="section-title">Shards Overview</h2>
|
|
|
+ <h2 class="section-title">Shards Overview</h2>
|
|
|
|
|
|
|
|
|
- <table class="table table-striped table-hover">
|
|
|
- <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>
|
|
|
+ <table class="table table-striped table-hover">
|
|
|
+ <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>
|
|
|
|
|
|
|
|
|
- <h3 class="section-title">Shard Detail</h3>
|
|
|
+ <h3 class="section-title">Shard Detail</h3>
|
|
|
|
|
|
|
|
|
- <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">
|
|
|
- Gráfico temporal basado en <code>shard_world_snapshot</code>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </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">
|
|
|
+ Gráfico temporal basado en <code>shard_world_snapshot</code>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- <!-- ===================================================== -->
|
|
|
- <!-- TAB: PLAYERS -->
|
|
|
- <!-- ===================================================== -->
|
|
|
- <div class="tab-pane fade" id="tabPlayers">
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <!-- TAB: PLAYERS -->
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <div class="tab-pane fade" id="tabPlayers">
|
|
|
|
|
|
|
|
|
- <h2 class="section-title">Players (Global)</h2>
|
|
|
+ <h2 class="section-title">Players (Global)</h2>
|
|
|
|
|
|
|
|
|
- <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>
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- <!-- ===================================================== -->
|
|
|
- <!-- TAB: RANKINGS -->
|
|
|
- <!-- ===================================================== -->
|
|
|
- <div class="tab-pane fade" id="tabRankings">
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <!-- TAB: RANKINGS -->
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <div class="tab-pane fade" id="tabRankings">
|
|
|
|
|
|
|
|
|
- <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>
|
|
|
+ <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)
|
|
|
- </div>
|
|
|
- <div class="tab-pane fade" id="rankShard">
|
|
|
- Ranking por shard usando <code>shard_player_stats</code>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div class="tab-content">
|
|
|
+ <div class="tab-pane fade show active" id="rankGlobal">
|
|
|
+ Ranking global de jugadores (agregación multi‑shard)
|
|
|
+ </div>
|
|
|
+ <div class="tab-pane fade" id="rankShard">
|
|
|
+ Ranking por shard usando <code>shard_player_stats</code>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- <!-- ===================================================== -->
|
|
|
- <!-- TAB: EVENTS -->
|
|
|
- <!-- ===================================================== -->
|
|
|
- <div class="tab-pane fade" id="tabEvents">
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <!-- TAB: EVENTS -->
|
|
|
+ <!-- ===================================================== -->
|
|
|
+ <div class="tab-pane fade" id="tabEvents">
|
|
|
|
|
|
|
|
|
- <h2 class="section-title">Event Explorer</h2>
|
|
|
+ <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="alert alert-info mb-2">
|
|
|
+ Vista paginada y filtrable de <code>master_events</code>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- <table class="table table-sm table-hover">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>Time</th>
|
|
|
- <th>Shard</th>
|
|
|
- <th>Player</th>
|
|
|
- <th>Action</th>
|
|
|
- <th>Δ</th>
|
|
|
- <th>Winner</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody id="tableEvents"></tbody>
|
|
|
- </table>
|
|
|
+ <table class="table table-sm table-hover">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>Time</th>
|
|
|
+ <th>Shard</th>
|
|
|
+ <th>Player</th>
|
|
|
+ <th>Action</th>
|
|
|
+ <th>Δ</th>
|
|
|
+ <th>Winner</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody id="tableEvents"></tbody>
|
|
|
+ </table>
|
|
|
|
|
|
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
-https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js
|
|
|
+
|
|
|
+<script
|
|
|
+ src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
|
+ integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
|
|
+ crossorigin="anonymous">
|
|
|
+</script>
|
|
|
+
|
|
|
+<script>
|
|
|
+
|
|
|
+ const API_BASE = "http://localhost:8010";
|
|
|
+ const REFRESH_INTERVAL_MS = 1000;
|
|
|
+
|
|
|
+ async function refreshShardsTab() {
|
|
|
+ try {
|
|
|
+ await loadDashboardSummary();
|
|
|
+ await loadShardsOverview();
|
|
|
+ } catch (err) {
|
|
|
+ console.error("Error actualizando Shards Overview", err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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>
|
|
|
+ `;
|
|
|
+
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ 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);
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
</body>
|