diff --git a/docs/ui-refresh.md b/docs/ui-refresh.md index 54f4942..c484305 100644 --- a/docs/ui-refresh.md +++ b/docs/ui-refresh.md @@ -10,7 +10,7 @@ The interface keeps the existing dark palette and blue accent, with shared contr 6. **Deployment organization:** Active / Blueprints / History views. Failures and warnings link to filtered history. Destroy fleet lives in Fleet actions and retains its explicit preview. 7. **Navigation:** Fleet / Library / Operations / Betas / Settings, with section navigation and a mobile menu. Existing route URLs remain usable. 8. **Status and progress:** shared status labels and tones distinguish ordinary waiting, work in progress, attention, passing tests, and promotion. Deployment, rollout, and beta details share operation history. -9. **Server detail hub:** live status, current operations, roster, logs, effective configuration, inventory, diagnostics, and scoped rollout history. Missing or stale player counts are unknown. Inventory identifies scripts, plugins, configs, and other files. +9. **Server detail hub:** live status, current operations, scoreboard, logs, effective configuration, inventory, diagnostics, and scoped rollout history. The scoreboard shows game-reported names, copyable GUIDs, score, kills, headshots, downs, and revives, with sortable stats, map, round, and update time. Final scores remain visible until the next game starts. Missing or stale player counts are unknown. Inventory identifies scripts, plugins, configs, and other files. 10. **Dialogs and feedback:** in-app confirmations for deletion, rollback, reconciliation, and unsaved edits. File comparisons use a native modal dialog with Escape and focus restoration. Refresh errors retain loaded data and provide retry controls. 11. **Keyboard and forms:** labeled fields, roving keyboard tabs, visible focus, a skip link, and separate navigation/action targets. Removed nested buttons and buttons inside rollout links. 12. **Readability:** brighter secondary metadata, readable small labels, contrasting primary-button text, and full-opacity offline cards. diff --git a/src/lib/ui/components/PlayerRoster.svelte b/src/lib/ui/components/PlayerRoster.svelte deleted file mode 100644 index 647d628..0000000 --- a/src/lib/ui/components/PlayerRoster.svelte +++ /dev/null @@ -1,119 +0,0 @@ - - -{#if health === 'starting'} -
Server starting
-{:else if health === 'offline'} -
server unreachable
-{:else if !roster || roster.length === 0} -
lobby is empty
-{:else} - -
- - - - {#if !compact}{/if} - - - - {#if !compact}{/if} - - - - {#each sorted as p (p.guid ?? p.name + '-' + p.num)} - - {#if !compact}{/if} - - - - {#if !compact}{/if} - - {/each} - -
#playerscorepingguid
{p.num ?? ''}{p.name || '—'}{p.score}{p.ping}{p.guid ? (p.guid.length > 12 ? p.guid.slice(0, 10) + '…' : p.guid) : '—'}
-
-{/if} - - diff --git a/src/lib/ui/components/PlayerScoreboard.svelte b/src/lib/ui/components/PlayerScoreboard.svelte new file mode 100644 index 0000000..91f430c --- /dev/null +++ b/src/lib/ui/components/PlayerScoreboard.svelte @@ -0,0 +1,135 @@ + + +{#if !report} + +{:else} +
+
+ {report.event === 'game_end' ? 'Final scoreboard' : 'Latest scoreboard'} +
+

{mapLabel ?? mapTitle(report.map)}

+ {#if report.event !== 'game_start'}Round {report.round}{/if} + {#if report.event === 'game_end'}Game ended + {:else if report.event === 'game_start'}Game started{/if} +
+
+
+ {players.length} {players.length === 1 ? 'player' : 'players'} + +
+
+ + {#if !players.length} + + {:else} + +
+ + + + + + {#each columns as column (column.key)} + + {/each} + + + + {#each sorted as player} + + + {#each columns as column (column.key)} + + {/each} + + {/each} + +
{report.event === 'game_end' ? 'Final' : 'Latest'} player scores for {report.server}
Player + +
+ {player.name} +
GUID
+
+ {#if player[column.key] === null}— + {:else}{number.format(player[column.key]!)}{/if} +
+
+ {/if} +

{report.event === 'game_start' ? 'Player stats arrive with the next round report.' : report.event === 'game_end' ? 'Final stats from this game. The scoreboard resets when the next game starts.' : 'Stats update at each round change and game end.'}

+{/if} + + diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index e61caca..aa2f4d4 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -11,10 +11,10 @@ import ListFooter from '$ui/components/primitives/ListFooter.svelte'; import { page } from '$app/stores'; import DestroyServerModal from '$ui/components/DestroyServerModal.svelte'; - import { uiNow } from '$ui/stores/servers.js'; + import { telemetryByServer, uiNow } from '$ui/stores/servers.js'; import type { RosterPlayer, Health, RconEndpoint, RconFailure } from '$ui/stores/servers.js'; import { elapsedSince } from '$ui/duration.js'; - import PlayerRoster from '$ui/components/PlayerRoster.svelte'; + import PlayerScoreboard from '$ui/components/PlayerScoreboard.svelte'; import LogPanel from '$ui/components/LogPanel.svelte'; import { gametypeLabel } from '$ui/gametype.js'; import { decodeRconError, rconFixHint } from '$ui/rconError.js'; @@ -99,7 +99,7 @@ let rconProbe = $state(null); let probingRcon = $state(false); let destroyOpen = $state(false); - let activeTab = $state('roster'); + let activeTab = $state('scoreboard'); let tabLoading = $state(false); let tabError = $state(''); let historyLimit = $state(25); @@ -131,7 +131,7 @@ let effectivePlayers = $derived(fresh ? server?.last_players ?? null : null); let effectiveMap = $derived(server?.last_map ?? null); let effectiveGametype = $derived(server?.last_gametype ?? null); - let effectiveRoster = $derived(server?.roster ?? null); + const scoreboard = $derived($telemetryByServer.get(name)); let effectiveHealth = $derived(server?.health === 'online' && !fresh ? 'stale' : server?.health ?? 'offline'); let effectiveSeen = $derived(server?.last_seen_at ?? null); let effectiveErr = $derived(server?.last_error ?? null); @@ -202,7 +202,7 @@ } // Tab config — Diagnostics gets a warning badge when there's an issue. let tabs = $derived([ - { id: 'roster', label: 'Roster', badge: effectiveRoster?.length ?? null }, + { id: 'scoreboard', label: 'Scoreboard', badge: scoreboard?.players.length ?? null }, { id: 'logs', label: 'Logs' }, { id: 'config', label: 'Config' }, { id: 'inventory', label: 'Inventory', badge: inventory.length || null }, @@ -286,9 +286,9 @@
{#if tabError}{/if} - {#if activeTab === 'roster'} + {#if activeTab === 'scoreboard'} - + {#key name}{/key} {:else if activeTab === 'logs'}