Jesse ea012f1329 Bundle Station Master v0.5.2, and fix the package version scheme
Submodule pin moves to v0.5.2, which carries the working splash-page
multiplayer door and the GET /api/health probe behind it.

The interface goes back to serving `/` — the splash, which is the front
door to all three ways to play. It pointed at /play.html only because the
splash's multiplayer door was hardcoded to "Coming soon", making the front
page a dead end on a real server; v0.5.2 removes the reason for the
workaround. The splash now probes /api/health on load, so served from here
it opens the lobby and served as a plain static site it says plainly that
it needs a server.

The package version was wrong and is corrected: 1.0.0:0 -> 0.5.2:0. That
1.0.0 was the scaffold's placeholder, left in by mistake — versions.md's
consistency checklist requires the upstream half to match the bundled tag,
which is what makes "bump the pin, bump the version" one mechanical action
rather than a judgement call. No historical version file is needed: the
placeholder carried no migration and was never published anywhere.

The correction is a downgrade in ExVer terms, so StartOS refused to install
over the existing copy ("uninit target range `!` is unsatisfiable"). The
installed package was therefore removed and reinstalled — checked first
that the volume held only an auto-generated join secret and no games. Two
consequences, both one-time and both recorded in UPDATING.md: the join
secret is regenerated, and the host's public domain binding was dropped
with the uninstall and has to be re-added.

Verified on phoenix.local: builds as v0.5.2:0, installs, starts, and serves
/api/health 200 with the right body; the splash carries all three doors
including a live ./play.html?lobby, no "Coming soon" anywhere, all three
ids the probe addresses present, and #lobby rendering on play.html?lobby.
2026-08-21 11:13:40 -04:00

Station Master Logo

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

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

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
S
Description
Multiplayer Service for StationMaster Game
Readme
861 KiB
Languages
TypeScript 98.5%
Dockerfile 1.3%
Makefile 0.2%