12 KiB
Development
The controller uses SvelteKit, TypeScript and SQLite (better-sqlite3), with the
Node adapter and server-sent events. The host agent uses Python 3's standard
library and runs as a systemd service; release verification calls gpgv.
Hosts do not need Node.js.
Agents connect outbound to the controller over HTTPS and manage Docker/Plutainer and local RCON on each host. The controller owns fleet metadata and queues; it does not require a Docker socket or direct access to host filesystems.
Run locally
Use Node.js 22.13+ (the container uses Node.js 22), npm and Python 3. Install
GnuPG's gpg, gpg-agent and gpgv to run real release-signing integration tests; local test
runs skip those tests when either executable is absent.
npm ci
export EC_SETUP_TOKEN="$(openssl rand -hex 32)"
export BETA_ARCHIVE_DIR=./data/beta-archive
EC_AUTH_ALLOW_HTTP=true npm run dev
Use the exported token at /setup to create the first administrator, then remove
it from the environment. Local metadata defaults to ./data/cherry.db; use a
separate database when testing enrollment. HTTP login is for local development;
production agent transport requires HTTPS. See authentication
for access and recovery details.
Synthetic UI preview
Create a disposable fixture without connecting game hosts:
npx svelte-kit sync
fixture_parent=$(mktemp -d "${TMPDIR:-/tmp}/ec-ui.XXXXXX")
npm run seed:fixture -- --db "$fixture_parent/preview/cherry.db" --watch
The sync step generates the SvelteKit TypeScript configuration that the seeder needs in a fresh checkout.
The seeder prints the generated paths. Leave that terminal open to refresh synthetic observations every 15 seconds. In a second terminal, load the printed environment file and start the controller on localhost:
source /absolute/path/printed/above/preview.env
npm run dev -- --host 127.0.0.1 --port 3000
Open http://127.0.0.1:3000/login. The private fixture.json next to the database
contains the demo account's generated password. It is not printed in command
output. To run a fixed snapshot instead of a live feeder, use a fresh directory
and replace --watch with --now 2026-10-07T12:00:00Z. Domain data is deterministic
for that timestamp; account credentials are always generated separately. Old
snapshots naturally show stale observations when viewed against today's clock.
The fixture contains 3 hosts, 12 canonical Black Ops I/II servers, and 500 match records: 494 historical and 6 live. It includes solo through four-player squads, unknown starting counts, enough players and matches to exercise pagination, rosters, seven days of utilization and capacity samples, and retained Activity from campaigns, betas, host operations, maintenance, recovery, and autoscaling. The fleet has 6 live matches, 2 empty lobbies, a preparing server, and 3 problems: a stopped server, a server not responding, and a server on the offline host. Script and config downloads contain harmless synthetic text.
--db must be an absolute path in a new directory whose parent exists. The
seeder refuses existing directories, databases, sidecars and symlinks, and never
uses an inherited DB_PATH. It runs the application's database migrations with
isolated paths, without starting the web server or workers. A failed seed removes
only its newly reserved directory. Completed fixtures remain after Ctrl+C.
The preview controller runs its usual local workers. Automatic maintenance and autoscaling are paused; all seeded operations are terminal. Hostnames, runtime images, library IDs and credentials are synthetic, so there are no runnable game files or authenticated agents. Use this environment only for local UI review. The feeder checks the fixture marker and restores only its known observations, including the deliberately offline host. Stop both terminals when finished and remove the generated fixture directory.
Checks and builds
npm run lint
npm run check
npm test
npm run test:agent
npm run build
The Gitea Actions workflow runs these checks on
every push and pull request after npm ci. It uses a Node.js 22 Debian container
with Python 3, gpg, gpg-agent, gpgv and native build tools for better-sqlite3. Signing
tests create disposable keys in temporary directories; CI never receives release
private keys. The runner must support Docker jobs with the ubuntu-latest label.
npm run check runs before the tests
to generate the SvelteKit configuration needed in a fresh checkout. Application
tests use two workers in CI to bound resource use for database-heavy fixtures.
The build step runs the same npm run build command used by the Dockerfile,
including agent packaging and the report and launch bundles.
Run npm run format to apply Prettier formatting before committing. It uses
four-space indentation and a 100-column target for supported source, config and
documentation files, including Svelte components. Python, shell and game scripts
are outside Prettier's supported languages. Generated agent downloads and the npm
lockfile keep their generator's formatting.
npm run lint checks formatting without changing files, then runs the recommended
JavaScript, TypeScript and Svelte ESLint rules. Generated build output and local
runtime data are excluded. Type checking remains part of npm run check.
The ESLint config documents exceptions for existing Svelte navigation, list and
reactivity patterns. Individual suppressions explain intentional validation and
test fixtures. Node.js 22.13 is the minimum supported 22.x version for the lint
toolchain.
The package.json overrides keep SvelteKit 2 on patched devalue 5.x (5.9.4 or
newer) and cookie 0.7.x releases until its dependency ranges include those fixes.
These dependencies ship in the server bundle even though SvelteKit is a
development dependency.
Dependency security checks must include development dependencies: use npm audit
without --omit=dev.
npm run dev and npm run build regenerate the downloadable agent, installer,
standalone updater and checksum manifest. Packaging preserves an existing
static/agent/manifest.json.sig only when its manifest bytes remain identical;
changes to the agent, updater or installer remove the stale signature. Builds
never sign, and hosts reject unsigned releases while retaining installed code.
Finalize and review release code, run npm run agent:package, and explicitly
sign it on the offline publisher machine:
python3 agent/sign_release.py --gnupghome /secure/electric-cherry-signing --fingerprint FULL_SIGNING_KEY_FINGERPRINT
Commit or distribute the signed public artifacts before building the controller image from the same source revision. Keep the signing home outside the repository, Docker context, controller and CI. Follow Publishing signed releases for key creation, host provisioning and rotation.
Builds also bundle report workers
under build/reports and launch preparation under build/launch. Development
and test commands compile those workers automatically. Restart npm run dev
after changing agent or worker code, or run npm run agent:package to refresh
only the downloadable agent assets while the development server is running.
To run the production build locally, run these commands from the repository root with the controller environment configured as above:
npm run build
NODE_ENV=production node build
NODE_ENV=production makes the report and launch workers load from build/reports
and build/launch. Without it, they look under .svelte-kit/, where the production
build does not create worker bundles. The Docker image sets this variable already.
Automated tests simulate Docker and game hosts. Real Debian installation and T5/T6 server validation use the host acceptance checklist.
Code layout
Paths below are relative to the repository root.
| Path | Responsibility |
|---|---|
src/routes |
Pages and API endpoints |
src/lib/ui |
Shared components, stores and UI patterns |
src/lib/server/hosts |
Enrollment, readiness and command transport |
src/lib/server/backends |
Instance identity and remote file access |
src/lib/server/provisioning/native.ts |
Lifecycle and campaign queues connected to agents |
src/lib/server/game-files |
Immutable uploads and streaming distribution |
src/lib/server/db |
SQLite schema, migrations and shared artifact storage |
src/lib/server/reports |
Metrics, leaderboard and capacity workers |
agent |
Python service, installer, packaging and tests |
Performance diagnostics
Set EC_PERF_LOG=true to log slow requests, report computation/cache timings and
event-loop delay. Responses expose Server-Timing; browser navigation records an
ec:navigation performance measure. Report workers use separate read-only SQLite
connections and bounded caches. Metrics snapshots can be reused for up to 30
seconds when their inputs are unchanged.
Benchmark the leaderboard against an isolated synthetic database:
npm run benchmark:leaderboard
npm run benchmark:leaderboard -- --scenario all --baseline-ref <commit>
The comparison checks response parity and warm query timings.
Set EC_SLOW_SQL_MS=20 to log runtime statements taking at least 20 ms. Logging
omits bound values and string literals and is disabled by default. Capture timings
under the same fleet load before and after database changes. Inspect database
size, large tables/indexes, WAL size and free pages inside the controller with:
npm run diagnostics:database -- /data/cherry.db
This opens an existing database read-only without running migrations. Substitute
./data/cherry.db for a default local installation.
Storage and upgrades
Game-file uploads live in dedicated filesystem storage. Script, plugin and configuration artifact bodies are stored once by SHA-256 in SQLite; versions, staging plans, rollback snapshots and launch commands reference those shared bytes. Startup transactionally backfills older rows. Current agents fetch assigned artifacts through an authenticated endpoint and verify their hashes; older agents receive inline content until they update.
Back up the persistent data directory before upgrading. Startup logs show
migration phases, elapsed time and resident memory. Completed migrations are
retained across restarts; interrupted transactions roll back and retry on startup.
The one-time vacuum needs disk space for a temporary database copy. Do not
remove cherry.db-wal to recover from an interrupted upgrade.
The controller uses a 128 MiB page cache; each reader worker uses 32 MiB.
Main-database migrations use 16 MiB with memory mapping disabled. Temporary
storage stays disk-backed. Containers use /data/.sqlite-tmp for scratch files;
override SQLITE_TMPDIR only with another writable, disk-backed directory.
For a local data disk, EC_SQLITE_MMAP_MB=256 enables memory-mapped reads; leave it
at zero for network mounts. Query statistics are optimized at startup and hourly.
WAL reuse is capped at 64 MiB after checkpoints, though active transactions can
temporarily grow it larger.