import { i18n } from './i18n' import { sdk } from './sdk' import { uiPort, dataDir } from './utils' import { storeJson } from './fileModels/store.json' import { fetchHealth } from './serverApi' export const main = sdk.setupMain(async ({ effects }) => { console.info(i18n('Starting Station Master!')) // Reactive, field-scoped: rotating the join secret (getJoinSecret.ts) rewrites store.json, // which re-runs setupMain and restarts the daemon with the new value. const joinSecret = await storeJson.read((s) => s.joinSecret).const(effects) const adminSecret = await storeJson.read((s) => s.adminSecret).const(effects) return sdk.Daemons.of(effects).addDaemon('server', { subcontainer: sdk.SubContainer.of( effects, { imageId: 'main' }, sdk.Mounts.of().mountVolume({ volumeId: 'data', subpath: null, mountpoint: dataDir, readonly: false, }), 'station-master-sub', ), exec: { command: ['node', 'src/server/index.ts'], env: { // PORT/BIND_ADDRESS/DIST_DIR are left at src/server/index.ts's own defaults (8081, // 0.0.0.0, ./dist relative to the Dockerfile's WORKDIR) — nothing here needs to differ // from them, so only what actually must be supplied is set explicitly. DATA_DIR: dataDir, // generateSecrets.ts seeds this before main.ts ever runs, so it is never actually // empty — the fallback only avoids threading `string | undefined` through `env`, which // wants `Record`. JOIN_SECRET: joinSecret ?? '', // Unset leaves the server's /api/games routes switched off entirely rather than open, so // an empty string here fails closed — see src/server/http.ts in the game repo. ADMIN_SECRET: adminSecret ?? '', }, }, /** * Reports what the server is actually doing, not merely that it is up. * * `/api/health` carries the game counts, so the check that proves the server is answering can * say how many tables are in play in the same breath — which is the thing an administrator * glancing at the service page wants to know. * * A refusal is reported as `starting`, never `failure`: the server replays its saved games * before it binds the port, so a boot legitimately looks like nothing is listening. Calling * that a failure would make an ordinary restart look like a crash. */ ready: { display: i18n('Multiplayer Server'), fn: async () => { const counts = await fetchHealth() if (counts === null) { return { result: 'starting', message: i18n('The multiplayer server is not ready'), } } const games = counts.active === 0 ? i18n('no games in progress') : counts.active === 1 ? i18n('1 game in progress') : `${counts.active} ${i18n('games in progress')}` // Only mentioned when there is one — a permanent "0 waiting to start" is noise on a line // that is read at a glance. const waiting = counts.lobby === 0 ? '' : counts.lobby === 1 ? `, ${i18n('1 waiting to start')}` : `, ${counts.lobby} ${i18n('waiting to start')}` return { result: 'success', message: `${i18n('Multiplayer server is ready')} — ${games}${waiting}`, } }, }, requires: [], }) })