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);