Always show RCON reply on server detail — raw dump + parsed values

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 11:29:32 -04:00
co-authored by Claude Opus 4.7
parent 57746eef19
commit d8ed0d2572
4 changed files with 92 additions and 5 deletions
+79
View File
@@ -384,6 +384,53 @@
</section>
{/if}
{#if effectiveSnippet || effectiveEndpoint}
<section class="panel">
<div class="rcon-hdr">
<h2>RCON reply</h2>
{#if effectiveEndpoint}
<button class="probe-btn" onclick={probeRcon} disabled={probingRcon}>
{probingRcon ? 'probing rcon…' : 'probe rcon'}
</button>
{/if}
</div>
<div class="parsed-grid">
<div class="stat">
<div class="k">endpoint</div>
<div class="v mono">{effectiveEndpoint ? `${effectiveEndpoint.host}:${effectiveEndpoint.port}` : '—'}</div>
</div>
<div class="stat">
<div class="k">players</div>
<div class="v mono">{effectivePlayers ?? '—'}</div>
</div>
<div class="stat">
<div class="k">map</div>
<div class="v mono">{effectiveMap ?? '—'}</div>
</div>
<div class="stat">
<div class="k">gametype</div>
<div class="v mono">{effectiveGametype ?? '—'}</div>
</div>
<div class="stat">
<div class="k">roster parsed</div>
<div class="v mono">{effectiveRoster?.length ?? 0} row{(effectiveRoster?.length ?? 0) === 1 ? '' : 's'}</div>
</div>
<div class="stat">
<div class="k">last seen</div>
<div class="v mono">{ago(effectiveSeen)}</div>
</div>
</div>
{#if effectiveSnippet}
<div class="raw-hdr">raw reply ({effectiveSnippet.split('\n').length} lines)</div>
<pre class="snippet">{effectiveSnippet}</pre>
{:else}
<div class="raw-hdr dim">no reply captured yet — click <strong>probe rcon</strong> above</div>
{/if}
</section>
{/if}
<section class="panel">
<h2>Roster</h2>
<PlayerRoster roster={effectiveRoster} health={effectiveHealth} />
@@ -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);