From 4bcbc6328241214f8c84d3632433f823cc805432 Mon Sep 17 00:00:00 2001 From: Bharat Kathi Date: Thu, 1 Oct 2026 21:39:05 -0700 Subject: [PATCH] feat: show online badge for players currently on the server --- .../warden/stats/ServerStatusReporter.java | 12 ++++++----- warden/api/plugin.go | 21 ++++++++++++++----- warden/api/server.go | 4 +++- warden/model/server_status.go | 3 +++ web/src/lib/warden.ts | 1 + web/src/pages/PlayersPage.tsx | 14 +++++++++++-- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/plugin/src/main/java/com/gauchoracing/warden/stats/ServerStatusReporter.java b/plugin/src/main/java/com/gauchoracing/warden/stats/ServerStatusReporter.java index c627996..41933bf 100644 --- a/plugin/src/main/java/com/gauchoracing/warden/stats/ServerStatusReporter.java +++ b/plugin/src/main/java/com/gauchoracing/warden/stats/ServerStatusReporter.java @@ -4,6 +4,8 @@ import com.gauchoracing.warden.WardenPlugin; import com.gauchoracing.warden.staff.VanishManager; import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.bukkit.Server; import org.bukkit.entity.Player; @@ -28,8 +30,8 @@ public final class ServerStatusReporter implements Listener { private final long startedAt = ManagementFactory.getRuntimeMXBean().getStartTime(); private final AtomicBoolean pending = new AtomicBoolean(); - record ServerStatus(int online, int maxPlayers, int uniquePlayers, double tps, double mspt, - long startedAt) {} + record ServerStatus(int online, List players, int maxPlayers, int uniquePlayers, + double tps, double mspt, long startedAt) {} private record Stopping(boolean stopping) {} @@ -41,13 +43,13 @@ public ServerStatusReporter(WardenPlugin plugin, VanishManager vanish) { /** Call from the main thread. Sampling is main-thread; the POST is not. */ public void report() { Server server = plugin.getServer(); - int online = 0; + List players = new ArrayList<>(); for (Player player : server.getOnlinePlayers()) { if (!vanish.isVanished(player.getUniqueId())) { - online++; + players.add(player.getUniqueId().toString()); } } - ServerStatus status = new ServerStatus(online, server.getMaxPlayers(), + ServerStatus status = new ServerStatus(players.size(), players, server.getMaxPlayers(), server.getOfflinePlayers().length, server.getTPS()[0], server.getAverageTickTime(), startedAt); plugin.runAsync(() -> { diff --git a/warden/api/plugin.go b/warden/api/plugin.go index ac4031f..91f1aa7 100644 --- a/warden/api/plugin.go +++ b/warden/api/plugin.go @@ -150,11 +150,12 @@ func normalizePlayer(rawUUID string, rawUsername string) (string, string, error) } type serverStatusReport struct { - Online int `json:"online"` - MaxPlayers int `json:"max_players"` - UniquePlayers int `json:"unique_players"` - TPS float64 `json:"tps"` - MSPT float64 `json:"mspt"` + Online int `json:"online"` + Players []string `json:"players"` + MaxPlayers int `json:"max_players"` + UniquePlayers int `json:"unique_players"` + TPS float64 `json:"tps"` + MSPT float64 `json:"mspt"` // Epoch milliseconds, as the JVM reports its own start time. StartedAt int64 `json:"started_at"` // Sent once from onDisable in place of a sample. @@ -175,9 +176,19 @@ func ReportServerStatus(c *gin.Context) { c.Status(http.StatusNoContent) return } + players := make([]string, 0, len(report.Players)) + for _, raw := range report.Players { + uuid, err := service.NormalizeUUID(raw) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + players = append(players, uuid) + } status := model.ServerStatus{ RecordedAt: time.Now(), Online: report.Online, + OnlinePlayers: players, MaxPlayers: report.MaxPlayers, UniquePlayers: report.UniquePlayers, TPS: report.TPS, diff --git a/warden/api/server.go b/warden/api/server.go index d8e799e..cd20346 100644 --- a/warden/api/server.go +++ b/warden/api/server.go @@ -11,6 +11,7 @@ import ( type serverStatusResponse struct { State string `json:"state"` Online int `json:"online"` + OnlinePlayers []string `json:"online_players"` MaxPlayers int `json:"max_players"` UniquePlayers int `json:"unique_players"` UptimeMinutes int `json:"uptime_minutes"` @@ -24,7 +25,7 @@ type serverStatusResponse struct { func GetServerStatus(c *gin.Context) { Require(c, RequestTokenExists(c)) state, status := service.CurrentServerState() - response := serverStatusResponse{State: state} + response := serverStatusResponse{State: state, OnlinePlayers: []string{}} if status != nil { response.Online = status.Online response.MaxPlayers = status.MaxPlayers @@ -32,6 +33,7 @@ func GetServerStatus(c *gin.Context) { response.TPS = status.TPS response.RecordedAt = &status.RecordedAt if state != service.ServerOffline { + response.OnlinePlayers = status.OnlinePlayers response.UptimeMinutes = int(time.Since(status.StartedAt).Minutes()) } } diff --git a/warden/model/server_status.go b/warden/model/server_status.go index 8ac55b5..70f9fd4 100644 --- a/warden/model/server_status.go +++ b/warden/model/server_status.go @@ -14,6 +14,9 @@ type ServerStatus struct { MSPT float64 `json:"mspt"` // When the game server process started; uptime is RecordedAt minus this. StartedAt time.Time `json:"started_at"` + // Visible players' UUIDs. Only the latest sample's matters, so it is + // held in memory rather than stored with the history. + OnlinePlayers []string `json:"online_players" gorm:"-"` } func (ServerStatus) TableName() string { diff --git a/web/src/lib/warden.ts b/web/src/lib/warden.ts index caec357..60a963f 100644 --- a/web/src/lib/warden.ts +++ b/web/src/lib/warden.ts @@ -118,6 +118,7 @@ export type ServerState = "active" | "empty" | "offline" export type ServerStatus = { state: ServerState online: number + online_players: string[] max_players: number unique_players: number uptime_minutes: number diff --git a/web/src/pages/PlayersPage.tsx b/web/src/pages/PlayersPage.tsx index 553be1a..85dc905 100644 --- a/web/src/pages/PlayersPage.tsx +++ b/web/src/pages/PlayersPage.tsx @@ -3,10 +3,11 @@ import { Link } from "react-router-dom" import { PageContainer, PageHeader } from "@/components/PageContainer" import { SkinFrame } from "@/components/SkinFrame" +import { Badge } from "@/components/ui/badge" import { Card, CardContent } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Skeleton } from "@/components/ui/skeleton" -import { errorMessage, type MinecraftAccount, useAccounts } from "@/lib/warden" +import { errorMessage, type MinecraftAccount, useAccounts, useServerStatus } from "@/lib/warden" function formatPlaytime(minutes: number) { const hours = Math.floor(minutes / 60) @@ -41,6 +42,8 @@ function RosterStat({ label, value }: { label: string; value: string }) { export default function PlayersPage() { const accounts = useAccounts() + const serverStatus = useServerStatus() + const onlinePlayers = new Set(serverStatus.data?.online_players ?? []) const [filter, setFilter] = useState("") const term = filter.trim().toLowerCase() @@ -116,7 +119,14 @@ export default function PlayersPage() { label="Sessions" value={account.stats ? account.stats.sessions.toLocaleString() : "—"} /> -
{formatLastSeen(seenAt(account))}
+ {onlinePlayers.has(account.uuid) ? ( + + + Online + + ) : ( +
{formatLastSeen(seenAt(account))}
+ )}