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:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user