fix: honor Komodo health and prefer fresh telemetry across fleet status

This commit is contained in:
2026-09-27 15:34:30 -04:00
parent 46a89ea4b0
commit 8ea696417f
27 changed files with 488 additions and 315 deletions
+1 -1
View File
@@ -204,7 +204,7 @@
{#if view === 'cards'}
<div class="cards">{#each group.servers as server (server.name)}
<article class="server-tile" class:pinned={pinned.includes(server.name)}>
<ServerCard name={server.name} players={server.last_players} map={server.last_map ?? server.map}
<ServerCard name={server.name} telemetry={server.telemetry} players={server.last_players} map={server.last_map ?? server.map}
gametype={server.last_gametype} roster={server.roster} health={server.health}
lastSeenAt={server.last_seen_at} lastError={server.last_error} held={server.held}
activeSince={server.active_since} inactiveSince={server.inactive_since}
+2 -24
View File
@@ -2,38 +2,16 @@ import { db } from '$lib/server/db/index.js';
import { kick } from '$lib/server/pipeline/worker.js';
import { json } from '@sveltejs/kit';
import { listServers, heldByBeta } from '$lib/server/fleet/servers.js';
import { classifyHealth } from '$lib/server/fleet/health.js';
import { isProvisioning } from '$lib/server/fleet/provisioning.js';
import { serverObservation } from '$lib/server/fleet/status.js';
import { queueCreateServer } from '$lib/server/provisioning/index.js';
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
import type { RconPlayer } from '$lib/server/integrations/rcon.js';
import type { GameMode, ZombieGame } from '$lib/server/fleet/map-aliases.js';
function safeJson<T>(raw: string | null): T | null {
if (!raw) return null;
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
export async function GET() {
const rconHost = config.rcon.host;
const servers = listServers().map((s) => {
const provisioning = isProvisioning(s.name);
const ready = !provisioning && (!s.container_state || s.container_state === 'running');
return {
...s,
last_players: ready ? s.last_players : null,
last_error: ready ? s.last_error : null,
active_since: ready ? s.active_since : null,
inactive_since: ready ? s.inactive_since : null,
game_started_at: ready ? s.game_started_at : null,
game_ended_at: ready ? s.game_ended_at : null,
roster: ready ? safeJson<RconPlayer[]>(s.last_roster) : null,
health: provisioning ? 'provisioning' : classifyHealth(s.last_seen_at, s.last_error, s.container_state),
replySnippet: ready ? s.last_reply_snippet : null,
...serverObservation(s),
// 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,
+5 -3
View File
@@ -2,13 +2,15 @@ import { json } from '@sveltejs/kit';
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
import { listServers } from '$lib/server/fleet/servers.js';
import { listServerKeys } from '$lib/server/fleet/server-keys.js';
import { classifyHealth } from '$lib/server/fleet/health.js';
import { serverObservation } from '$lib/server/fleet/status.js';
import { freshStatusTimestamp } from '$lib/server-freshness.js';
import { tcpProbe } from '$lib/server/integrations/netProbe.js';
import type { SetupCheck, SystemStatus } from '$lib/system-status.js';
export async function GET({ url }: { url: URL }) {
const servers = listServers();
const readyServers = servers.filter(server => classifyHealth(server.last_seen_at, server.last_error, server.container_state) === 'online').length;
const readyServers = servers.filter(server => serverObservation(server).health === 'online').length;
const rconServers = servers.filter(server => !server.last_error && freshStatusTimestamp(server.last_seen_at, Date.now()) && server.container_state === 'running').length;
const savedKeys = listServerKeys().length;
const missing = [!config.komodo.address && 'KOMODO_ADDRESS', !config.komodo.apiKey && 'KOMODO_API_KEY', !config.komodo.apiSecret && 'KOMODO_API_SECRET'].filter(Boolean);
const connection: SetupCheck = { id: 'komodo', title: 'Komodo connection', state: komodoConfigured() ? 'unverified' : 'unconfigured', detail: komodoConfigured() ? 'Credentials configured. Run connection check to verify host reachability.' : `Set ${missing.join(', ')} in the app environment.`, href: '#setup-guide', action: 'Setup instructions' };
@@ -28,7 +30,7 @@ export async function GET({ url }: { url: URL }) {
const checks: SetupCheck[] = [
connection,
{ id: 'target', title: 'Deployment host', state: config.komodo.targetServer ? 'ready' : 'unconfigured', detail: config.komodo.targetServer ? `New servers target ${config.komodo.targetServer}.` : 'Set KOMODO_TARGET_SERVER to the exact host name shown in Komodo.', href: '#setup-guide', action: 'Setup instructions' },
{ id: 'rcon', title: 'Game server connection', state: !rconConfigured() ? 'unconfigured' : readyServers ? 'ready' : 'unverified', detail: !rconConfigured() ? 'Set RCON_PASSWORD to the password used by your game servers.' : readyServers ? `${readyServers} servers have fresh successful RCON reports.` : 'RCON password configured. A running server is needed to verify a response.', href: servers.length ? '/' : '#setup-guide', action: servers.length ? 'Review fleet' : 'Setup instructions' },
{ id: 'rcon', title: 'Game server connection', state: !rconConfigured() ? 'unconfigured' : rconServers ? 'ready' : 'unverified', detail: !rconConfigured() ? 'Set RCON_PASSWORD to the password used by your game servers.' : rconServers ? `${rconServers} servers have fresh successful RCON reports.` : 'RCON password configured. A running server is needed to verify a response.', href: servers.length ? '/' : '#setup-guide', action: servers.length ? 'Review fleet' : 'Setup instructions' },
{ id: 'game-files', title: 'Game files on the host', state: 'unverified', detail: 'T5 and T6 game-file volumes must be populated on the Komodo host. Their contents cannot be verified from this app.', href: '#game-files', action: 'Game-file instructions' },
{ id: 'keys', title: 'Saved server keys', state: savedKeys ? 'ready' : 'attention', detail: `${savedKeys} saved ${savedKeys === 1 ? 'key' : 'keys'}. Key presence does not confirm validity with Plutonium.`, href: '/server-keys', action: savedKeys ? 'Manage keys' : 'Add a key' },
{ id: 'first-server', title: 'First server ready', state: readyServers ? 'ready' : 'attention', detail: readyServers ? `${readyServers} of ${servers.length} servers are responding.` : servers.length ? 'Servers are registered but none has a fresh healthy report. Open diagnostics to investigate.' : 'Create your first server after completing the setup checks.', href: servers.length ? '/' : '/provisioning', action: servers.length ? 'Review fleet' : 'Create a server' }
+12 -6
View File
@@ -70,6 +70,10 @@
status?: { players: number; map: string | null; gametype: string | null; roster: RosterPlayer[] };
}
interface Server {
telemetry_after?: string | null;
status_source?: 'telemetry' | 'rcon' | null;
rcon_status?: import('$lib/server-observation.js').GameObservation;
telemetry?: import('$lib/telemetry.js').GameTelemetry;
name: string;
komodo_type: string; komodo_name: string; komodo_server: string | null;
game_port: number | null; rcon_host: string | null;
@@ -145,10 +149,11 @@
let effectivePlayers = $derived(fresh ? server?.last_players ?? null : null);
let effectiveMap = $derived(server?.last_map ?? null);
let effectiveGametype = $derived(server?.last_gametype ?? null);
const scoreboard = $derived($telemetryByServer.get(name));
const scoreboard = $derived(server?.telemetry ?? $telemetryByServer.get(name));
let effectiveHealth = $derived<Health>(server?.health === 'online' && !fresh ? 'stale' : server?.health ?? 'offline');
let effectiveSeen = $derived(server?.last_seen_at ?? null);
let effectiveErr = $derived(server?.last_error ?? null);
let effectiveErr = $derived(effectiveHealth === 'starting' || effectiveHealth === 'provisioning'
? null : server?.rcon_status?.last_error ?? server?.last_error ?? null);
let effectiveEndpoint = $derived<RconEndpoint | null>(server?.endpoint ?? null);
let decoded = $derived(decodeRconError(effectiveErr));
let effectiveReason = $derived<RconFailure | null>(decoded?.reason ?? null);
@@ -217,7 +222,7 @@
}
// Tab config — Diagnostics gets a warning badge when there's an issue.
let tabs = $derived([
{ id: 'scoreboard', label: 'Scoreboard', badge: scoreboard?.players.length ?? null },
{ id: 'scoreboard', label: 'Scoreboard', badge: null },
{ id: 'chat', label: 'Chat' },
{ id: 'matches', label: 'Matches' },
{ id: 'logs', label: 'Logs' },
@@ -296,7 +301,7 @@
{/if}
</div>
{#if effectiveHealth !== 'starting' && effectiveHealth !== 'provisioning'}<RoundTelemetry {name} />{/if}
{#if effectiveHealth === 'online'}<RoundTelemetry {name} report={scoreboard} since={server?.telemetry_after ?? null} />{/if}
</Card>
@@ -315,7 +320,7 @@
{#if tabError}<div class="ui-error" role="alert">{tabError}<Button size="sm" onclick={() => loadTab(name, activeTab)}>Retry</Button></div>{/if}
{#if activeTab === 'scoreboard'}
<Card padding="lg">
{#key name}<PlayerScoreboard game={fleetIdentity(server).game} report={scoreboard} mapLabel={scoreboard ? fleetIdentity({ ...server, last_map: scoreboard.map }).map : undefined} />{/key}
{#key name}<PlayerScoreboard current={fresh} after={server?.telemetry_after ?? null} game={fleetIdentity(server).game} report={scoreboard} mapLabel={scoreboard ? fleetIdentity({ ...server, last_map: scoreboard.map }).map : undefined} />{/key}
</Card>
{:else if activeTab === 'chat'}
<Card padding="lg">
@@ -373,6 +378,7 @@
{/if}
</div>
<p class="sub">Current player count: {server.status_source === 'telemetry' ? 'game telemetry' : server.status_source === 'rcon' ? 'RCON' : 'unavailable'}. RCON probes below show that connection's separate results.</p>
{#if integrations}
<div class="diag-grid">
<div class="kv"><span class="k">Endpoint</span><span class="v mono">{endpointLabel(effectiveEndpoint)}</span></div>
@@ -422,7 +428,7 @@
<div class="alert alert-ok">
<Badge variant="online" uppercase>Connected</Badge>
<span class="alert-detail">
{rconProbe.status?.players ?? '?'} players
RCON: {rconProbe.status?.players ?? '?'} players
{#if rconProbe.status?.map} · map <strong>{rconProbe.status.map}</strong>{/if}
{#if rconProbe.status?.gametype} · {rconProbe.status.gametype}{/if}
</span>