Improve operational UX, recovery flows, and action safety
Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements. Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
import StatusBadge from '$ui/components/primitives/StatusBadge.svelte';
|
||||
import ListFooter from '$ui/components/primitives/ListFooter.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import DestroyServerModal from '$ui/components/DestroyServerModal.svelte';
|
||||
import { telemetryByServer, uiNow } from '$ui/stores/servers.js';
|
||||
import type { RosterPlayer, Health, RconEndpoint, RconFailure } from '$ui/stores/servers.js';
|
||||
@@ -100,7 +101,13 @@
|
||||
let rconProbe = $state<RconProbeResult | null>(null);
|
||||
let probingRcon = $state(false);
|
||||
let destroyOpen = $state(false);
|
||||
let activeTab = $state('scoreboard');
|
||||
const sectionIds = ['scoreboard', 'logs', 'config', 'inventory', 'diagnostics', 'history'];
|
||||
const activeTab = $derived(sectionIds.includes($page.url.searchParams.get('tab') ?? '') ? $page.url.searchParams.get('tab')! : 'scoreboard');
|
||||
function changeTab(tab: string) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('tab', tab);
|
||||
void goto(url, { noScroll: true, keepFocus: true });
|
||||
}
|
||||
let tabLoading = $state(false);
|
||||
let tabError = $state('');
|
||||
let historyLimit = $state(25);
|
||||
@@ -147,10 +154,10 @@
|
||||
let effectiveInactiveSince = $derived<string | null>(server?.inactive_since ?? null);
|
||||
let effectiveGameStartedAt = $derived<string | null>(server?.game_started_at ?? null);
|
||||
let effectiveGameEndedAt = $derived<string | null>(server?.game_ended_at ?? null);
|
||||
let activeElapsed = $derived(effectiveHealth !== 'starting' && effectiveActiveSince ? elapsedSince(effectiveActiveSince, $uiNow) : null);
|
||||
let inactiveElapsed = $derived(effectiveHealth !== 'starting' && effectiveInactiveSince ? elapsedSince(effectiveInactiveSince, $uiNow) : null);
|
||||
let activeElapsed = $derived(fresh && effectiveActiveSince ? elapsedSince(effectiveActiveSince, $uiNow) : null);
|
||||
let inactiveElapsed = $derived(fresh && effectiveInactiveSince ? elapsedSince(effectiveInactiveSince, $uiNow) : null);
|
||||
let gameInProgress = $derived(!!effectiveGameStartedAt && !effectiveGameEndedAt && !!effectiveActiveSince);
|
||||
let gameElapsed = $derived(effectiveHealth !== 'starting' && gameInProgress ? elapsedSince(effectiveGameStartedAt, $uiNow) : null);
|
||||
let gameElapsed = $derived(fresh && gameInProgress ? elapsedSince(effectiveGameStartedAt, $uiNow) : null);
|
||||
|
||||
let hasDiagnosticIssue = $derived(effectiveHealth !== 'online' && effectiveHealth !== 'starting');
|
||||
|
||||
@@ -212,6 +219,7 @@
|
||||
]);
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{serverTitle(name)} · {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} · Electric Cherry</title></svelte:head>
|
||||
<DestroyServerModal bind:open={destroyOpen} {name} players={effectivePlayers} />
|
||||
|
||||
{#if server}
|
||||
@@ -223,9 +231,7 @@
|
||||
<Breadcrumbs items={[{ label: 'Fleet', href: '/' }, { label: serverTitle(name) }]} />
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
<Button variant="danger" onclick={() => destroyOpen = true}>
|
||||
Destroy
|
||||
</Button>
|
||||
<details class="server-actions"><summary>Server actions</summary><Button variant="danger" onclick={() => destroyOpen = true}>Queue server removal</Button></details>
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
|
||||
@@ -280,9 +286,15 @@
|
||||
</Card>
|
||||
|
||||
{#if $fleetError}<div class="ui-error" role="alert">{$fleetError}<Button size="sm" onclick={refreshFleet}>Retry</Button></div>{/if}
|
||||
{#if hasDiagnosticIssue}
|
||||
<section class="connection-issue" aria-label="Server connection issue">
|
||||
<div><strong>{effectiveHealth === 'stale' ? 'Server report is outdated' : 'Server is not responding'}</strong><p>Player count is unknown. {effectiveSeen ? `Last successful report ${ago(effectiveSeen, $uiNow)}.` : 'No successful report has been received.'} {integrations && !integrations.rcon ? 'RCON is not configured.' : ''}</p></div>
|
||||
<div class="issue-actions"><Button size="sm" onclick={() => changeTab('diagnostics')}>Diagnose connection</Button><Button size="sm" variant="ghost" onclick={() => changeTab('logs')}>Open logs</Button>{#if integrations && (!integrations.rcon || !integrations.komodo)}<Button size="sm" href="/settings">Check system setup</Button>{/if}</div>
|
||||
</section>
|
||||
{/if}
|
||||
{#if operations.length}<section class="ui-panel"><h2>Current operations</h2><div class="server-operations">{#each operations as operation (operation.key)}<a href={operation.href}><strong>{operation.label}</strong><span>{operation.detail}</span><StatusBadge state={operation.state} /></a>{/each}</div></section>{/if}
|
||||
<div class="tabs-row">
|
||||
<Tabs {tabs} bind:value={activeTab} ariaLabel="Server sections" idPrefix="server-tabs" panelId="server-panel" />
|
||||
<Tabs {tabs} value={activeTab} onchange={changeTab} ariaLabel="Server sections" idPrefix="server-tabs" panelId="server-panel" />
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" class:diagnostics={activeTab === 'diagnostics'} role="tabpanel" id="server-panel" aria-labelledby={"server-tabs-" + activeTab}>
|
||||
@@ -329,9 +341,6 @@
|
||||
</Card>
|
||||
<ListFooter total={inventory.length} bind:limit={inventoryLimit} label="files" />
|
||||
{:else if activeTab === 'diagnostics'}
|
||||
{#key name}
|
||||
<TelemetryDiagnostics server={name} />
|
||||
{/key}
|
||||
<Card padding="lg">
|
||||
<div class="panel-head">
|
||||
<h2>RCON diagnostic</h2>
|
||||
@@ -493,6 +502,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
<div id="telemetry">{#key name}<TelemetryDiagnostics server={name} />{/key}</div>
|
||||
{:else if activeTab === 'history'}
|
||||
<Card padding="lg">
|
||||
<div class="panel-head">
|
||||
@@ -537,6 +547,12 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.server-actions { text-align: right; }
|
||||
.server-actions summary { cursor: pointer; padding: 9px 12px; border: 1px solid var(--border-strong); border-radius: var(--radius-md); font-size: var(--text-sm); }
|
||||
.server-actions[open] summary { margin-bottom: 10px; }
|
||||
.connection-issue { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; padding: 16px; border: 1px solid var(--state-warning); border-radius: var(--radius-md); background: var(--surface); }
|
||||
.connection-issue p { margin: 5px 0 0; color: var(--text-secondary); font-size: var(--text-sm); }
|
||||
.issue-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.server-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user