Files
Eric WagonerandClaude Fable 5.1 ee5690ebda A credibility pass over the whole repo: the board's mechanics in board.ts, one copy of each helper and stylesheet rule, the kit's plumbing this game never used removed, and the ops scripts counting traffic through one parser
The engine and the bot no longer import each other: geometry, movement
and captures live in src/lib/game/board.ts and both use it. The escape
test, the four directions, the king's neighbours and the enumeration of
a side's moves each exist once; the tally counts by the named end
sentences rather than by regex over them; exports nobody imports are
exports no more. The tests pin the bot's opening move and the three-
beside-the-throne capture they named but never exercised.

In the client: .small, the word-as-button, the × that dismisses, the
visually-hidden rule and the frame of the reading pages are in app.css
once; the preferences panel and the report slip share one modal shape;
the room store drops the simultaneous-round fields this game never read
and gains seatEmpty and seatUnheld, which the lobby and the join page
read instead of three spellings of their own. The wire shapes for
reports and the tally are declared in view.ts for both sides. The
artwork component is re-indented for the top level it lives at.

On the server and in deploy: the plaintext-token fallback and its
migration script guarded ledgers this game never wrote; the seat line
now requires the hash and the start line the rules revision. The route
table lists every route. The visitors digest and the nightly rollup
count Caddy's log through deploy/traffic.py, and the rollup writes the
finished-games count it had been computing behind "and False". The
reports digest is one program, deploy/report-digest.ts, that
pull-reports.sh and the desk skill both use. The deploy README, the
visitors skill and the reports skill no longer describe a browser-only
game, a /play route or a rules text in docs/; conventions.md carries
the kit's Preferences and tally sections.

Kit-shared files touched, to port back: server/src/{index,rooms,store,
tally,reports}.ts, deploy/{deploy.sh,replay-ledgers.ts,pull-reports.sh,
traffic.py,report-digest.ts,*-rollup.sh,*-visitors.sh,*-pulse.sh},
src/lib/net/{client.ts,room.svelte.ts,view.ts}, Preferences.svelte,
ReportSlip.svelte, TableTalk.svelte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwFKMuQnPAEHJ5yA1q4orh
2026-09-23 20:02:45 -04:00

94 lines
4.6 KiB
Markdown

# Deploying Hnefatafl
Every game, against a housecarl or a friend, is a room on a small Node
process that keeps it as an append-only ledger of moves in
/var/lib/hnefatafl/rooms. Production is one DigitalOcean droplet: Caddy
terminates TLS with automatic certificates, serves the static build from
/opt/hnefatafl/build, and proxies /api and /ws to the game server on port
8789, which runs as the `hnefatafl` user under systemd from
/opt/hnefatafl/app.
## Sizing
The smallest droplet, `s-1vcpu-512mb-10gb` ($4 a month), carries both Caddy
and the game server comfortably: the server is one small Node process capped
at 300 MB by its unit file, and a room is a few kilobytes of ledger.
## Current production
- Droplet: `hnefatafl` (nyc3, s-1vcpu-512mb-10gb, tag `hnefatafl`), IP 209.97.148.244
- URLs: https://tafl.kestrelsnest.social (A record at Hover, where kestrelsnest.social's
DNS lives) and https://hnefatafl.209.97.148.244.sslip.io (always works, zero DNS).
- Everyday deploy: `deploy/deploy.sh 209.97.148.244`
- Players' reports: `deploy/pull-reports.sh` mirrors /var/lib/hnefatafl/feedback.jsonl
and the screenshots to ~/Desktop/hnefatafl-reports with a digest;
`deploy/report-reply.sh 209.97.148.244 <id> <status> "text"` answers one.
## New droplet from scratch
1. `doctl compute droplet create hnefatafl --region nyc3 \
--size s-1vcpu-512mb-10gb --image ubuntu-24-04-x64 \
--ssh-keys <your-key-ids> --tag-name hnefatafl --wait`
2. `scp deploy/setup-droplet.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> \
"bash /root/setup-droplet.sh 'tafl.kestrelsnest.social, hnefatafl.<ip>.sslip.io'"`
(point the A record at the new IP first, or leave the real name out until it is).
3. `scp deploy/setup-server.sh deploy/Caddyfile.tmpl root@<ip>:/root/ && ssh root@<ip> "bash /root/setup-server.sh"`
4. `deploy/deploy.sh <ip>`
The sslip.io hostname works with no DNS at all. To add a real name, point an
A record at the droplet and add the name to the first line of
/etc/caddy/Caddyfile (space separated), then `systemctl reload caddy`; Caddy
fetches the certificate on first request.
## Operations scripts on the droplet
`deploy.sh` installs these to /usr/local/bin, the `traffic.py` they share to
/usr/local/lib/hnefatafl, and the cron file to /etc/cron.d/hnefatafl on every
deploy, so the live copies are the repo copies:
- `hnefatafl-visitors.sh [day]`: who is here now and who came that day.
- `hnefatafl-pulse.sh`: the weekly health check (service, errors, rollup
trend, backup, box).
- `hnefatafl-rollup.sh [day]`: one JSON line per day of counts, run
nightly at 00:12 UTC into /var/lib/hnefatafl/rollup.jsonl.
- `hnefatafl-backup.sh`: nightly at 07:23 UTC, mirrors /var/lib/hnefatafl
to the `kestrel-wizwar-backups` Space under `hnefatafl/` (a current copy
and dated snapshots kept 90 days). It needs rclone with the Spaces
credentials in /root/.config/rclone/rclone.conf, copied by hand from the
wizwar droplet; until then it logs "skipped".
Errors from the game server go to Sentry, project `hnefatafl` in the
locallygrownnet organisation; the DSN is in the unit file. The nightly rollup
and backup check in with Sentry Crons when /root/.hnefatafl-sentry-cron-rollup
and /root/.hnefatafl-sentry-cron hold their check-in URLs (the ingest
URL with the project's cron path and public key), so a missed night is noticed.
To have every new issue, regression and reappearance posted to Slack the way
wizwar's are, run `deploy/sentry-slack-alert.sh [#channel]` once from your own
shell with `SENTRY_TOKEN` (an org auth token with alerts:write) and
`SLACK_CHANNEL_ID` (the channel's Slack ID) set; the token never leaves the
shell.
Before every deploy, `deploy/verify-ledgers.sh <ip>` fetches every production
ledger and replays it with the local engine, comparing each room with what the
server shows. A ledger the new engine refuses or replays differently stops the
deploy: the server would rewrite that game on restart.
## Everyday deploys
deploy/deploy.sh <ip>
Runs the type-checks, the tests and the build locally, rsyncs `build/` to
the droplet keeping the previous week's hashed assets, rsyncs the server and
engine sources, installs dependencies, and restarts the game server. A room
is replayed from its ledger when the server comes back, which takes a few
seconds; Caddy holds requests that land in the gap.
## Operations
- Logs: `ssh root@<ip> journalctl -u hnefatafl -f` for the game server,
`journalctl -u caddy -f` and /var/lib/caddy/access.log for the web side.
- Restart: `ssh root@<ip> systemctl restart hnefatafl`
- Rooms: `/var/lib/hnefatafl/rooms/<CODE>.jsonl`, one ledger per game; the
nightly backup keeps them in the Space.