Built from source via a git submodule pinned to a tag, not a published image — the Dockerfile, main.ts, and manifest should never need to change for an ordinary version bump, only the submodule pin (see UPDATING.md). One volume, one interface serving the browser client + lobby/intent API + SSE stream from a single origin, no dependencies. The server-wide join secret (D14) is seeded on install, exposed via the Get Join Secret action, and blocks start behind a critical task until retrieved — the same first-set/rotation pattern as actual-budget-startos's admin password. Verified on phoenix.local: installs, the critical task correctly blocks an ordinary start, force-starting confirms the daemon binds its port and serves the client, and store.json correctly holds the install-seeded join secret. Not verified: the Get Join Secret action's execution end-to-end — start-cli's `package action run` fails with a client-side deserialization error that reproduces identically against actual-budget's already-shipped equivalent action, so this looks like a start-cli issue rather than a defect here. icon.svg is still the scaffold's hello-world placeholder — no real Station Master icon exists yet to ship in its place.
161 lines
6.9 KiB
Markdown
161 lines
6.9 KiB
Markdown
<p align="center">
|
|
<img src="icon.svg" alt="Station Master Logo" width="21%">
|
|
</p>
|
|
|
|
# Station Master on StartOS
|
|
|
|
> Everything not listed in this document should behave the same as upstream
|
|
> Station Master. If a feature, setting, or behavior is not mentioned here,
|
|
> the upstream documentation is accurate and fully applicable — see the
|
|
> Documentation section of `instructions.md` for links.
|
|
|
|
Station Master is a railroad operations board game with an authoritative multiplayer server;
|
|
solitaire needs no server and is not what this package is for. This package runs that server —
|
|
the browser client, the lobby, and the intent/SSE API — as a single StartOS service.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Image and Container Runtime](#image-and-container-runtime)
|
|
- [Volume and Data Layout](#volume-and-data-layout)
|
|
- [File Models](#file-models)
|
|
- [Dependencies](#dependencies)
|
|
- [Network Access and Interfaces](#network-access-and-interfaces)
|
|
- [Installation and First-Run Flow](#installation-and-first-run-flow)
|
|
- [Actions](#actions)
|
|
- [Tasks](#tasks)
|
|
- [Health Checks](#health-checks)
|
|
- [Backups and Restore](#backups-and-restore)
|
|
- [Limitations and Differences](#limitations-and-differences)
|
|
- [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers)
|
|
|
|
---
|
|
|
|
## Image and Container Runtime
|
|
|
|
Built from source with a custom `Dockerfile` — there is no published Station Master image.
|
|
|
|
| What to Document | Value |
|
|
| --- | --- |
|
|
| Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies |
|
|
| Architectures | x86_64, aarch64 |
|
|
| Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations |
|
|
|
|
One subcontainer: `station-master-sub`, running the single daemon `server`.
|
|
|
|
## Volume and Data Layout
|
|
|
|
One volume, `data`, mounted at `/data` (`DATA_DIR`).
|
|
|
|
| What to Document | Value |
|
|
| --- | --- |
|
|
| Volume names | `data` |
|
|
| Mount points | `/data` |
|
|
| StartOS files | `store.json` — holds the join secret (see File Models) |
|
|
| Database | None — flat files. Each game is `games/<gameId>/game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt }`) plus `turn-timings.json`, an `index.json` naming every game, and lobby state for games not yet started |
|
|
|
|
## File Models
|
|
|
|
One StartOS-managed file, `store.json`, on the `data` volume.
|
|
|
|
- **`store.json`** — JSON, holds `joinSecret` (a string). Seeded on install with a random
|
|
24-character value (`init/generateJoinSecret.ts`) — the daemon will not start without one, so
|
|
it is never left unset. Rewritten only by the **Get Join Secret** action, which generates a new
|
|
value on every run; the value is otherwise never touched, so a hand edit to it survives until
|
|
the action is next run. `main.ts` reads it reactively, so a rewrite restarts the daemon with the
|
|
new value automatically.
|
|
|
|
Everything else under `/data` — `games/`, `index.json`, lobby and session state — is the
|
|
application's own persistence, written directly by the server process, not by a StartOS file
|
|
model.
|
|
|
|
## Dependencies
|
|
|
|
None.
|
|
|
|
## Network Access and Interfaces
|
|
|
|
One interface, `ui`, on the single port the daemon listens on. It serves the browser client, the
|
|
lobby and intent HTTP API, and the per-seat SSE game stream — all from the same origin, which is
|
|
what lets a player reach the server from a LAN address and another player reach it from a
|
|
different one in the same game.
|
|
|
|
## Installation and First-Run Flow
|
|
|
|
No setup wizard. On install, a join secret is generated and stored immediately (see File Models),
|
|
and a critical task is raised pointing at **Get Join Secret** — the service starts and is usable
|
|
the moment the daemon is healthy, but a player cannot create or join a game until the join secret
|
|
has been retrieved and shared with them.
|
|
|
|
## Actions
|
|
|
|
- **Get Join Secret** (`get-join-secret`) — run this any time you want to read the current join
|
|
secret, or to invalidate it and issue a new one. Every run **generates a fresh secret,
|
|
overwrites the stored one, and restarts the daemon** with it — there is no read-only mode.
|
|
Rotating does not disconnect players already seated in a running game (D14's join secret gates
|
|
the lobby door, not an in-progress session); it only invalidates the old value for anyone who
|
|
has not yet joined or created a game. Completes in a few seconds, safe to repeat.
|
|
|
|
## Tasks
|
|
|
|
- **Get the join secret to share with players** — raised on install, severity `critical`. Points
|
|
at the **Get Join Secret** action. Clears the moment that action is run for the first time; it
|
|
does not return afterward (running it again to rotate does not re-raise it).
|
|
|
|
## Health Checks
|
|
|
|
- **Multiplayer Server** — `checkPortListening` against the daemon's port. "Not ready" means the
|
|
process has not yet bound its port; on a normal start this clears within a second or two; on a
|
|
cold container start it briefly reflects the daemon starting up.
|
|
|
|
## Backups and Restore
|
|
|
|
Strategy: the entire `data` volume is backed up wholesale (`ofVolumes`) — every file copied
|
|
exactly as it sits on disk, nothing dumped and replayed.
|
|
|
|
That includes every game's full intent history, so a restore can resume any in-progress game
|
|
exactly where it left off, and the join secret in `store.json`, so previously shared join links
|
|
keep working after a restore. Nothing needs to be rebuilt or re-entered after a restore; the
|
|
server resumes every saved game on boot the same way it does after an ordinary restart.
|
|
|
|
## Limitations and Differences
|
|
|
|
1. **No compile step.** The image runs the server directly from TypeScript source using Node's
|
|
native type stripping, rather than building a `dist/` for the server the way the browser
|
|
client is built. This is upstream's own deployment shape (`docs/architecture/deployment.md` in
|
|
the game repo), not something this package changed.
|
|
2. **The join secret has no per-player identity.** It is a single server-wide value (D14) that
|
|
gates who may create or join a game — it is not a username or password, and StartOS has no
|
|
visibility into who a player is once they've joined.
|
|
3. **No accounts, and one game at a time per person is expected but not enforced** — a deliberate
|
|
upstream design decision (`docs/architecture/multiplayer.md` §11 D13), not a StartOS-specific
|
|
limitation.
|
|
|
|
---
|
|
|
|
## Quick Reference for AI Consumers
|
|
|
|
```yaml
|
|
package_id: station-master
|
|
image: built from source (Dockerfile)
|
|
architectures: [x86_64, aarch64]
|
|
subcontainers: [station-master-sub]
|
|
volumes:
|
|
data: /data
|
|
file_models:
|
|
- store.json
|
|
startos_managed_env_vars:
|
|
- DATA_DIR
|
|
- JOIN_SECRET
|
|
dependencies: none
|
|
interfaces:
|
|
ui: { type: ui, port: 8081 }
|
|
actions:
|
|
- get-join-secret
|
|
tasks:
|
|
- { action: get-join-secret, severity: critical }
|
|
health_checks:
|
|
- Multiplayer Server
|
|
```
|