Commit Graph
122 Commits
Author SHA1 Message Date
josh 3d111880d3 Add active/inactive/game-time timers to fleet + server pages
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.
2026-09-13 21:45:54 -04:00
josh 79b2ba1464 corrected map aliases 2026-09-13 19:30:17 -04:00
joshandClaude Opus 4.7 be9cf10e02 Wait for container teardown before deleting Komodo deployment entity
execute/DestroyDeployment queues the container removal and returns before
it actually completes. write/DeleteDeployment refuses while a container
still exists for the deployment, so the entity survived every teardown.

Poll komodoInspectHealthy between the two calls until it stops reporting
a running container (up to 30s), then delete the entity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-13 19:01:13 -04:00
joshandClaude Opus 4.7 58f48c6906 Fix container port mapping to match host port
PLUTAINER_PORT sets the port the container listens on, so the mapping
should be <port>:<port>, not <port>:28960 — otherwise the container is
bound to a port nothing inside is listening on.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-13 18:47:24 -04:00
joshandClaude Opus 4.7 13a29e5ace Add deploy + destroy server flow; migrate to t5_<mode>_<slug>_<n> naming
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>
2026-09-13 18:38:42 -04:00
joshandClaude Opus 4.7 8c15cae80d Replace -beta sidecar and .OLD backup with DB snapshots
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>
2026-09-13 17:57:43 -04:00
joshandClaude Opus 4.7 30b0d1d4da Fix delete FK exhaustively; unset-scope surfaces as drift
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>
2026-09-12 23:59:43 -04:00
joshandClaude Opus 4.7 34dc2ff00f Fix delete-script FK error; trigger drift on scope change
- 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>
2026-09-12 23:46:56 -04:00
joshandClaude Opus 4.7 f716036d89 Small UI cleanup, auto-refresh, and per-kind artifact routes
- 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>
2026-09-12 23:30:04 -04:00
josh f10f0cefef drift update 2026-09-12 23:15:14 -04:00
josh 6ae9004ed2 Drift now covers all three destinations 2026-09-12 19:16:14 -04:00
josh 8c40bbeff1 config and plugin 2026-09-12 19:11:44 -04:00
josh 85bcb05a7d multi script beta 2026-09-12 18:23:56 -04:00
joshandClaude Opus 4.7 1afdfddc87 Promote as a real deployment; version state and scope dropdowns
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>
2026-09-12 14:24:35 -04:00
joshandClaude Opus 4.7 edfe4dfc09 Add drift detection and removal as a real deployment
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>
2026-09-12 13:44:52 -04:00
joshandClaude Opus 4.7 590f803d93 Auto-derive server.game and server.map from the server name
Server folders encode the game and map (e.g. t5_ascension_1 = game t5, map
alias ascension, lobby 1). A T5_MAP_ALIASES table translates the friendly
alias to the canonical mapname RCON reports (ascension → zombie_cosmodrome),
covering every base + Rezurrection map. seedServerRegistry now populates
game and map at insert time and backfills existing NULL rows on every boot;
unknown aliases log a warning so we know when to add them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-12 13:35:43 -04:00
joshandClaude Opus 4.7 93d5a8e2e5 Rework as scripts-centric, upload-only, gated beta lifecycle
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>
2026-09-12 13:25:06 -04:00
joshandClaude Opus 4.7 6aae72f508 Add per-server log viewer (console.log + games_sp.log)
New Logs section on the server detail page — tabbed viewer with
auto-refresh every 5s (toggleable), sticky-to-bottom scrolling, and a
manual refresh button. Two whitelisted files per server:

  /root/zombies/t5/<name>/logs/console.log
  /root/zombies/t5/<name>/logs/games_sp.log

Backend:
  - src/lib/server/fleet/logs.ts — safe path resolution + tailLog()
    that reads only the last 256 KB of the file, splits on newlines,
    drops the (potentially partial) first line if we didn't start at
    byte 0, and returns the last N lines with size + mtime + truncated.
    Two path-traversal guards: server directory must be a real
    subdirectory of SERVERS_ROOT, and the target must resolve inside
    it.
  - GET /api/servers/[name]/logs/[file]?lines=200 — whitelisted keys
    only (console, games_sp), 400 on unknown keys, ok:true+missing:
    true when the log file hasn't been written yet.

Frontend:
  - LogPanel.svelte with tabs, auto-refresh checkbox, size/mtime meta,
    and a <pre> body that snaps to bottom on refresh unless the user
    has scrolled up.
  - 7 unit tests covering path safety (including .. traversal) and
    tail behavior (missing file → null, small file, oversized file
    → truncated, trailing newline handling).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-12 11:50:19 -04:00
joshandClaude Opus 4.7 8affb11b57 Surface raw RCON reply bytes + Probe RCON button
"unparseable reply (34 bytes)" told us Plutonium answered but hid the
actual bytes, forcing another guess-and-check round. rconStatusResult
now emits a formatRawReply() hex + ASCII dump of the raw UDP payload on
every bad-reply, persisted in a new last_reply_snippet column so it
survives page loads.

Wrong-password detection widens beyond the "bad rcon" phrasing to
"(bad|invalid|wrong)…(rcon|password|pass)"; print-body replies that
don't match get their first non-header line surfaced verbatim so at
least *some* server-supplied text reaches the UI.

Probe RCON button (POST /api/servers/[name]/probe-rcon) fires the same
code path as the periodic sweep on demand, cuts the RCON iteration loop
from 30s per attempt to sub-second, and refreshes the header/roster/
health when it succeeds. Snippet <details> auto-opens on bad-reply so
the bytes are one glance away.

migrateServers() picks up the new column automatically via the
introspection path added last commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-12 11:12:50 -04:00
joshandClaude Opus 4.7 9bff47a9a4 Perk-a-cola redesign + surface RCON map/gametype/roster/health
Themes the fleet manager around the Electric Cherry perk-a-cola from Mob of
the Dead — cherry-red art-deco label with electric cyan discharge. New
design tokens (Alfa Slab One + Inter + JetBrains Mono, self-hosted), full
redesign of layout/dashboard/cards/detail page, tokens pass on
deployments/betas/pipeline/beta/diff, cherry+lightning favicon.

Lights up server data that RCON was already fetching but the UI dropped.
parsePlayerRoster() extracts num/score/ping/guid/name from both status
dialects. Worker persists last_map/last_gametype/last_roster/last_seen_at/
last_error (new columns with idempotent boot-time ALTERs) and emits them
over SSE. Fleet cards now show health dot, map · gametype, top-3 roster
preview, and last-seen. Detail page adds a sortable full roster with
distinct empty-lobby vs offline states. New health classifier
(online/stale/offline) drives the visual state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-12 08:20:54 -04:00
josh 854f5532a9 Replace IW4MAdmin with direct RCON for player counts
IW4M kept hitting a network wall (container -> host LAN IP -> IW4M
loopback bind) that we couldn't cleanly punch through, and the two-minute
!rt token added enough operational friction that the pause-all-deployments
gate never got a fair test. RCON gives us the same fact — how many
players are on a server right now — direct from each game server over
UDP, with no session, no token, no proxy.

- Add integrations/rcon.ts: dgram UDP client. rconGetPlayerCount() and
  rconGetStatus() parse both `status` (print + scoreboard) and
  `statusResponse` (info line + rows) dialects. Timeout via
  RCON_TIMEOUT_MS.
- Add komodoResolveGamePort() + extractUdpPort(). Reads Komodo's read
  API for a server, walks both docker-compose-style port objects and
  docker-run-style "host:container/udp" strings, caches into
  servers.game_port on first success.
- Replace pipelineReady() guard in sweep with a simple
  integrations-configured check. Same held/failed/manual outcomes,
  but keyed on RCON+Komodo instead of IW4M+auth-state.
- Worker's status refresh fans out per-server over RCON at concurrency=4
  so a 15-server fleet doesn't spike UDP.
- Schema: drop iw4m_match/iw4m_value, add game_port + rcon_host.
- Delete every auth artifact: TokenModal, auth store, auth-state module,
  /api/auth/token, /api/events/auth, iw4m.test.ts. Layout no longer
  mounts the modal, pipeline store no longer tracks a frozen state.
- README + .env.example + docker-compose.yml + komodo/stack.toml swap
  IW4M_* env for RCON_PASSWORD / RCON_HOST / RCON_TIMEOUT_MS. Same
  host.docker.internal:host-gateway extra_hosts entry, repurposed
  for RCON reach.
- Tests: add rcon.test.ts and komodo.test.ts, update sweep.test.ts to
  drop the auth-freeze case. 23/23 passing.
2026-09-12 01:33:06 -04:00
josh d99711563e Initial commit — SvelteKit fleet manager for T5 Plutonium servers
Ports push_scripts.sh (1497 lines of bash) to a SvelteKit + SQLite web app
that manages a fleet of T5 Plutonium servers running on Komodo. Same push
and beta workflow as the shell script, with a live SSE pipeline view, a
!rt-token-gated auth modal that hard-pauses deployments on stale tokens,
per-file audit diffs, and a Komodo-container deployment target.

- Push/beta engines port the exact filter, held-by-beta, conflict, and
  stale-backup rules from the shell script
- SQLite (better-sqlite3) replaces script_metadata.json; a one-shot
  importer moves legacy state over on first boot
- 16 vitest tests cover push add/update/skip, beta open→rollback,
  open→promote, conflict guards, sweep frozen/manual branches, and IW4M
  login/status parsing
- docker/ + komodo/stack.toml deploy as a managed container on the same
  host as the game servers, /root/zombies bind-mounted at the same path
  the shell script used
2026-09-12 00:23:51 -04:00