From d8ed0d2572eb44ed2c167de3e0e69a476d2aa2ab Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 12 Sep 2026 11:29:32 -0400 Subject: [PATCH] =?UTF-8?q?Always=20show=20RCON=20reply=20on=20server=20de?= =?UTF-8?q?tail=20=E2=80=94=20raw=20dump=20+=20parsed=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Player count says 1 but roster shows 0 for t5_ascension_1 — a parser mismatch (likely Plutonium's status body uses Steam IDs instead of the 16+ char hex GUIDs my roster regex expects). To diagnose these without guessing, always keep the raw RCON reply around and expose it on the detail page. rconStatusResult now includes rawReply on success too (used to only persist it on bad-reply). Worker + probe-rcon endpoint persist it to last_reply_snippet on every tick, so the byte-level truth about the reply survives page loads. New "RCON reply" panel on the detail page (visible whenever we have a snippet or an endpoint) shows: - Probe RCON button (was buried inside the offline-only diagnostic panel; now accessible when healthy too) - Parsed values in a grid: endpoint, players, map, gametype, roster row count, last seen — so mismatches between "what the parser extracted" and "what the reply actually said" are one glance apart - Raw hex+ASCII dump of the last reply so the operator can read literal player rows and see why the roster parser gave 0 Co-Authored-By: Claude Opus 4.7 --- src/lib/server/integrations/rcon.ts | 12 ++- src/lib/server/pipeline/worker.ts | 4 +- .../api/servers/[name]/probe-rcon/+server.ts | 2 +- src/routes/servers/[name]/+page.svelte | 79 +++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/lib/server/integrations/rcon.ts b/src/lib/server/integrations/rcon.ts index 892d76c..04f42b6 100644 --- a/src/lib/server/integrations/rcon.ts +++ b/src/lib/server/integrations/rcon.ts @@ -249,7 +249,14 @@ export interface RconStatus { } export type RconStatusResult = - | { ok: true; endpoint: RconEndpoint; status: RconStatus } + | { + ok: true; + endpoint: RconEndpoint; + status: RconStatus; + // Kept on success too, so operators can compare "what we parsed" + // vs "what the server actually sent" from the detail page. + rawReply: string; + } | { ok: false; endpoint: RconEndpoint | null; @@ -366,6 +373,7 @@ export async function rconStatusResult(server: string): Promise { setServerField(srv.name, 'last_seen_at', now); setServerField(srv.name, 'last_checked_at', now); setServerField(srv.name, 'last_error', null); - setServerField(srv.name, 'last_reply_snippet', null); + setServerField(srv.name, 'last_reply_snippet', result.rawReply); entries[i] = { server: srv.name, players: result.status.players, @@ -75,7 +75,7 @@ async function fanoutStatus(): Promise { lastError: null, endpoint: result.endpoint, reason: null, - replySnippet: null + replySnippet: result.rawReply }; } else { // RCON unreachable — leave last_seen_at alone (health decays diff --git a/src/routes/api/servers/[name]/probe-rcon/+server.ts b/src/routes/api/servers/[name]/probe-rcon/+server.ts index b21f9e5..3c959f8 100644 --- a/src/routes/api/servers/[name]/probe-rcon/+server.ts +++ b/src/routes/api/servers/[name]/probe-rcon/+server.ts @@ -29,7 +29,7 @@ export const POST: RequestHandler = async ({ params }) => { setServerField(name, 'last_seen_at', now); setServerField(name, 'last_checked_at', now); setServerField(name, 'last_error', null); - setServerField(name, 'last_reply_snippet', null); + setServerField(name, 'last_reply_snippet', result.rawReply); } else { const encoded = `${result.reason}:${result.detail}`; setServerField(name, 'last_checked_at', now); diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index 8b40c85..434a21c 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -384,6 +384,53 @@ {/if} + {#if effectiveSnippet || effectiveEndpoint} +
+
+

RCON reply

+ {#if effectiveEndpoint} + + {/if} +
+ +
+
+
endpoint
+
{effectiveEndpoint ? `${effectiveEndpoint.host}:${effectiveEndpoint.port}` : '—'}
+
+
+
players
+
{effectivePlayers ?? '—'}
+
+
+
map
+
{effectiveMap ?? '—'}
+
+
+
gametype
+
{effectiveGametype ?? '—'}
+
+
+
roster parsed
+
{effectiveRoster?.length ?? 0} row{(effectiveRoster?.length ?? 0) === 1 ? '' : 's'}
+
+
+
last seen
+
{ago(effectiveSeen)}
+
+
+ + {#if effectiveSnippet} +
raw reply ({effectiveSnippet.split('\n').length} lines)
+
{effectiveSnippet}
+ {:else} +
no reply captured yet — click probe rcon above
+ {/if} +
+ {/if} +

Roster

@@ -768,6 +815,38 @@ border-radius: var(--r-sm); } + .rcon-hdr { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + .rcon-hdr h2 { margin: 0; } + .parsed-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 8px; + margin-bottom: 14px; + } + .raw-hdr { + font-family: var(--font-body); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--ink-500); + margin: 0 0 6px; + } + .raw-hdr.dim { + padding: 12px 14px; + text-transform: none; + letter-spacing: 0; + font-size: 12px; + background: var(--cherry-875); + border: 1px dashed var(--cherry-700); + border-radius: var(--r-sm); + } + .raw-hdr strong { color: var(--cream); } + .snippet-details { margin-top: 10px; font-family: var(--font-mono);