Add active/inactive/game-time timers to fleet + server pages
Fleet cards and the per-server detail hero now show three live-ticking timers: Active (since players > 0), Inactive (since server emptied), and Game (since InitGame in games_sp.log, back-computed from the log's own MM:SS prefix so it stays accurate across restarts). Anchors are persisted on `servers` and fanned out via the SSE bus; a single 1s ticker feeds every card. Idle map cycles are filtered out so the game timer doesn't flicker.
This commit is contained in:
+18
-2
@@ -21,6 +21,10 @@
|
||||
health: Health;
|
||||
endpoint: { host: string; port: number } | null;
|
||||
held: HeldRow[];
|
||||
active_since: string | null;
|
||||
inactive_since: string | null;
|
||||
game_started_at: string | null;
|
||||
game_ended_at: string | null;
|
||||
}
|
||||
|
||||
let servers = $state<ServerRow[]>([]);
|
||||
@@ -47,7 +51,11 @@
|
||||
roster: fallback.roster,
|
||||
health: fallback.health,
|
||||
lastSeenAt: fallback.last_seen_at,
|
||||
lastError: fallback.last_error
|
||||
lastError: fallback.last_error,
|
||||
activeSince: fallback.active_since,
|
||||
inactiveSince: fallback.inactive_since,
|
||||
gameStartedAt: fallback.game_started_at,
|
||||
gameEndedAt: fallback.game_ended_at
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -57,7 +65,11 @@
|
||||
roster: live.roster,
|
||||
health: live.health,
|
||||
lastSeenAt: live.lastSeenAt,
|
||||
lastError: live.lastError
|
||||
lastError: live.lastError,
|
||||
activeSince: live.activeSince,
|
||||
inactiveSince: live.inactiveSince,
|
||||
gameStartedAt: live.gameStartedAt,
|
||||
gameEndedAt: live.gameEndedAt
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -91,6 +103,10 @@
|
||||
lastSeenAt={live.lastSeenAt}
|
||||
lastError={live.lastError}
|
||||
held={s.held}
|
||||
activeSince={live.activeSince}
|
||||
inactiveSince={live.inactiveSince}
|
||||
gameStartedAt={live.gameStartedAt}
|
||||
gameEndedAt={live.gameEndedAt}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { statusByServer } from '$ui/stores/servers.js';
|
||||
import { statusByServer, uiNow } from '$ui/stores/servers.js';
|
||||
import type { RosterPlayer, Health, RconEndpoint, RconFailure } from '$ui/stores/servers.js';
|
||||
import { elapsedSince } from '$ui/duration.js';
|
||||
import StatusDot from '$ui/components/StatusDot.svelte';
|
||||
import PlayerRoster from '$ui/components/PlayerRoster.svelte';
|
||||
import LogPanel from '$ui/components/LogPanel.svelte';
|
||||
@@ -78,6 +79,10 @@
|
||||
endpoint: RconEndpoint | null;
|
||||
replySnippet: string | null;
|
||||
held: Array<{ target: string; betaId: number }>;
|
||||
active_since: string | null;
|
||||
inactive_since: string | null;
|
||||
game_started_at: string | null;
|
||||
game_ended_at: string | null;
|
||||
}
|
||||
interface InventoryEntry {
|
||||
relPath: string;
|
||||
@@ -159,6 +164,29 @@
|
||||
rconProbe?.rawReply ?? live?.replySnippet ?? server?.replySnippet ?? null
|
||||
);
|
||||
|
||||
// Occupancy + match timers — live SSE first, DB row as fallback so the
|
||||
// display works on initial load before the first SSE event arrives.
|
||||
let effectiveActiveSince = $derived<string | null>(
|
||||
live?.activeSince ?? server?.active_since ?? null
|
||||
);
|
||||
let effectiveInactiveSince = $derived<string | null>(
|
||||
live?.inactiveSince ?? server?.inactive_since ?? null
|
||||
);
|
||||
let effectiveGameStartedAt = $derived<string | null>(
|
||||
live?.gameStartedAt ?? server?.game_started_at ?? null
|
||||
);
|
||||
let effectiveGameEndedAt = $derived<string | null>(
|
||||
live?.gameEndedAt ?? server?.game_ended_at ?? null
|
||||
);
|
||||
let activeElapsed = $derived(effectiveActiveSince ? elapsedSince(effectiveActiveSince, $uiNow) : null);
|
||||
let inactiveElapsed = $derived(effectiveInactiveSince ? elapsedSince(effectiveInactiveSince, $uiNow) : null);
|
||||
let gameInProgress = $derived(!!effectiveGameStartedAt && !effectiveGameEndedAt);
|
||||
let gameElapsed = $derived(gameInProgress ? elapsedSince(effectiveGameStartedAt, $uiNow) : null);
|
||||
let showTiming = $derived(
|
||||
effectiveHealth !== 'offline' &&
|
||||
(activeElapsed !== null || inactiveElapsed !== null || gameElapsed !== null)
|
||||
);
|
||||
|
||||
function ago(iso: string | null): string {
|
||||
if (!iso) return 'never';
|
||||
const d = Date.now() - new Date(iso).getTime();
|
||||
@@ -321,6 +349,27 @@
|
||||
<span class="pair"><span class="k">last seen</span> <span class="v">{ago(effectiveSeen)}</span></span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showTiming}
|
||||
<div class="hero-timing">
|
||||
{#if activeElapsed}
|
||||
<div class="tblock active">
|
||||
<div class="tlabel">Active</div>
|
||||
<div class="tvalue">{activeElapsed}</div>
|
||||
</div>
|
||||
{:else if inactiveElapsed}
|
||||
<div class="tblock inactive">
|
||||
<div class="tlabel">Inactive</div>
|
||||
<div class="tvalue">{inactiveElapsed}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if gameElapsed}
|
||||
<div class="tblock game">
|
||||
<div class="tlabel">Game</div>
|
||||
<div class="tvalue">{gameElapsed}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if effectiveHealth !== 'online' && integrations}
|
||||
@@ -677,6 +726,32 @@
|
||||
.v.health-stale { color: var(--amber-500); }
|
||||
.v.health-offline { color: var(--danger-500); }
|
||||
|
||||
.hero-timing {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32px;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.tblock { display: flex; flex-direction: column; gap: 4px; }
|
||||
.tlabel {
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--ink-500);
|
||||
}
|
||||
.tvalue {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-lg);
|
||||
color: var(--cream);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.tblock.active .tvalue { color: var(--spark-500); }
|
||||
.tblock.inactive .tvalue { color: var(--ink-300); }
|
||||
.tblock.game .tvalue { color: var(--accent, var(--spark-500)); }
|
||||
|
||||
.panel {
|
||||
background: var(--bg-nav);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user