Surface structured RCON failure reasons in the fleet UI

rconGetStatus previously collapsed at least five distinct failure modes
(no password, no port, socket error, timeout, bad reply) into a single
null return, so the UI could only say "rcon unreachable" — useless when
diagnosing a real Alcatraz outage.

Refactored to rconStatusResult() returning a discriminated union with a
RconFailure reason and human-facing detail. The worker persists these as
"<reason>:<detail>" in last_error (no schema change). /api/servers now
returns the derived endpoint (host:port) per server plus a top-level
integrations block (rcon/komodo configured, timeout, default host).

Server detail page gains an RCON diagnostic panel when health is not
online: shows the exact endpoint being dialed, integration status,
timeout, a color-coded reason tag, the detail, and a targeted fix hint.
Wrong-password replies are detected via "bad rcon" text in the print
body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 09:06:27 -04:00
co-authored by Claude Opus 4.7
parent 9bff47a9a4
commit 7b4e6e0565
9 changed files with 372 additions and 72 deletions
+14 -1
View File
@@ -1,6 +1,7 @@
import { json } from '@sveltejs/kit';
import { listServers, heldByBeta } from '$lib/server/fleet/servers.js';
import { classifyHealth } from '$lib/server/fleet/health.js';
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
import type { RconPlayer } from '$lib/server/integrations/rcon.js';
function safeJson<T>(raw: string | null): T | null {
@@ -13,11 +14,23 @@ function safeJson<T>(raw: string | null): T | null {
}
export async function GET() {
const rconHost = config.rcon.host;
const servers = listServers().map((s) => ({
...s,
roster: safeJson<RconPlayer[]>(s.last_roster),
health: classifyHealth(s.last_seen_at, s.last_error),
// Derived endpoint — same logic as endpointFor() but without asking
// Komodo. If we don't have a port yet, endpoint is null.
endpoint: s.game_port ? { host: s.rcon_host ?? rconHost, port: s.game_port } : null,
held: [...heldByBeta(s.name).entries()].map(([target, slug]) => ({ target, slug }))
}));
return json({ servers });
return json({
servers,
integrations: {
rcon: rconConfigured(),
komodo: komodoConfigured(),
rconTimeoutMs: config.rcon.timeoutMs,
defaultRconHost: rconHost
}
});
}