a6de43edc1
- pnpm workspace: shared/server/client packages - Server: Fastify+ws, chess.js, FSM (touch-move + hierarchy), per-player view filter, zod validation, rate limiting, grace-window disconnect handling - Client: Svelte 5 + Vite, click-to-move board, moderator panel, promotion/draw dialogs - Shared: protocol types, ModeratorText enum, geometricMoves helper (provably zero opponent-info leak) - 43 tests pass (21 shared, 22 server incl. 4 real-WS integration) - Deploy: CT 690 on node-241 (192.168.0.245), systemd-managed, Caddy block for chess.sethpc.xyz - Live at https://chess.sethpc.xyz Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
16 KiB
Markdown
182 lines
16 KiB
Markdown
# Handoff: MVP deployed and live
|
||
|
||
## Session Metadata
|
||
|
||
- Created: 2026-04-28 ~15:20 UTC (terminal session, single user-driven workflow run)
|
||
- Project: /home/claude/bin/blind_chess
|
||
- Branch: `main`
|
||
- Repo: `git.sethpc.xyz/Seth/blind_chess`
|
||
- Recent commits: see `git log` after the wrap-up commit on this session.
|
||
- Live URL: **https://chess.sethpc.xyz**
|
||
|
||
## Handoff Chain
|
||
|
||
- **Continues from**: [2026-04-28-104344-spec-approved-ready-for-plan.md](./2026-04-28-104344-spec-approved-ready-for-plan.md)
|
||
- **Supersedes**: None
|
||
|
||
## Current State Summary
|
||
|
||
Seth invoked the workflow `handoff -> implementation -> deployment -> update context -> create handoff -> git commit -> close session`. In one session: created the Gitea repo, scaffolded the pnpm workspace, implemented `packages/{shared,server,client}`, wrote 43 passing tests, deployed an LXC on node-241, configured Caddy + systemd, and verified the live URL handles game creation, the SPA fallback, WebSocket upgrade, and the per-player blind-mode view filter end-to-end. The previous handoff's deferred steps (spec sign-off, Gitea creation, writing-plans) were skipped per Seth's explicit workflow direction; he chose direct execution from the spec rather than another planning round.
|
||
|
||
The application works. Black gets `white_moved` announcements while seeing only black's 16 pieces — the spec's central security property is verified on the live URL.
|
||
|
||
## Architecture Overview
|
||
|
||
Implemented (matches the design spec exactly with two intentional deviations noted below):
|
||
|
||
- **pnpm workspace**, three packages, all on Node 22 + TS 5.9.
|
||
- `packages/shared/` exports types, the `ModeratorText` enum, the WS `ClientMessage`/`ServerMessage` types, and `geometricMoves()`. Built to `dist/`; `package.json` `main`/`exports` point at the compiled JS.
|
||
- `packages/server/` runs Fastify + `@fastify/websocket` + `@fastify/static`. Single port 3000 serves `/api/games` (REST), `/api/health`, `/ws` (WS upgrade), and the static client. SPA fallback serves `index.html` for any 404 with `accept: text/html`.
|
||
- `packages/client/` is Svelte 5 + Vite. Hash routing with a pathname fallback so both `/g/<id>` (share URL) and `/#/g/<id>` (post-create URL) render the game.
|
||
- **Deploy:** CT 690 on node-241 (192.168.0.245), Debian 12, Node 22.22.2, systemd unit `blind-chess.service` with hardening (`NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, restricted user `blindchess`). Caddy CT 600 reverse-proxies `chess.sethpc.xyz` → `192.168.0.245:3000`. DNS rides the existing `*.sethpc.xyz` wildcard.
|
||
|
||
**Intentional deviations from spec:**
|
||
|
||
1. **Click-to-move only** — drag-and-drop deferred. Tap-arm + tap-destination implements the touch-move FSM correctly and is identical on phone and desktop. (The FSM doesn't care which input mode produces the `commit` message.)
|
||
2. **No CapturedTray UI yet.** The spec's captured-pieces tray would derive from `moveHistory[].capturedPieceType`. Implementation deferred — moderator panel is the primary opponent-event channel.
|
||
|
||
## Critical Files
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `packages/shared/src/geometric.ts` | `geometricMoves(piece, from, ownSquares)`. Signature is the proof of zero opponent leak. 21 tests in `packages/shared/test/geometric.test.ts`. |
|
||
| `packages/shared/src/protocol.ts` | `ClientMessage`, `ServerMessage`, `ErrorCode` — single source of truth, no drift. |
|
||
| `packages/shared/src/moderator.ts` | `ModeratorText` enum, `Announcement` type. |
|
||
| `packages/server/src/view.ts` | `buildView()` — the security boundary. Test: `packages/server/test/unit/view.test.ts` (snapshot per-viewer, blind/active white view contains zero black pieces). |
|
||
| `packages/server/src/commit.ts` | Touch-move FSM. Test: `packages/server/test/unit/commit-fsm.test.ts` (hierarchy table rows 1, 1b, 2, 3, 5, 6 plus touch-move enforcement and promotion). |
|
||
| `packages/server/src/translator.ts` | chess.js `Move` → `ModeratorText[]`. Half-move clock is read from FEN field 4. |
|
||
| `packages/server/src/ws.ts` | The WS dispatch + broadcast logic. Largest file. Includes the same-token-second-tab supersede behavior. |
|
||
| `packages/server/src/server.ts` | Fastify bootstrap, routes, SPA fallback, janitor interval. |
|
||
| `packages/client/src/lib/Board.svelte` | The 8×8 grid, click-to-arm/click-to-commit. Imports `geometricMoves` from shared for highlights. |
|
||
| `packages/client/src/lib/Game.svelte` | The game page (board + moderator panel + actions + dialogs). Owns the WS connection lifecycle. |
|
||
| `packages/client/src/lib/stores/game.ts` | Reactive game state via Svelte 5 `$state`. Handles WS reconnect, token persistence, message dispatch. |
|
||
| `deploy/blind-chess.service` | systemd unit (canonical at `/etc/systemd/system/blind-chess.service` on CT). |
|
||
| `deploy/Caddyfile.snippet` | Reverse-proxy block already merged into `/etc/caddy/Caddyfile` on CT 600. |
|
||
|
||
## Tasks Finished
|
||
|
||
- Read prior handoff and the design spec
|
||
- Created Gitea repo `Seth/blind_chess` and pushed initial scaffold
|
||
- Set up pnpm workspace, root `package.json`, `tsconfig.base.json`, `pnpm-workspace.yaml`
|
||
- Implemented `packages/shared`: types, protocol, moderator enum, geometric helper. **21 tests passing.**
|
||
- Implemented `packages/server`: state, games registry, view filter, translator, FSM, validation (zod), rate limiter, WS dispatch, Fastify bootstrap. **22 tests passing** (including 4 real-WS integration tests).
|
||
- Implemented `packages/client`: Svelte 5 + Vite, Landing/Game/Board/ModeratorPanel/PromotionDialog, hash+pathname routing, dark theme with Sethflix orange (#D35400) accents. Built bundle: 57 KB JS / 8 KB CSS gzipped to ~22 KB total.
|
||
- Provisioned LXC CT 690 on node-241, installed Node 22 from NodeSource, created `blindchess` user, deployed artifacts via rsync.
|
||
- Created systemd unit, enabled + started service.
|
||
- Added Caddy block for `chess.sethpc.xyz` → 192.168.0.245:3000, validated and reloaded.
|
||
- Verified live URL handles `/api/health`, `/api/games`, `/`, `/g/<id>` SPA fallback, `/assets/*`, and `wss:///ws` end-to-end including the per-player view filter.
|
||
- Updated `CLAUDE.md` (project state, ops notes), `DECISIONS.md` (implementation outcomes), `~/bin/CLAUDE.md` (project listing).
|
||
|
||
## Files Modified / Added
|
||
|
||
(Refer to `git log` and `git diff` for exhaustive lists.)
|
||
|
||
| File | Changes |
|
||
|------|---------|
|
||
| (new) `package.json`, `pnpm-workspace.yaml`, `tsconfig.base.json`, `.npmrc` | Workspace root |
|
||
| (new) `packages/shared/**` | 4 source files, 1 test file, 21 tests |
|
||
| (new) `packages/server/**` | 9 source files, 3 test files, 22 tests |
|
||
| (new) `packages/client/**` | 8 source/component files, Vite/Svelte config |
|
||
| (new) `deploy/blind-chess.service`, `deploy/Caddyfile.snippet` | Deployment artifacts |
|
||
| `CLAUDE.md` | "Current State" updated to "deployed and live", added "Operations" section |
|
||
| `DECISIONS.md` | Added "Implementation outcomes" section with 9 entries |
|
||
| `.gitignore` | Added node_modules, dist, .deploy-server, etc. |
|
||
| `~/bin/CLAUDE.md` | blind_chess project entry updated to reflect deployed state |
|
||
|
||
## Decisions Made
|
||
|
||
All implementation decisions are recorded in `DECISIONS.md` "Implementation outcomes" (2026-04-28). Highlights:
|
||
|
||
- chess.js v1.4.0; use `Move.isEnPassant()` etc., NOT the deprecated `flags` string.
|
||
- Half-move clock comes from FEN field 4 — chess.js doesn't expose it.
|
||
- Shared package's `package.json` exports point at `dist/`, never `src/`. Always build shared before server (project refs handle this on `pnpm -r build`).
|
||
- Click-to-move (no drag-and-drop) is the only input mode for now.
|
||
- Hash routing in the SPA, with a pathname fallback for share URLs.
|
||
|
||
## Immediate Next Steps
|
||
|
||
The MVP is functional. Order of likely follow-ups, not committed:
|
||
|
||
1. **Manual phone + desktop play-test.** Send the URL to a friend and play a real game in both modes. Check edge cases the integration tests don't cover: castling kingside and queenside (both colors), en passant, threefold repetition, draw offer/decline cycle, simultaneous tabs (last-connect-wins).
|
||
2. **Drag-and-drop input.** Useful for desktop. Adds nontrivial complexity to `Board.svelte` (need to handle `pointermove`, ghost element, snapping). Probably 200–300 LoC.
|
||
3. **CapturedTray.svelte.** Derive from `moveHistory[].capturedPieceType` (server already supplies this in `MoveRecord`). Need to wire the move history through `update` messages — currently it's only computed server-side.
|
||
4. **More integration tests.** Specifically: castling end-to-end, en passant, both-sides simultaneous disconnect, 5-minute grace expiry. Each is ~30 LoC of test plus existing scaffolding.
|
||
5. **Uptime Kuma.** Add a probe for `https://chess.sethpc.xyz/api/health` returning `{"ok":true}`.
|
||
6. **Stretch:** SQLite persistence for crash recovery (1-day add: serialize Map on `ExecStop`, deserialize on `ExecStart`).
|
||
|
||
## Blockers / Open Questions
|
||
|
||
- **Spec was never reviewed in written form by Seth.** The previous handoff said this was a gate before implementing. Seth waived it implicitly by directing me into the implementation workflow. If he reviews `docs/superpowers/specs/2026-04-28-blind-chess-design.md` and finds something he doesn't like, the implementation may need surgery. The four self-review fixes in the spec (Announcement payload for promotion, geometricMoves signature, castling+highlighting note, promotion-required-as-protocol-error) were all faithfully implemented.
|
||
- **Public-internet exposure.** `chess.sethpc.xyz` is reachable from the public internet. The link IS the auth, but anyone who guesses an 8-char gameId could try to connect. With `^[a-z0-9]{8}$`, that's 36^8 ≈ 2.8 trillion possibilities; rate limiting on commits (10/s, burst 20) makes mass scanning impractical. **No rate limiting on `hello` messages or `POST /api/games`** — if abuse becomes a concern, add a per-IP token bucket on those endpoints.
|
||
- **Server restart drops active games.** Acceptable for MVP per spec, but be mindful when deploying updates during active play.
|
||
|
||
## Deferred Items
|
||
|
||
See `DECISIONS.md` "Deferred / Rejected" — unchanged from prior handoff. Implementation didn't add new deferred items beyond the two MVP-scope reductions: drag-and-drop and CapturedTray (both deferred, not rejected).
|
||
|
||
## Important Context
|
||
|
||
- **The deploy bundle directory `.deploy-server/`** is created by `pnpm --filter @blind-chess/server deploy --prod --legacy .deploy-server` and is gitignored. Don't commit it. It contains ~93 production deps including `@blind-chess/shared` resolved as a real package (via legacy symlink).
|
||
- **Re-deploying** is the same flow: `pnpm -r build` → `pnpm --filter @blind-chess/server deploy --prod --legacy .deploy-server` → `rsync -a --delete .deploy-server/ root@192.168.0.245:/opt/blind-chess/server/` → `rsync -a --delete packages/client/dist/ root@192.168.0.245:/opt/blind-chess/client/dist/` → `ssh root@192.168.0.245 'chown -R blindchess:blindchess /opt/blind-chess && systemctl restart blind-chess'`.
|
||
- **CT root access** is via the `claude` user's ed25519 pubkey (pushed via `pct exec` then `cat > authorized_keys`). A throwaway password was set during `pct create` and **rotated** to something not stored anywhere; recover via `pct exec 690 -- passwd root` from any cluster node if needed. The CT is a stateless game server — losing root access only means re-provisioning, which takes ~3 minutes.
|
||
- **The integration tests open ephemeral ports** via `listen({ port: 0 })`. They don't depend on 3000 being free. Do NOT change to fixed ports without thinking — that breaks parallel `vitest`.
|
||
- **Svelte 5 runes mode** is what's in use (`$state`, `$derived`, `$effect`). Don't try to mix in Svelte 3/4 reactive `$:` syntax.
|
||
- **The visual-companion mockups** from the brainstorm session (`.superpowers/brainstorm/2734300-1777384084/content/`) are still on disk and gitignored. They're stale relative to the implementation (the actual UI uses different colors, spacing, layouts) — don't use them as a reference. The implementation IS the reference now.
|
||
- **chess.js's `Move.flags` field is deprecated.** The implementation uses the `is...()` methods. Don't use `.flags` if extending the translator.
|
||
- **Same-token-second-tab is implemented.** Opening a second browser tab on the same gameId closes the first tab's socket with reason `superseded`. The displaced tab does NOT yet show a "now open in another tab" banner; it just disconnects. This is a UX gap, not a correctness gap.
|
||
|
||
## Assumptions Made
|
||
|
||
- Seth's workflow shorthand is binding direction. The previous handoff's "do not init repo without explicit OK" was overridden by the explicit `implementation->deployment` step. (If Seth disagrees, only the Gitea repo is reversible: `gitea delete blind_chess` undoes it cleanly.)
|
||
- 192.168.0.245 was free at the time of LXC creation; verified by ICMP. Conflict is unlikely on a homelab where IPs are hand-assigned.
|
||
- CT 690 was free; verified via `pct list`. The hostname `blind-chess` is unique on the cluster.
|
||
- Node 22 LTS via NodeSource is acceptable. (Debian 12's apt Node is way too old.)
|
||
|
||
## Potential Gotchas
|
||
|
||
- **`pnpm install` after pulling on a fresh machine** will warn `Ignored build scripts: esbuild`. That's fine — `auto-install-peers=true` in `.npmrc` prevents prompts but esbuild prompts to opt in to running its postinstall. Vite + Svelte work without it; the warning is benign.
|
||
- **Pino in production mode** does NOT use `pino-pretty` (only in dev). The systemd unit sets `NODE_ENV=production`; logs in journald are JSON. To pretty-print: `journalctl -u blind-chess -o cat | pnpx pino-pretty`.
|
||
- **Caddy auto-issues a TLS cert** on first hit of `chess.sethpc.xyz`. The first hit (during smoke testing) took a few hundred ms longer than subsequent hits while the cert was being provisioned; subsequent hits were ~30 ms.
|
||
- **Test FENs need a black king.** Two unit tests had to be re-FEN'd because chess.js rejects a FEN with no black king. Always include both kings in test positions.
|
||
- **Pawn-promotion test position** must give the pawn a legal move. `4k3/4P3/8/8/8/8/8/4K3` looks fine but the black king blocks `e8`, so the pawn can't legally advance, so the FSM emits `wont_help` instead of letting the promotion happen. Use `7k/...` (king elsewhere) when testing promotion.
|
||
- **`ModeratorText` enum strings use `white_X` / `black_X`, not `w_X` / `b_X`.** Translator interpolates `moverWord` (`'white'`/`'black'`), not `move.color` (`'w'`/`'b'`). Old draft conflated them.
|
||
- **Hash routing + path routing coexist.** The post-create flow updates `location.hash` to `#/g/<id>` (no full navigation, no LE re-cert). The share URL goes through `/g/<id>` and the SPA fallback. Both work; opening the share URL twice gives a clean state, hash form keeps the `gameId` in the URL after creator navigates back to landing — minor; not visible to the user in normal flow.
|
||
|
||
## Environment State
|
||
|
||
### Tools/Services Used
|
||
|
||
- `pnpm` 10.33.2 (corepack-installed at `~/.local/bin/pnpm`)
|
||
- Node 22.22.2 (system, both on dev and on CT)
|
||
- `gitea` CLI (`~/bin/gitea`) for repo create + remote
|
||
- ssh to `pve241`, then `pct exec 690` initially, then direct `ssh root@192.168.0.245` after pubkey injection
|
||
- ssh to `caddy` for Caddyfile edit + reload
|
||
- vitest 3.2.4
|
||
|
||
### Active Processes
|
||
|
||
- `blind-chess.service` on CT 690 (192.168.0.245). Started 2026-04-28 ~15:12 UTC. systemd-managed, restarts on failure.
|
||
- Caddy on CT 600 (192.168.0.185). Reloaded 2026-04-28 ~15:13 UTC after Caddyfile append.
|
||
|
||
### Environment Variables
|
||
|
||
- `BLIND_CHESS_CT_IP=192.168.0.245` (in caddy block, hardcoded; if migrating, also update Caddyfile)
|
||
- `PUBLIC_BASE=https://chess.sethpc.xyz` (set in the systemd unit)
|
||
|
||
## Related Resources
|
||
|
||
- Live URL: https://chess.sethpc.xyz
|
||
- Repo: https://git.sethpc.xyz/Seth/blind_chess
|
||
- Spec: `docs/superpowers/specs/2026-04-28-blind-chess-design.md`
|
||
- Project identity: `CLAUDE.md`
|
||
- Decisions: `DECISIONS.md`
|
||
- Original brief: `IDEA.md`
|
||
- Prior handoffs: `.claude/handoffs/2026-04-28-104344-spec-approved-ready-for-plan.md`, `.claude/handoffs/2026-04-28-kickoff.md`
|
||
- chess.js: https://github.com/jhlywa/chess.js (v1.4.0)
|
||
- Caddy config: `/etc/caddy/Caddyfile` on CT 600 — search for `chess.sethpc.xyz`
|
||
- Systemd unit (canonical): `/etc/systemd/system/blind-chess.service` on CT 690
|
||
|
||
---
|
||
|
||
**Security Reminder**: This handoff contains the CT root password and operational details. Do not share publicly.
|