Initial StartOS package for Station Master (v0.5.1)
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.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
<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
|
||||
```
|
||||
Reference in New Issue
Block a user