Explorar o código

UI shard details and plot

Daniel Garcia Costa hai 1 mes
pai
achega
ce9248d1ae

+ 114 - 21
GUI/index.html

@@ -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";
 

+ 20 - 0
src/main/java/es/uv/dagarcos/master/controller/DashboardController.java

@@ -2,7 +2,12 @@ package es.uv.dagarcos.master.controller;
 
 import es.uv.dagarcos.master.dto.DashboardSummaryDto;
 import es.uv.dagarcos.master.dto.ShardOverviewDto;
+import es.uv.dagarcos.master.dto.ShardPlayerStatsDto;
+import es.uv.dagarcos.master.dto.ShardWorldSnapshotDto;
 import es.uv.dagarcos.master.service.ShardDashboardService;
+import es.uv.dagarcos.master.service.ShardPlayerStatsService;
+import es.uv.dagarcos.master.service.ShardWorldSnapshotService;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -15,6 +20,10 @@ public class DashboardController {
 
     @Autowired
     private ShardDashboardService dashboardService;
+    @Autowired 
+    private ShardPlayerStatsService shardPlayerStatsService;
+    @Autowired 
+    private ShardWorldSnapshotService shardWorldSnapshotService;
 
     @GetMapping("/shards")
     public List<ShardOverviewDto> getShardsOverview() {
@@ -25,4 +34,15 @@ public class DashboardController {
     public DashboardSummaryDto getDashboardSummary() {
         return dashboardService.getSummary();
     }
+
+    @GetMapping("/shards/{shardId}/players")
+    public List<ShardPlayerStatsDto> getShardPlayers(@PathVariable String shardId) {
+        return shardPlayerStatsService.getRankingForShard(shardId);
+    }
+
+    @GetMapping("/shards/{shardId}/snapshots")
+    public List<ShardWorldSnapshotDto> getShardSnapshots(@PathVariable String shardId){
+        return shardWorldSnapshotService.getSnapshots(shardId);
+    }
+
 }

+ 40 - 0
src/main/java/es/uv/dagarcos/master/dto/ShardPlayerStatsDto.java

@@ -0,0 +1,40 @@
+package es.uv.dagarcos.master.dto;
+
+public class ShardPlayerStatsDto {
+
+    private String playerId;
+    private long score;
+    private long totalActions;
+
+    public ShardPlayerStatsDto(String playerId, long score, long totalActions) {
+        this.playerId = playerId;
+        this.score = score;
+        this.totalActions = totalActions;
+    }
+
+    public String getPlayerId() {
+        return playerId;
+    }
+
+    public void setPlayerId(String playerId) {
+        this.playerId = playerId;
+    }
+
+    public long getScore() {
+        return score;
+    }
+
+    public void setScore(long score) {
+        this.score = score;
+    }
+
+    public long getTotalActions() {
+        return totalActions;
+    }
+
+    public void setTotalActions(long totalActions) {
+        this.totalActions = totalActions;
+    }
+
+    
+}

+ 46 - 0
src/main/java/es/uv/dagarcos/master/dto/ShardWorldSnapshotDto.java

@@ -0,0 +1,46 @@
+package es.uv.dagarcos.master.dto;
+
+import java.time.Instant;
+
+public class ShardWorldSnapshotDto {
+
+    private Instant timestamp;
+    private long totalPlayers;
+    private long totalEvents;
+
+    public ShardWorldSnapshotDto(
+            Instant timestamp,
+            long totalPlayers,
+            long totalEvents) {
+
+        this.timestamp = timestamp;
+        this.totalPlayers = totalPlayers;
+        this.totalEvents = totalEvents;
+    }
+
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(Instant timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public long getTotalPlayers() {
+        return totalPlayers;
+    }
+
+    public void setTotalPlayers(long totalPlayers) {
+        this.totalPlayers = totalPlayers;
+    }
+
+    public long getTotalEvents() {
+        return totalEvents;
+    }
+
+    public void setTotalEvents(long totalEvents) {
+        this.totalEvents = totalEvents;
+    }
+
+    
+}

+ 9 - 0
src/main/java/es/uv/dagarcos/master/repository/ShardPlayerStatsRepository.java

@@ -5,6 +5,7 @@ import es.uv.dagarcos.master.domain.Shard;
 import es.uv.dagarcos.master.domain.ShardPlayerStats;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
 import java.util.List;
 import java.util.Optional;
@@ -24,4 +25,12 @@ public interface ShardPlayerStatsRepository extends JpaRepository<ShardPlayerSta
     """)
     List<Object[]> countPlayersPerShard();
 
+    @Query("""
+        SELECT s
+        FROM ShardPlayerStats s
+        WHERE s.shard.externalId = :shardId
+        ORDER BY s.score DESC
+    """)
+    List<ShardPlayerStats> findByShardOrdered(@Param("shardId") String shardId);
+
 }

+ 5 - 0
src/main/java/es/uv/dagarcos/master/repository/ShardWorldSnapshotRepository.java

@@ -9,4 +9,9 @@ import java.util.List;
 public interface ShardWorldSnapshotRepository extends JpaRepository<ShardWorldSnapshot, Long> {
 
     List<ShardWorldSnapshot> findByShardOrderBySnapshotAtDesc(Shard shard);
+
+    List<ShardWorldSnapshot> findByShardExternalIdOrderBySnapshotAtAsc(String shardId);
+
+    List<ShardWorldSnapshot> findByShardOrderBySnapshotAtAsc(Shard shard);
+
 }

+ 14 - 0
src/main/java/es/uv/dagarcos/master/service/ShardPlayerStatsService.java

@@ -1,11 +1,13 @@
 package es.uv.dagarcos.master.service;
 
 import es.uv.dagarcos.master.domain.*;
+import es.uv.dagarcos.master.dto.ShardPlayerStatsDto;
 import es.uv.dagarcos.master.repository.ShardPlayerStatsRepository;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.Instant;
+import java.util.List;
 
 @Service
 public class ShardPlayerStatsService {
@@ -50,4 +52,16 @@ public class ShardPlayerStatsService {
         statsRepository.save(stats);
     }
 
+    public List<ShardPlayerStatsDto> getRankingForShard(String shardId) {
+
+        return statsRepository.findByShardOrdered(shardId)
+                .stream()
+                .map(s -> new ShardPlayerStatsDto(
+                        s.getPlayer().getExternalPlayerId(),
+                        s.getScore(),
+                        s.getTotalActions()
+                ))
+                .toList();
+    }
+
 }

+ 22 - 0
src/main/java/es/uv/dagarcos/master/service/ShardWorldSnapshotService.java

@@ -1,9 +1,13 @@
 package es.uv.dagarcos.master.service;
 
 import es.uv.dagarcos.master.domain.*;
+import es.uv.dagarcos.master.dto.ShardWorldSnapshotDto;
 import es.uv.dagarcos.master.repository.MasterEventRepository;
 import es.uv.dagarcos.master.repository.PlayerGlobalRepository;
 import es.uv.dagarcos.master.repository.ShardWorldSnapshotRepository;
+
+import java.util.List;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -11,6 +15,8 @@ import org.springframework.stereotype.Service;
 public class ShardWorldSnapshotService {
 
     @Autowired
+    private ShardService shardService;
+    @Autowired
     private ShardWorldSnapshotRepository snapshotRepository;
     @Autowired
     private MasterEventRepository masterEventRepository;
@@ -32,4 +38,20 @@ public class ShardWorldSnapshotService {
 
         snapshotRepository.save(snapshot);
     }
+
+    public List<ShardWorldSnapshotDto> getSnapshots(String shardId) {
+        
+        Shard shard = shardService.findByExternalId(shardId);
+
+        return snapshotRepository
+                .findByShardOrderBySnapshotAtAsc(shard)
+                .stream()
+                .map(s -> new ShardWorldSnapshotDto(
+                        s.getSnapshotAt(),
+                        s.getTotalPlayers(),
+                        s.getTotalEvents()
+                ))
+                .toList();
+
+    }
 }

+ 1 - 1
src/main/resources/application.properties

@@ -13,7 +13,7 @@ spring.sql.init.mode=always
 # Consola web de H2
 spring.h2.console.enabled=true
 # Mostrar log de consultas por consola
-spring.jpa.show-sql=true
+spring.jpa.show-sql=false
 
 ### CONFIGURACIÓN DEL DATASOURCE ###
 #spring.datasource.url=jdbc:h2:mem:masterdb