Files
blind_chess/.claude/handoffs/2026-04-28-152000-mvp-deployed.md
claude (blind_chess) a6de43edc1 feat: implement and deploy blind_chess MVP
- 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>
2026-04-28 11:20:18 -04:00

182 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 200300 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.