Surface raw RCON reply bytes + Probe RCON button

"unparseable reply (34 bytes)" told us Plutonium answered but hid the
actual bytes, forcing another guess-and-check round. rconStatusResult
now emits a formatRawReply() hex + ASCII dump of the raw UDP payload on
every bad-reply, persisted in a new last_reply_snippet column so it
survives page loads.

Wrong-password detection widens beyond the "bad rcon" phrasing to
"(bad|invalid|wrong)…(rcon|password|pass)"; print-body replies that
don't match get their first non-header line surfaced verbatim so at
least *some* server-supplied text reaches the UI.

Probe RCON button (POST /api/servers/[name]/probe-rcon) fires the same
code path as the periodic sweep on demand, cuts the RCON iteration loop
from 30s per attempt to sub-second, and refreshes the header/roster/
health when it succeeds. Snippet <details> auto-opens on bad-reply so
the bytes are one glance away.

migrateServers() picks up the new column automatically via the
introspection path added last commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 11:12:50 -04:00
co-authored by Claude Opus 4.7
parent a5b9733bd0
commit 8affb11b57
11 changed files with 207 additions and 18 deletions
+1
View File
@@ -19,6 +19,7 @@ export async function GET() {
...s,
roster: safeJson<RconPlayer[]>(s.last_roster),
health: classifyHealth(s.last_seen_at, s.last_error),
replySnippet: s.last_reply_snippet,
// 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,
@@ -0,0 +1,41 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { rconStatusResult } from '$lib/server/integrations/rcon.js';
import { getServer, setServerField } from '$lib/server/fleet/servers.js';
/**
* On-demand RCON probe for a specific server. Same code path as the periodic
* sweep but callable from the UI so operators can iterate on RCON password /
* port / firewall changes without waiting for the next 30s tick.
*
* Mirrors the persist logic in worker.ts so the DB stays consistent with
* whatever the caller sees.
*/
export const POST: RequestHandler = async ({ params }) => {
const name = params.name;
if (!name) return json({ ok: false, error: 'missing server name' }, { status: 400 });
const row = getServer(name);
if (!row) return json({ ok: false, error: `unknown server: ${name}` }, { status: 404 });
const result = await rconStatusResult(name);
const now = new Date().toISOString();
if (result.ok) {
setServerField(name, 'last_players', result.status.players);
setServerField(name, 'last_map', result.status.map);
setServerField(name, 'last_gametype', result.status.gametype);
setServerField(name, 'last_roster', JSON.stringify(result.status.roster));
setServerField(name, 'last_seen_at', now);
setServerField(name, 'last_checked_at', now);
setServerField(name, 'last_error', null);
setServerField(name, 'last_reply_snippet', null);
} else {
const encoded = `${result.reason}:${result.detail}`;
setServerField(name, 'last_checked_at', now);
setServerField(name, 'last_error', encoded);
setServerField(name, 'last_reply_snippet', result.rawReply ?? null);
}
return json(result);
};