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:
@@ -131,6 +131,61 @@ export function parseInfoLine(text: string): Record<string, string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface RconPlayer {
|
||||
num: number | null; // client slot — print dialect only
|
||||
score: number;
|
||||
ping: number;
|
||||
guid: string | null; // 32-hex — print dialect only
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract every player row from a `status` (print) or `getstatus`
|
||||
* (statusResponse) response. Returns [] when the payload has none.
|
||||
* Two dialects, same output shape:
|
||||
*
|
||||
* print: num score ping <32-hex-guid> <name-with-spaces>
|
||||
* statusResponse: score ping "<name>"
|
||||
*/
|
||||
export function parsePlayerRoster(text: string): RconPlayer[] {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const sep = lines.findIndex((l) => /^-+\s+-+/.test(l.trim()));
|
||||
if (sep !== -1) {
|
||||
const out: RconPlayer[] = [];
|
||||
// Match: num score ping guid(32-hex) name-rest
|
||||
const re = /^\s*(\d+)\s+(-?\d+)\s+(\d+)\s+([0-9a-fA-F]{16,})\s+(.+?)\s*$/;
|
||||
for (let i = sep + 1; i < lines.length; i++) {
|
||||
const m = re.exec(lines[i]);
|
||||
if (!m) continue;
|
||||
out.push({
|
||||
num: Number(m[1]),
|
||||
score: Number(m[2]),
|
||||
ping: Number(m[3]),
|
||||
guid: m[4],
|
||||
name: m[5]
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// statusResponse dialect: rows after the info line, format: score ping "name"
|
||||
const info = lines.findIndex((l) => l.startsWith('\\'));
|
||||
if (info === -1) return [];
|
||||
const out: RconPlayer[] = [];
|
||||
const re = /^\s*(-?\d+)\s+(\d+)\s+"(.*)"\s*$/;
|
||||
for (let i = info + 1; i < lines.length; i++) {
|
||||
const m = re.exec(lines[i]);
|
||||
if (!m) continue;
|
||||
out.push({
|
||||
num: null,
|
||||
score: Number(m[1]),
|
||||
ping: Number(m[2]),
|
||||
guid: null,
|
||||
name: m[3]
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function rconGetPlayerCount(server: string): Promise<number | null> {
|
||||
const ep = await endpointFor(server);
|
||||
if (!ep) return null;
|
||||
@@ -145,6 +200,7 @@ export interface RconStatus {
|
||||
players: number;
|
||||
map: string | null;
|
||||
gametype: string | null;
|
||||
roster: RconPlayer[];
|
||||
}
|
||||
|
||||
export async function rconGetStatus(server: string): Promise<RconStatus | null> {
|
||||
@@ -162,6 +218,7 @@ export async function rconGetStatus(server: string): Promise<RconStatus | null>
|
||||
return {
|
||||
players,
|
||||
map: info.mapname ?? info.sv_mapname ?? null,
|
||||
gametype: info.g_gametype ?? null
|
||||
gametype: info.g_gametype ?? null,
|
||||
roster: parsePlayerRoster(text)
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user