Complete workflow navigation and document page groups

This commit is contained in:
2026-10-05 19:25:55 -04:00
parent f20cfe1300
commit 9d4ff992a4
6 changed files with 33 additions and 10 deletions
+19 -3
View File
@@ -17,7 +17,7 @@ docker compose up -d --build
Open EC and create the first administrator using `EC_SETUP_TOKEN` (at least 32
characters; generate it with `openssl rand -hex 32`). After setup, remove that
token from the environment. Administrators can add administrators, operators and
viewers from **Settings → Users**. See [Users and access](docs/authentication.md)
viewers from **Administration → Users**. See [Users and access](docs/authentication.md)
for permissions, sessions, password recovery and upgrade instructions.
Operator pages and management APIs require login. Enrolled agents authenticate
@@ -32,11 +32,11 @@ including SQLite metadata and published game-file storage.
including its DLC files. There are no library names or version labels to enter.
Each upload becomes the latest files for that game after verification.
Uploads work before any host is enrolled.
2. **Hosts → Add Host**: configure a Debian 12/13 amd64 machine and run its
2. **Fleet → Hosts → Add Host**: configure a Debian 12/13 amd64 machine and run its
generated enrollment command. The host automatically prepares its Plutainer
runtime and the latest T5 and T6 files, when available. The host page
shows setup progress. Automatic agent updates are enabled by default.
3. Add the exact Plutonium server keys in **Keys**, or supply missing keys when
3. Add the exact Plutonium server keys in **Administration → Keys**, or supply missing keys when
launching.
4. Launch a server or collection. Collection previews show cached capacity
estimates; Apply saves the queue and opens its progress page. Provisioning
@@ -61,6 +61,22 @@ continue. The host page shows update status and the last check.
## Fleet and operations
The sidebar groups pages into **Fleet**, **Library**, **Operations**, **Analytics**,
and **Administration** (administrators only). On small screens, open the navigation
menu in the header. The account menu contains **My account**, including your
browser's display timezone; keyboard shortcuts are available through **?**.
**Operations → Overview** combines work that needs attention, running workflows,
and recent results. Related provisioning attempts and host commands stay grouped
under their workflow. Normal waits for empty lobbies remain under **Running**;
successful routine checks remain in **Host operation history**. Full history is
available on each workflow page.
Server details use **Live**, **Matches**, **Configuration**, **Diagnostics**, and
**History** sections. Hosts use **Overview**, **Servers**, **Setup**, and **Settings**.
Contextual Back links preserve the originating filters and views, while breadcrumbs
show each page's place in the navigation. Existing URLs and server tab links work.
The fleet registry lives in SQLite, independently of host filesystems. Status
combines agent container health, fresh game telemetry and agent-local RCON.
Unreachable hosts report unknown instance state; they do not trigger duplicate
+1 -1
View File
@@ -20,7 +20,7 @@ their access checks. HTTPS requirements still apply. See the
password. Only the holder of the token can create the first administrator.
4. Remove `EC_SETUP_TOKEN` after setup and restart/recreate the controller.
Setup remains closed once any user exists, even if the token is still set.
5. Use **Settings → Users** to create additional accounts. Share temporary
5. Use **Administration → Users** to create additional accounts. Share temporary
passwords privately; each user must change theirs on first login.
Existing installations become protected on upgrade. Existing game servers and
+1 -1
View File
@@ -1,6 +1,6 @@
# Metrics
Metrics is a primary navigation tab at `/metrics` (keyboard shortcut: `g m`).
Metrics is under **Analytics** at `/metrics` (keyboard shortcut: `g m`).
Choose the past 24 hours, 7 days, or 30 days. The page refreshes every minute.
The overview shows **Time with players** (occupied server-hours divided by known
+2 -2
View File
@@ -291,7 +291,7 @@
<BackLink fallback={basePath} />
{#if scopeComplete}<div class="scope-complete" role="status"><strong>Scope saved.</strong> {scopeComplete.composed ? `Intended configuration may change on ${scopeComplete.count} eligible servers.` : scopeComplete.detached ? `Inventory tracking detached on ${scopeComplete.detached} servers; live files remain.` : `${scopeComplete.count} servers are in the old or new scope.`} Maintenance detects resulting drift and rebuilds eligible servers when automation is enabled. <a href={scopeComplete.href}>View drift recovery →</a></div>{/if}
{#if latestVersion}<Card padding="lg"><div class="release-summary"><div><h2>Latest {artifactVersionLabel(latestVersion)}</h2><StatusBadge state={latestVersion.id === a.current_version_id ? 'promoted' : latestVersion.state} /><p>{latestVersion.notes || 'No version notes provided.'}</p></div>{#if runningBetaId !== null}<Button variant="primary" href={contextHref(`/betas/${runningBetaId}`, $page.url)}>View active beta</Button>{:else if latestVersion.id !== a.current_version_id}<Button variant="primary" href={betaSelectionHref([{ scriptId: id, versionId: latestVersion.id }])}>Test latest version</Button>{:else}<span class="dim">Latest version is promoted</span>{/if}</div></Card>{/if}
{#if latestVersion}<Card padding="lg"><div class="release-summary"><div><h2>Latest {artifactVersionLabel(latestVersion)}</h2><StatusBadge state={latestVersion.id === a.current_version_id ? 'promoted' : latestVersion.state} /><p>{latestVersion.notes || 'No version notes provided.'}</p></div>{#if runningBetaId !== null}<Button variant="primary" href={contextHref(`/betas/${runningBetaId}`, $page.url)}>View active beta</Button>{:else if latestVersion.id !== a.current_version_id}<Button variant="primary" href={contextHref(betaSelectionHref([{ scriptId: id, versionId: latestVersion.id }]), $page.url)}>Test latest version</Button>{:else}<span class="dim">Latest version is promoted</span>{/if}</div></Card>{/if}
{#if runningBetaId}
<div class="warn-banner">
@@ -382,7 +382,7 @@
<Badge variant="online" size="sm">Promoted</Badge>
{:else}
<StatusBadge state={v.state} size="sm" />
{#if runningBetaId === null}<Button size="sm" href={betaSelectionHref([{scriptId:id,versionId:v.id}])}>Test version</Button>{/if}
{#if runningBetaId === null}<Button size="sm" href={contextHref(betaSelectionHref([{scriptId:id,versionId:v.id}]), $page.url)}>Test version</Button>{/if}
{/if}
<details class="version-technical"><summary>File details</summary><span>SHA-256: {v.sha256}</span><span>Size: {v.size.toLocaleString()} bytes</span></details>
</div>
+8 -2
View File
@@ -1,4 +1,8 @@
<script lang="ts">
import { page } from '$app/stores';
import { contextHref, safeReturnHref } from '$lib/navigation-context.js';
import BackLink from '$ui/components/BackLink.svelte';
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
import { timeFormat } from '$ui/stores/time-zone.js';
import { onMount, tick } from 'svelte';
import { beforeNavigate, goto } from '$app/navigation';
@@ -167,14 +171,16 @@
const response=await fetch('/api/betas',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({scripts:picks.map(p=>({scriptId:p.scriptId,scriptVersionId:p.versionId})),servers:[...pickedServers],testPlan:parseBetaPlan(testPlan),notes:betaLabel.trim()||undefined,waitForHealth:true,waitForMatch:false})}), body=await response.json();
if(!response.ok||!body.ok)throw new Error(body.error??'Could not start beta. Review the selected versions and servers.');
draftReady=false;try{localStorage.removeItem(betaDraftKey);}catch{}allowLeave=true;
await goto(`/betas/${body.result.betaId}`);
await goto(contextHref(`/betas/${body.result.betaId}`, safeReturnHref($page.url.searchParams.get('returnTo'), '/betas')));
}catch(error){flash=(error as Error).message;}finally{openingBeta=false;}
}
</script>
<svelte:head><title>New beta | Electric Cherry</title></svelte:head>
<div class="page">
<SectionHeader title="New beta" subtitle="Choose exact file versions and test servers, then review before starting.">{#snippet actions()}<Button href="/betas" variant="ghost">Back to betas</Button>{/snippet}</SectionHeader>
<Breadcrumbs items={[{ label: 'Betas', href: '/betas' }, { label: 'New beta' }]} />
<BackLink fallback="/betas" />
<SectionHeader title="New beta" subtitle="Choose exact file versions and test servers, then review before starting." />
{#if flash}<div class="ui-error" role="alert">{flash}{#if !scripts.length}<Button size="sm" onclick={()=>initialize()}>Retry</Button>{/if}</div>{/if}
{#if $fleetError}<div class="ui-error" role="alert">{$fleetError}<Button size="sm" onclick={() => refreshFleet(true)}>Retry servers</Button></div>{/if}
{#if notice}<p role="status" class="notice">{notice}</p>{/if}
+2 -1
View File
@@ -33,7 +33,7 @@
interface Detail {
deployment: {
id: string; action: string; started_at: string;
finished_at: string | null; triggered_by: string | null;
finished_at: string | null; triggered_by: string | null; beta_id?: number | null;
};
servers: ServerRow[];
files: FileRow[];
@@ -149,6 +149,7 @@
</section>
{#if detail.watchdogIncidentId}<p class="watchdog-source">Started by Maintenance · <a href={contextHref(`/maintenance/${encodeURIComponent(detail.watchdogIncidentId)}`, $page.url)}>View incident and recovery history</a></p>{/if}
{#if detail.deployment.beta_id}<p class="watchdog-source">Started from <a href={contextHref(`/betas/${detail.deployment.beta_id}`, $page.url)}>Beta #{detail.deployment.beta_id}</a> · Review its files, checks, and test servers.</p>{/if}
{#if detail.servers.some(server => !server.server_deleted_at && ['held', 'manual'].includes(server.restart_state))}
<div><Button onclick={resume} loading={resuming} disabled={resuming}>Resume when empty</Button><p>Apply held changes and restart after the safety checks pass.</p></div>