Restraint-first design language: neutral gray surfaces + one blue accent
(cyan/amber/red reserved for state), 8pt spacing, softer radii, one
ambient + one raised shadow, standardized motion tokens. Alfa Slab One
demoted to BrandMark only; Inter carries all page copy.
New primitives (Button, Card, Badge, Stat, SectionHeader, Skeleton,
EmptyState, Tabs, Breadcrumbs, StatusPip, Field, Input, Select, Textarea)
plus motion utilities (tweenNumber, viewTransition) and a global toast
system. All eight route surfaces rewritten on top: Fleet gains an
overview hero, Server detail restructures into tabs, Deployments gets a
collapsible drift summary + per-server pipeline visual, and the
scripts/configs/plugins/betas flows adopt the new form primitives.
Sticky translucent nav with backdrop blur, view-transition cross-fades
between routes, staggered card entrance on Fleet, breathing status pip
halo, tabular-numeric player counts. Gmail-style g-key nav shortcuts.
Mobile stacks below 720px; prefers-reduced-motion neutralizes durations.
Legacy tokens preserved as aliases so any un-migrated code still renders.
games_sp.log is not wiped on restart, so old J; lines from prior
sessions were poisoning the anchor. Now the scanner walks the tail
backward to the LATEST InitGame and only considers lines after it,
returning an opaque cycle key (the first J; line) plus a state. The
worker stamps game_started_at/ended_at with Date.now() at observation
time instead of inferring from the log's own timestamps. Same-players
game restarts produce a new key and cleanly re-anchor. When the server
transitions to 0 players, game state is cleared server-side and the
UI also gates the timer display on activeSince.
Adds servers.game_cycle_key column with additive migration.
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.
Full server lifecycle management from Electric Cherry: pick game/mode/map/pluto
key in the Fleet page, EC provisions the deployment via Komodo, allocates a
port in 28960-28980, creates the 0o777 host dir, and starts the container.
Destroy button tears everything down (Komodo Destroy+Delete, rm -rf host dir,
transactional DB cleanup) and surfaces any partial-failure warnings inline.
Same phase cuts over to the new naming scheme (t5_zombie_cosmodrome_1 etc.).
Komodo entity names now match EC server names 1:1 (no more _/- transform), and
the map catalog carries friendly/slug/canonical for each of the 11 t5 zombie
maps. Server rows gain game_mode, map_slug, host_dir, config_file, game_id,
image, created_by, and destroyed_at columns for provisioning + audit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Beta deploys used to rename foo.gsc -> foo.OLD and write beta bytes to
a foo-beta.gsc sidecar, forcing the mod loader to know the naming
convention. Now the beta content lives at the actual target filename
(same as configs already did) and the pre-beta bytes are captured in
a new beta_file_snapshots table for rollback.
- schema.sql: new beta_file_snapshots(beta_id, server, rel_path,
prior_content BLOB, prior_sha256, prior_size). Handles scripts,
plugins, and the composed dedicated_sp.cfg uniformly.
- db/index.ts: assertSnapshotSchemeSafe boot guard refuses to start
if a legacy running/ready-promote beta is present on first upgrade.
A settings sentinel (snapshots_v1) makes it one-shot.
- beta.ts openBeta: read prior bytes, insert snapshot, overwrite
target directly.
- beta.ts rollbackBeta: restore from snapshot if had_original, else
delete the fresh file. Drop snapshot rows after restore.
- beta.ts promoteBeta: no more rm -beta / rm .OLD calls, pull prior
content from the snapshot table for the deployment audit, drop
snapshots after promote.
- files.ts: delete betaFile() and oldFile().
- drift.ts: docstring updated; no special-casing needed since beta
bytes and inventory now agree on the target rel_path.
- servers/[name]/+page.svelte: drop dead -beta/.OLD row filters.
- tests: plugin write asserts the target filename; unused
betaFile/oldFile tests removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
deleteScript: previous fix missed beta_servers.prior_version_id,
beta_server_scripts.prior_version_id, and deployments.beta_id. Cascade
from betas.script_id -> beta_servers/beta_server_scripts left dangling
prior_version_id refs, and deletions of cascaded betas dangled
deployments.beta_id. Now nulls every NO ACTION reference before the
DELETE inside a single transaction.
Scope unset no longer touches disk from the API. Instead it detaches
inventory rows for scripts/plugins so drift Pass 1 flags the on-disk
copies as untracked; configs rely on drift Pass 2's compose-diff, which
already handles scope=null naturally. The operator reconciles from
/deployments using the existing rescan + reconcile buttons — same
visible flow as any other drift, no more silent synchronous file
removal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- deleteScript: null out deployments.script_id/script_version_id/prior_version_id
and deployment_scripts.prior_version_id inside a transaction before the
cascade. These are historical audit FKs (NO ACTION), so the cascade from
script_versions was blowing up any delete of a promoted script.
- Scope endpoint fires runDriftDeployment in the background after any scope
mutation so orphan/missing files from the change get reconciled.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove redundant "manage scripts" button and pipeline sidebar from Fleet page
- Bump log tail default 200 -> 1000 lines
- Auto-refresh (5s) on deployments list/detail, betas list, server detail, and artifact detail
- Drift ignores dedicated.cfg (Plutonium MP config; regenerates if deleted)
- Extract ArtifactDetail component; mount under /scripts/[id], /configs/[id], /plugins/[id] so configs and plugins no longer land on /scripts URLs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The script detail page is now an informational card (target, scope, matched
servers, current + latest version) with edit-scope and upload-new-version
behind native <dialog> modals. The Open Beta panel is gone from the script
page entirely.
/betas gains an Open a beta section at the top. Its script dropdown filters
to entries that actually have something to test (unpromoted latest version,
no active beta), and pre-populates version + servers on script pick.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Turns promotion into a first-class deployment (action='promote') so it lands
in the deployments UI with before/after content, per-server restart tracking,
and one-click rollback. Non-beta matching-scope servers get restart_state=
pending so the existing sweep drains and restarts them the same way drift does.
Also adds a per-version state (untested/tested/test-failed) so the upload
flow can visibly signal that a beta must land before promotion, and swaps
the scope-value text box for game/map dropdowns populated from the fleet.
Beta rollback at ready-promote is now labelled Reject and marks the version
test-failed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A drift deployment scans each server's sp/ tree for files not in the
server_scripts inventory (untracked scripts left over from before EC was
managing the fleet), deletes them, and records the removed bytes in
deployment_files for audit. The existing sweep then runs the standard
wait-for-empty → Komodo restart cycle on each affected server.
To make that cycle safer, the sweep now has a second phase: after Komodo
restart succeeds we wait for komodoInspectHealthy + RCON to both come
back green before marking the deployment_server row 'done'. If either
stays broken the row stays pending and keeps retrying.
New: /api/drift (GET scan, POST run), drift panel on /deployments.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every T5 script lands in the flat sp/ directory, so the target path is
always just the uploaded filename. Removed the field from the Scripts
upload form and the server derives targetRelPath from file.name.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scripts become first-class: named, versioned, uploaded via the UI, optionally
scoped to a game or map. Any change that would land on scope-matched servers
(marking-required or new version of a scoped script) must first pass a beta.
The beta runs a per-server state machine — deploy -beta.gsc → wait empty →
Komodo restart → wait healthy → optionally wait for a real match end
(games_sp.log 'ExitLevel: executed') — before it's eligible for promotion.
Rollback is always available and restores from .OLD.
The shared/scripts mirror and its betas/ staging folder are gone. Every
server now has a tracked script inventory (server_scripts table), populated
only by explicit upload flows.
New: /scripts, /scripts/[id], /betas/[id] UI; /api/scripts + /api/betas +
/api/servers/[name]/scripts endpoints; scripts.ts, inventory.ts, beta.ts
(rewritten), beta-runner.ts, match-log.ts, komodoInspectHealthy().
Removed: push.ts, /api/push, BetaPanel, legacy importer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Swap the cherry-red primary for the logo's blue, bump body 13→15px
and h1 32→40px, and add semantic tokens (--bg-*, --accent-*, --text-*)
so future palette shifts are a single-file edit. Cherry-red survives
only for danger states and the brand cherry mark.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Firefox's console was spammed with "downloadable font: download failed
status=2147746065" for every @fontsource family — Inter, Alfa Slab One,
JetBrains Mono. Root cause: the built CSS referenced
./files/inter-latin-400-normal.woff2 but no font files were ever emitted
to build/client/_app/immutable/assets/.
Vite's CSS `@import` inlines the imported file's *text* but doesn't
crawl the resulting url() references to emit their assets. Since
tokens.css was doing `@import '@fontsource/inter/400.css'` and being
pulled into +layout.svelte via a Svelte <style> @import, the fonts
lived entirely in CSS space where Vite's asset emission never saw them.
Moved the @fontsource imports into src/lib/ui/styles/fonts.ts and
side-effect-import that module from +layout.svelte's <script>. JS
imports go through the module graph, so Vite walks the CSS, sees the
url() references, and emits the woff/woff2 binaries.
Verified: build now emits 80 font files (14 subsets × 5-6 weights)
into build/client/_app/immutable/assets/ and the CSS url()s resolve
to the correct hashed filenames in the same directory.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Now that the pipeline works end-to-end, the "RCON reply" panel with its
parsed-value grid and hex dump was pure debug scaffolding that ran on
every page load — it duplicated every field the hero already surfaces
and shoved a 13-line hex block into the fold. Deleted the whole
section; the on-demand raw dump lives inside the RCON diagnostic panel
(which still only renders when health isn't online, so it stays out of
the way on healthy servers).
Hero metadata compressed: status and last-seen pairs only render when
health isn't online, since the pulsing dot conveys "healthy + fresh"
better than a redundant text label. Mode pair hides when the gametype
isn't reported (Plutonium T5 doesn't include it in the status header).
Config panel hides the "held by beta" row when nothing is held —
matches how the deployment history section already only renders when
there's history.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>