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.
electric-cherry
Web app that manages a fleet of T5 Plutonium servers running under Komodo.
Successor to push_scripts.sh — same job (push shared GSC scripts, run beta
tests, restart empty servers, track deployments) with a live UI and real
audit diffs.
Stack
- SvelteKit (adapter-node) + TypeScript
- SQLite via better-sqlite3 (replaces
script_metadata.jsonas the source of truth) - Server-sent events for live pipeline / server-status streams
- RCON (UDP) direct-to-game-server for player counts
- Komodo API for restarts and per-server game-port discovery
- Runs as a single Komodo-managed container on the same host as the game
servers, with
/root/zombiesbind-mounted in
Layout
src/lib/server/db/— schema, migrations, better-sqlite3 handlesrc/lib/server/fleet/— file walking, push/beta enginessrc/lib/server/integrations/— Komodo, RCONsrc/lib/server/pipeline/— sweep loop, event bus, worker singletonsrc/routes/api/— REST + SSE endpointssrc/routes/— UI pages (dashboard, deployments, betas, per-server)Dockerfile,docker-compose.yml,entrypoint.sh— container buildkomodo/— Komodo stack definitionscripts/— legacy JSON importer, fixture seedertests/— vitest unit tests
How restarts stay safe
Every push (or beta open) creates a deployment row and marks each affected
server pending. A background sweep runs every 15s and:
- Asks each server over RCON: how many players are on right now?
- If 0 → tell Komodo to restart the server → row goes
done. - If >0 → hold the row
pendingwith the player count; try again next tick. - If RCON unreachable → hold
pendingwitherror='rcon unreachable'.
No token, no session, no auth freeze. If Komodo or RCON aren't configured,
the sweep marks pending rows manual so nothing hangs forever.
Local dev
Windows or macOS — no /root/zombies on disk, so seed a fixture tree first.
npm install
npm run seed:fixture
# Point env at the fixture (PowerShell)
$env:SHARED_SCRIPTS = "$PWD/test-fixtures/zombies/shared/scripts"
$env:SERVERS_ROOT = "$PWD/test-fixtures/zombies/t5"
$env:DB_PATH = "$PWD/data/cherry.db"
npm run dev
# open http://localhost:3000
Without RCON_PASSWORD / KOMODO_* set, the pipeline marks pending rows
manual after a sweep — the UI still works, you just don't get real
restarts. Enough to develop the push/beta flow.
Tests
npm test
Covers push (add/update/skip), beta lifecycle (open → rollback, open → promote, conflict blocks), sweep (integrations-off manual branch), and RCON parsing (fake UDP socket).
Deploy to the Komodo host
-
Build the image once locally or on the host:
docker compose build -
Import
komodo/stack.tomlinto Komodo (or point Komodo at thedocker-compose.ymlat repo root). -
Set the env vars in Komodo:
KOMODO_ADDRESS, KOMODO_API_KEY, KOMODO_API_SECRET RCON_PASSWORD # single shared password across every T5 server RCON_HOST # optional; defaults to host.docker.internal -
First boot triggers the legacy import: if
/root/zombies/script_metadata.jsonexists and/data/cherry.dbis empty,entrypoint.shrunsscripts/import-legacy.tsbefore starting the server. The source JSON gets renamed to.migrated. -
Load the app on the LAN or over tailscale (no web-app auth by design).
What still lives in the shell script
Nothing that's needed at runtime. Move push_scripts.sh to archive/ once
you've run a side-by-side parity check between the shell's dry-run and the
app's POST /api/push { dryRun: true }.
Non-goals
- Editing GSC files in-browser. Do that in your editor of choice; the
web app only cares about what's on disk in
SHARED_SCRIPTS. - Multi-user auth. LAN/tailscale scoping is the security model.
- Two-way sync with
script_metadata.json. The importer runs once. If the shell script is ever used again after that, it'll write to the JSON file and the web app won't see it.
References into the old script
Every port carries a // Ports push_scripts.sh:NN comment pointing back to
the original for the parts that had non-obvious behavior. Grep those if a
behavior seems wrong — the shell script is the reference implementation
until the parity check passes.