Explorar el Código

get events from shards

dagarcos hace 1 mes
padre
commit
4a62ab3151

+ 226 - 0
GUI/index.html

@@ -0,0 +1,226 @@
+<!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>
+</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-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 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>
+
+
+  <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>
+
+
+      <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="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>
+
+
+    <!-- ===================================================== -->
+    <!-- TAB: PLAYERS -->
+    <!-- ===================================================== -->
+    <div class="tab-pane fade" id="tabPlayers">
+
+
+      <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>
+
+
+    </div>
+
+
+    <!-- ===================================================== -->
+    <!-- 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>
+
+
+      <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>
+
+
+    <!-- ===================================================== -->
+    <!-- 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>
+
+
+      <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>
+
+
+https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js
+</body>

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

@@ -2,8 +2,10 @@ package es.uv.dagarcos.master;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
+@EnableScheduling
 public class MasterApplication {
 
 	public static void main(String[] args) {

+ 62 - 0
src/main/java/es/uv/dagarcos/master/controller/MasterEventController.java

@@ -0,0 +1,62 @@
+package es.uv.dagarcos.master.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.dagarcos.master.domain.MasterEvent;
+import es.uv.dagarcos.master.domain.PlayerGlobal;
+import es.uv.dagarcos.master.domain.Shard;
+import es.uv.dagarcos.master.domain.ShardPlayerStats;
+import es.uv.dagarcos.master.dto.ShardEventRequest;
+import es.uv.dagarcos.master.service.MasterEventService;
+import es.uv.dagarcos.master.service.PlayerGlobalService;
+import es.uv.dagarcos.master.service.ShardPlayerStatsService;
+import es.uv.dagarcos.master.service.ShardService;
+
+@RestController
+@RequestMapping("/master/events")
+public class MasterEventController {
+
+    @Autowired
+    private ShardService shardService;
+    @Autowired
+    private PlayerGlobalService playerGlobalService;
+    @Autowired
+    private MasterEventService masterEventService;
+    @Autowired
+    private ShardPlayerStatsService shardPlayerStatsService;
+
+
+    @PostMapping
+    public ResponseEntity<Void> receiveEvent(
+            @RequestBody ShardEventRequest request) {
+
+        Shard shard = shardService.findByExternalId(request.getShardId());
+
+        PlayerGlobal player =
+                playerGlobalService.getOrCreate(request.getPlayerId());
+
+        MasterEvent event = new MasterEvent(
+                shard,
+                player,
+                request.getTimestamp(),
+                request.getEventType(),
+                request.getAction(),
+                request.getPointsDelta(),
+                request.getWinner(),
+                request.getPayload()
+        );
+
+        masterEventService.save(event);
+
+        ShardPlayerStats stats = shardPlayerStatsService.getOrCreate(shard, player);
+        shardPlayerStatsService.applyEvent(stats, event);
+
+
+        return ResponseEntity.accepted().build();
+    }
+}

+ 18 - 1
src/main/java/es/uv/dagarcos/master/controller/ShardRegistrationController.java

@@ -26,10 +26,27 @@ public class ShardRegistrationController {
         lifecycleService.registerEvent(
                 shard,
                 ShardLifecycleEventType.START,
-                "Shard registrado desde core"
+                "Shard registrado"
         );
 
         return ResponseEntity.ok().build();
     }
+
+
+    @PostMapping("/{shardId}/heartbeat")
+    public ResponseEntity<Void> heartbeat(@PathVariable String shardId) {
+
+        Shard shard = shardService.findByExternalId(shardId);
+
+        shardService.updateHeartbeat(shard);
+        lifecycleService.registerEvent(
+                shard,
+                ShardLifecycleEventType.HEARTBEAT,
+                "Heartbeat recibido"
+        );
+
+        return ResponseEntity.ok().build();
+    }
+
 }
 

+ 80 - 0
src/main/java/es/uv/dagarcos/master/dto/ShardEventRequest.java

@@ -0,0 +1,80 @@
+package es.uv.dagarcos.master.dto;
+
+import java.time.Instant;
+
+public class ShardEventRequest {
+
+    private String shardId;
+    private String playerId;
+    private Instant timestamp;
+    private String eventType;
+    private String action;
+    private Integer pointsDelta;
+    private Boolean winner;
+    private String payload;
+
+    public ShardEventRequest(){}
+
+    public ShardEventRequest(String shardId, String playerId, Instant timestamp, String eventType, String action,
+            Integer pointsDelta, Boolean winner, String payload) {
+        this.shardId = shardId;
+        this.playerId = playerId;
+        this.timestamp = timestamp;
+        this.eventType = eventType;
+        this.action = action;
+        this.pointsDelta = pointsDelta;
+        this.winner = winner;
+        this.payload = payload;
+    }
+
+    public String getShardId() {
+        return shardId;
+    }
+    public void setShardId(String shardId) {
+        this.shardId = shardId;
+    }
+    public String getPlayerId() {
+        return playerId;
+    }
+    public void setPlayerId(String playerId) {
+        this.playerId = playerId;
+    }
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+    public void setTimestamp(Instant timestamp) {
+        this.timestamp = timestamp;
+    }
+    public String getEventType() {
+        return eventType;
+    }
+    public void setEventType(String eventType) {
+        this.eventType = eventType;
+    }
+    public String getAction() {
+        return action;
+    }
+    public void setAction(String action) {
+        this.action = action;
+    }
+    public Integer getPointsDelta() {
+        return pointsDelta;
+    }
+    public void setPointsDelta(Integer pointsDelta) {
+        this.pointsDelta = pointsDelta;
+    }
+    public Boolean getWinner() {
+        return winner;
+    }
+    public void setWinner(Boolean winner) {
+        this.winner = winner;
+    }
+    public String getPayload() {
+        return payload;
+    }
+    public void setPayload(String payload) {
+        this.payload = payload;
+    }
+
+    
+}

+ 25 - 0
src/main/java/es/uv/dagarcos/master/scheduler/ShardWorldSnapshotScheduler.java

@@ -0,0 +1,25 @@
+package es.uv.dagarcos.master.scheduler;
+
+import es.uv.dagarcos.master.domain.Shard;
+import es.uv.dagarcos.master.service.ShardService;
+import es.uv.dagarcos.master.service.ShardWorldSnapshotService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ShardWorldSnapshotScheduler {
+
+    @Autowired
+    private ShardService shardService;
+    @Autowired
+    private ShardWorldSnapshotService snapshotService;
+
+    @Scheduled(fixedRateString = "${master.snapshot.interval:120000}")
+    public void takeSnapshots() {
+
+        for (Shard shard : shardService.findAll()) {
+            snapshotService.createSnapshot(shard);
+        }
+    }
+}

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

@@ -29,4 +29,25 @@ public class ShardPlayerStatsService {
         stats.setLastUpdatedAt(Instant.now());
         statsRepository.save(stats);
     }
+
+
+    public void applyEvent(ShardPlayerStats stats, MasterEvent event) {
+
+        stats.setTotalActions(stats.getTotalActions() + 1);
+
+        switch (event.getAction()) {
+            case "EXPLORE" -> stats.setExplores(stats.getExplores() + 1);
+            case "GATHER"  -> stats.setGathers(stats.getGathers() + 1);
+            case "FIGHT"   -> stats.setFights(stats.getFights() + 1);
+            case "REST"    -> stats.setRests(stats.getRests() + 1);
+        }
+
+        if (event.getPointsDelta() != null) {
+            stats.setScore(stats.getScore() + event.getPointsDelta());
+        }
+
+        stats.setLastUpdatedAt(Instant.now());
+        statsRepository.save(stats);
+    }
+
 }

+ 19 - 1
src/main/java/es/uv/dagarcos/master/service/ShardWorldSnapshotService.java

@@ -1,6 +1,8 @@
 package es.uv.dagarcos.master.service;
 
 import es.uv.dagarcos.master.domain.*;
+import es.uv.dagarcos.master.repository.MasterEventRepository;
+import es.uv.dagarcos.master.repository.PlayerGlobalRepository;
 import es.uv.dagarcos.master.repository.ShardWorldSnapshotRepository;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -10,8 +12,24 @@ public class ShardWorldSnapshotService {
 
     @Autowired
     private ShardWorldSnapshotRepository snapshotRepository;
+    @Autowired
+    private MasterEventRepository masterEventRepository;
+    @Autowired
+    private PlayerGlobalRepository playerGlobalRepository;
+
+    public void createSnapshot(Shard shard) {
+
+        long totalEvents = masterEventRepository.countByShard(shard);
+
+        long totalPlayers = playerGlobalRepository.count();
+
+        ShardWorldSnapshot snapshot =
+                new ShardWorldSnapshot(
+                        shard,
+                        totalPlayers,
+                        totalEvents
+                );
 
-    public void saveSnapshot(ShardWorldSnapshot snapshot) {
         snapshotRepository.save(snapshot);
     }
 }

+ 5 - 0
src/main/resources/META-INF/additional-spring-configuration-metadata.json

@@ -0,0 +1,5 @@
+{"properties": [{
+  "name": "master.snapshot.interval",
+  "type": "java.lang.String",
+  "description": "Intervalo de creación de los snapshots por servidor (en milisegundos)"
+}]}

+ 2 - 9
src/main/resources/application.properties

@@ -1,5 +1,6 @@
-server.port=8080
+server.port=8010
 spring.application.name=master
+master.snapshot.interval=120000
 
 # Hibernate creará las tablas cada vez que arranque el servicio
 spring.jpa.hibernate.ddl-auto=create
@@ -21,11 +22,3 @@ spring.jpa.show-sql=true
 #spring.datasource.password=
 #spring.datasource.generate-unique-name=false
 
-
-### OTRA CONFIG ###
-# En caso de quere conectar con DB externa
-#spring.datasource.url=jdbc:h2:mem:masterdb
-#spring.datasource.driver-class-name=org.h2.Driver
-#spring.datasource.username=sa
-#spring.datasource.password=
-