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:
2026-09-19 21:34:34 -04:00
parent 940f81e6ca
commit 8ac420fe61
83 changed files with 2574 additions and 1190 deletions
+52
View File
@@ -0,0 +1,52 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { SystemStatus } from '$lib/system-status.js';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
import Button from '$ui/components/primitives/Button.svelte';
import Badge from '$ui/components/primitives/Badge.svelte';
let data = $state<SystemStatus | null>(null);
let loading = $state(false);
let error = $state('');
async function refresh(probe = false) {
if (loading) return;
loading = true; error = '';
try {
const response = await fetch('/api/system-status' + (probe ? '?probe=1' : ''));
if (!response.ok) throw new Error('Could not load system status. Try again.');
data = await response.json();
} catch (cause) { error = (cause as Error).message; }
finally { loading = false; }
}
onMount(() => { void refresh(); });
const labels = { ready: 'Ready', attention: 'Needs attention', unconfigured: 'Not configured', unverified: 'Not verified' };
</script>
<svelte:head><title>System status · Electric Cherry</title></svelte:head>
<section class="page">
<SectionHeader title="System status" subtitle="Check your connections and the prerequisites for a playable server.">
{#snippet actions()}<Button loading={loading} onclick={() => refresh(true)}>Check connections</Button>{/snippet}
</SectionHeader>
{#if error}<p class="ui-error" role="alert">{error}<Button size="sm" onclick={() => refresh()}>Retry</Button></p>{/if}
{#if data}
<p class="checked">Checked {new Date(data.checkedAt).toLocaleString()} · {data.readyServers} / {data.totalServers} servers ready</p>
<ol class="checks">{#each data.checks as check}<li><div class="heading"><h2>{check.title}</h2><Badge variant={check.state === 'ready' ? 'online' : check.state === 'unverified' ? 'neutral' : 'warning'}>{labels[check.state]}</Badge></div><p>{check.detail}</p>{#if check.href}<a href={check.href}>{check.action} →</a>{/if}</li>{/each}</ol>
{:else if loading}<p role="status">Checking system setup…</p>{/if}
<section id="setup-guide" class="guide"><h2>Connection setup</h2><p>Set these variables on the Electric Cherry container in Komodo, then restart the app to load the changes. Credentials are kept on the server and are never returned by this page.</p><dl><dt>Komodo</dt><dd><code>KOMODO_ADDRESS</code>, <code>KOMODO_API_KEY</code>, <code>KOMODO_API_SECRET</code>, and <code>KOMODO_TARGET_SERVER</code>. The target must match the host name in Komodo.</dd><dt>Game servers</dt><dd><code>RCON_PASSWORD</code> must match the game-server password. Set <code>RCON_HOST</code> if the default <code>host.docker.internal</code> does not reach the game host. Forward the allocated UDP game ports.</dd></dl></section>
<section id="game-files" class="guide"><h2>Game files and keys</h2><p>Populate the Komodo host's <code>t5_game_files</code> or <code>t6_game_files</code> Docker volume with the corresponding game files and required DLC. Override those names using <code>PLUTAINER_GAMEFILES_VOLUME</code> or <code>PLUTAINER_T6_GAMEFILES_VOLUME</code>. Electric Cherry and the host must see each server directory at the same absolute path.</p><p>Create a Plutonium server key for the correct game and save it for the exact game, map, and lobby in <a href="/server-keys">Server keys</a>. Use a launch preview to review the assigned ID and port before creating a server.</p></section>
</section>
<style>
.page { display: grid; gap: 20px; }
.checked { color: var(--text-secondary); font-size: var(--text-xs); margin: 0; }
.checks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; list-style: none; margin: 0; padding: 0; }
.checks li, .guide { padding: 20px; border: 1px solid var(--border-strong); border-radius: var(--radius-md); background: var(--surface); }
.heading { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; }
h2 { font-size: var(--text-md); margin: 0; }
p, dd { font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.65; }
a { font-size: var(--text-sm); }
dt { font-weight: var(--weight-semibold); margin-top: 12px; }
dd { margin: 5px 0 0; }
code { font-size: var(--text-xs); }
.guide { scroll-margin-top: 90px; }
@media (max-width: 700px) { .checks { grid-template-columns: 1fr; } }
</style>