|
|
@@ -14,6 +14,12 @@
|
|
|
.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>
|
|
|
@@ -90,25 +96,32 @@
|
|
|
|
|
|
<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>
|
|
|
-
|
|
|
-
|
|
|
- <h3 class="section-title">Shard Detail</h3>
|
|
|
-
|
|
|
+ <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">
|
|
|
@@ -121,7 +134,7 @@
|
|
|
<div class="col-md-6">
|
|
|
<h6>Activity Snapshot</h6>
|
|
|
<div class="alert alert-light">
|
|
|
- Gráfico temporal basado en <code>shard_world_snapshot</code>
|
|
|
+ <canvas id="snapshotChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -228,11 +241,15 @@
|
|
|
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 = 1000;
|
|
|
+ const REFRESH_INTERVAL_MS = 3000;
|
|
|
+
|
|
|
+ let selectedShardId = null;
|
|
|
+ let snapshotChart = null;
|
|
|
|
|
|
async function refreshShardsTab() {
|
|
|
try {
|
|
|
@@ -281,10 +298,86 @@
|
|
|
<td>${shard.totalPlayers}</td>
|
|
|
`;
|
|
|
|
|
|
+ if (shard.shardId === selectedShardId) {
|
|
|
+ tr.classList.add("shard-selected");
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 loadShardDetail() {
|
|
|
+ await loadShardPlayers();
|
|
|
+ await loadShardSnapshots();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /* FUNCIONES AUXILIARES */
|
|
|
function heartbeatColor(lastHeartbeat) {
|
|
|
if (!lastHeartbeat) return "secondary";
|
|
|
|