Parcourir la source

master UI - Shards overview

dagarcos il y a 1 mois
Parent
commit
bd64c1062e

+ 256 - 162
GUI/index.html

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

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

@@ -0,0 +1,28 @@
+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.service.ShardDashboardService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/master")
+@CrossOrigin // importante para abrir la GUI desde file://
+public class DashboardController {
+
+    @Autowired
+    private ShardDashboardService dashboardService;
+
+    @GetMapping("/shards")
+    public List<ShardOverviewDto> getShardsOverview() {
+        return dashboardService.getShardOverview();
+    }
+
+    @GetMapping("/dashboard/summary")
+    public DashboardSummaryDto getDashboardSummary() {
+        return dashboardService.getSummary();
+    }
+}

+ 53 - 0
src/main/java/es/uv/dagarcos/master/dto/DashboardSummaryDto.java

@@ -0,0 +1,53 @@
+package es.uv.dagarcos.master.dto;
+
+public class DashboardSummaryDto {
+
+    private long totalShards;
+    private long totalPlayers;
+    private long totalEvents;
+    private long activeShards;
+
+    public DashboardSummaryDto(long totalShards,
+                               long totalPlayers,
+                               long totalEvents,
+                               long activeShards) {
+        this.totalShards = totalShards;
+        this.totalPlayers = totalPlayers;
+        this.totalEvents = totalEvents;
+        this.activeShards = activeShards;
+    }
+
+    public long getTotalShards() {
+        return totalShards;
+    }
+
+    public void setTotalShards(long totalShards) {
+        this.totalShards = totalShards;
+    }
+
+    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;
+    }
+
+    public long getActiveShards() {
+        return activeShards;
+    }
+
+    public void setActiveShards(long activeShards) {
+        this.activeShards = activeShards;
+    }
+
+    
+}

+ 88 - 0
src/main/java/es/uv/dagarcos/master/dto/ShardOverviewDto.java

@@ -0,0 +1,88 @@
+package es.uv.dagarcos.master.dto;
+
+import java.time.Instant;
+
+public class ShardOverviewDto {
+
+    private String shardId;
+    private String name;
+    private String location;
+    private String status;
+    private Instant lastHeartbeat;
+    private long totalEvents;
+    private long totalPlayers;
+
+    public ShardOverviewDto(String shardId,
+                            String name,
+                            String location,
+                            String status,
+                            Instant lastHeartbeat,
+                            long totalEvents,
+                            long totalPlayers) {
+        this.shardId = shardId;
+        this.name = name;
+        this.location = location;
+        this.status = status;
+        this.lastHeartbeat = lastHeartbeat;
+        this.totalEvents = totalEvents;
+        this.totalPlayers = totalPlayers;
+    }
+
+    public String getShardId() {
+        return shardId;
+    }
+
+    public void setShardId(String shardId) {
+        this.shardId = shardId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getLocation() {
+        return location;
+    }
+
+    public void setLocation(String location) {
+        this.location = location;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public Instant getLastHeartbeat() {
+        return lastHeartbeat;
+    }
+
+    public void setLastHeartbeat(Instant lastHeartbeat) {
+        this.lastHeartbeat = lastHeartbeat;
+    }
+
+    public long getTotalEvents() {
+        return totalEvents;
+    }
+
+    public void setTotalEvents(long totalEvents) {
+        this.totalEvents = totalEvents;
+    }
+
+    public long getTotalPlayers() {
+        return totalPlayers;
+    }
+
+    public void setTotalPlayers(long totalPlayers) {
+        this.totalPlayers = totalPlayers;
+    }
+
+    
+}

+ 11 - 0
src/main/java/es/uv/dagarcos/master/repository/MasterEventRepository.java

@@ -4,6 +4,7 @@ import es.uv.dagarcos.master.domain.MasterEvent;
 import es.uv.dagarcos.master.domain.PlayerGlobal;
 import es.uv.dagarcos.master.domain.Shard;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 import java.time.Instant;
 import java.util.List;
@@ -20,5 +21,15 @@ public interface MasterEventRepository extends JpaRepository<MasterEvent, Long>
             Instant to
     );
 
+    long count();
+
     long countByShard(Shard shard);
+
+    @Query("""
+        SELECT e.shard.id, COUNT(e)
+        FROM MasterEvent e
+        GROUP BY e.shard.id
+    """)
+    List<Object[]> countEventsPerShard();
+
 }

+ 2 - 0
src/main/java/es/uv/dagarcos/master/repository/PlayerGlobalRepository.java

@@ -8,4 +8,6 @@ import java.util.Optional;
 public interface PlayerGlobalRepository extends JpaRepository<PlayerGlobal, Long> {
 
     Optional<PlayerGlobal> findByExternalPlayerId(String externalPlayerId);
+
+    long count();
 }

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

@@ -4,6 +4,7 @@ import es.uv.dagarcos.master.domain.PlayerGlobal;
 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 java.util.List;
 import java.util.Optional;
@@ -13,4 +14,14 @@ public interface ShardPlayerStatsRepository extends JpaRepository<ShardPlayerSta
     Optional<ShardPlayerStats> findByShardAndPlayer(Shard shard, PlayerGlobal player);
 
     List<ShardPlayerStats> findByShardOrderByScoreDesc(Shard shard);
+
+    long count();
+
+    @Query("""
+        SELECT sps.shard.id, COUNT(sps)
+        FROM ShardPlayerStats sps
+        GROUP BY sps.shard.id
+    """)
+    List<Object[]> countPlayersPerShard();
+
 }

+ 14 - 0
src/main/java/es/uv/dagarcos/master/repository/ShardRepository.java

@@ -2,7 +2,10 @@ package es.uv.dagarcos.master.repository;
 
 import es.uv.dagarcos.master.domain.Shard;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 
+import java.time.Instant;
 import java.util.Optional;
 
 public interface ShardRepository extends JpaRepository<Shard, Long> {
@@ -10,4 +13,15 @@ public interface ShardRepository extends JpaRepository<Shard, Long> {
     Optional<Shard> findByExternalId(String externalId);
 
     boolean existsByExternalId(String externalId);
+
+    long count();
+
+    @Query("""
+        SELECT COUNT(s)
+        FROM Shard s
+        WHERE s.lastHeartbeat IS NOT NULL
+          AND s.lastHeartbeat > :threshold
+    """)
+    long countActiveShards(@Param("threshold") Instant threshold);
+
 }

+ 66 - 0
src/main/java/es/uv/dagarcos/master/service/ShardDashboardService.java

@@ -0,0 +1,66 @@
+package es.uv.dagarcos.master.service;
+
+import es.uv.dagarcos.master.dto.DashboardSummaryDto;
+import es.uv.dagarcos.master.dto.ShardOverviewDto;
+import es.uv.dagarcos.master.domain.Shard;
+import es.uv.dagarcos.master.repository.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ShardDashboardService {
+
+    @Autowired
+    private ShardRepository shardRepository;
+
+    @Autowired
+    private MasterEventRepository eventRepository;
+
+    @Autowired
+    private ShardPlayerStatsRepository statsRepository;
+
+    @Autowired
+    private PlayerGlobalRepository playerRepository;
+
+    public List<ShardOverviewDto> getShardOverview() {
+
+        Map<Long, Long> eventsByShard = new HashMap<>();
+        for (Object[] row : eventRepository.countEventsPerShard()) {
+            eventsByShard.put((Long) row[0], (Long) row[1]);
+        }
+
+        Map<Long, Long> playersByShard = new HashMap<>();
+        for (Object[] row : statsRepository.countPlayersPerShard()) {
+            playersByShard.put((Long) row[0], (Long) row[1]);
+        }
+
+        return shardRepository.findAll().stream()
+                .map(shard -> new ShardOverviewDto(
+                        shard.getExternalId(),
+                        shard.getName(),
+                        shard.getLocation(),
+                        shard.getStatus().name(),
+                        shard.getLastHeartbeat(),
+                        eventsByShard.getOrDefault(shard.getId(), 0L),
+                        playersByShard.getOrDefault(shard.getId(), 0L)
+                ))
+                .toList();
+    }
+
+    public DashboardSummaryDto getSummary() {
+
+        Instant activeThreshold = Instant.now().minusSeconds(30);
+
+        return new DashboardSummaryDto(
+                shardRepository.count(),
+                playerRepository.count(),
+                eventRepository.count(),
+                shardRepository.countActiveShards(activeThreshold)
+        );
+    }
+}