Perk-a-cola redesign + surface RCON map/gametype/roster/health
Themes the fleet manager around the Electric Cherry perk-a-cola from Mob of the Dead — cherry-red art-deco label with electric cyan discharge. New design tokens (Alfa Slab One + Inter + JetBrains Mono, self-hosted), full redesign of layout/dashboard/cards/detail page, tokens pass on deployments/betas/pipeline/beta/diff, cherry+lightning favicon. Lights up server data that RCON was already fetching but the UI dropped. parsePlayerRoster() extracts num/score/ping/guid/name from both status dialects. Worker persists last_map/last_gametype/last_roster/last_seen_at/ last_error (new columns with idempotent boot-time ALTERs) and emits them over SSE. Fleet cards now show health dot, map · gametype, top-3 roster preview, and last-seen. Detail page adds a sortable full roster with distinct empty-lobby vs offline states. New health classifier (online/stale/offline) drives the visual state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { sweep } from './sweep.js';
|
||||
import { emit } from './bus.js';
|
||||
import type { ServerStatusEntry } from './bus.js';
|
||||
import { rconGetStatus } from '../integrations/rcon.js';
|
||||
import { listServers, seedServerRegistry } from '../fleet/servers.js';
|
||||
import { listServers, seedServerRegistry, setServerField } from '../fleet/servers.js';
|
||||
import { classifyHealth } from '../fleet/health.js';
|
||||
|
||||
const TICK_MS = 15_000;
|
||||
const STATUS_REFRESH_MS = 30_000;
|
||||
@@ -25,7 +27,7 @@ async function fanoutStatus(): Promise<void> {
|
||||
const servers = listServers();
|
||||
if (servers.length === 0) return;
|
||||
|
||||
const entries: Array<{ server: string; players: number | null; name?: string }> = new Array(servers.length);
|
||||
const entries: ServerStatusEntry[] = new Array(servers.length);
|
||||
let cursor = 0;
|
||||
|
||||
async function pump(): Promise<void> {
|
||||
@@ -34,11 +36,44 @@ async function fanoutStatus(): Promise<void> {
|
||||
if (i >= servers.length) return;
|
||||
const srv = servers[i];
|
||||
const status = await rconGetStatus(srv.name).catch(() => null);
|
||||
entries[i] = {
|
||||
server: srv.name,
|
||||
players: status?.players ?? null,
|
||||
name: status?.map ?? undefined
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (status) {
|
||||
// Persist last-observed state. last_seen_at is the health anchor.
|
||||
setServerField(srv.name, 'last_players', status.players);
|
||||
setServerField(srv.name, 'last_map', status.map);
|
||||
setServerField(srv.name, 'last_gametype', status.gametype);
|
||||
setServerField(srv.name, 'last_roster', JSON.stringify(status.roster));
|
||||
setServerField(srv.name, 'last_seen_at', now);
|
||||
setServerField(srv.name, 'last_checked_at', now);
|
||||
setServerField(srv.name, 'last_error', null);
|
||||
entries[i] = {
|
||||
server: srv.name,
|
||||
players: status.players,
|
||||
map: status.map,
|
||||
gametype: status.gametype,
|
||||
roster: status.roster,
|
||||
health: classifyHealth(now, null),
|
||||
lastSeenAt: now,
|
||||
lastError: null
|
||||
};
|
||||
} else {
|
||||
// RCON unreachable — leave last_seen_at alone (health decays
|
||||
// to offline naturally), record the failure reason.
|
||||
const error = 'rcon unreachable';
|
||||
setServerField(srv.name, 'last_checked_at', now);
|
||||
setServerField(srv.name, 'last_error', error);
|
||||
entries[i] = {
|
||||
server: srv.name,
|
||||
players: srv.last_players,
|
||||
map: srv.last_map,
|
||||
gametype: srv.last_gametype,
|
||||
roster: srv.last_roster ? safeJson(srv.last_roster) : null,
|
||||
health: classifyHealth(srv.last_seen_at, error),
|
||||
lastSeenAt: srv.last_seen_at,
|
||||
lastError: error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: STATUS_CONCURRENCY }, pump));
|
||||
@@ -46,6 +81,14 @@ async function fanoutStatus(): Promise<void> {
|
||||
emit({ type: 'server-status', entries });
|
||||
}
|
||||
|
||||
function safeJson<T>(raw: string): T | null {
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function startWorker(): void {
|
||||
if (started) return;
|
||||
started = true;
|
||||
|
||||
Reference in New Issue
Block a user