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>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# Handoff: Spec approved, ready for implementation plan
|
||||
|
||||
## Session Metadata
|
||||
|
||||
- Created: 2026-04-28 10:43:44
|
||||
- Project: /home/claude/bin/blind_chess
|
||||
- Branch: not a git repo (yet — see Pending Work)
|
||||
- Session duration: ~90 minutes (single brainstorming session)
|
||||
- Recent commits: none — repo not initialized yet.
|
||||
|
||||
## Handoff Chain
|
||||
|
||||
- **Continues from**: [2026-04-28-kickoff.md](./2026-04-28-kickoff.md)
|
||||
- Previous title: blind_chess Kickoff (2026-04-28)
|
||||
- **Supersedes**: None
|
||||
|
||||
> Review the previous handoff for full context before filling this one.
|
||||
|
||||
## Current State Summary
|
||||
|
||||
A 90-minute brainstorming session walked the project from "IDEA.md is populated" through every major architectural and gameplay decision and produced a complete design spec. Seth reviewed and approved each of the five design sections (architecture, data model, protocol, state machine, error handling/security/testing). The spec is on disk at `docs/superpowers/specs/2026-04-28-blind-chess-design.md`. Self-review applied four small fixes inline. **Seth ended the session before reviewing the written spec or invoking the `writing-plans` skill** — those are the next steps for the resuming agent.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Designed (not yet implemented):
|
||||
|
||||
- **Language/stack:** Node 22 + TypeScript, Fastify + `ws`, Svelte + Vite, `chess.js`.
|
||||
- **Repo shape:** pnpm workspace, three packages — `packages/server`, `packages/client`, `packages/shared`. Shared types are the load-bearing decision: the WS protocol drift surface is high-risk.
|
||||
- **State:** in-memory only (`Map<gameId, Game>`). No DB. Server restart drops active games — acceptable for MVP.
|
||||
- **Engine:** `chess.js` for rules + a custom ~80-LoC `geometricMoves` helper for pseudo-legal moves (chess.js doesn't expose those). The helper is a pure function of `(piece, from, ownSquares)` — provably no opponent input — so it can run on both server (for the `no_legal_moves` check) and client (for highlighting). It lives in `packages/shared`.
|
||||
- **Deploy:** new LXC on node-241, Caddy CT 600 routes `chess.sethpc.xyz` → port 3000, systemd-managed Node service. Single-port everything (HTTP + WS + static).
|
||||
- **Security boundary:** `buildView(game, viewer)` is the *only* function that emits board state to clients. Blind-mode views literally omit opponent pieces from the wire — they are absent, not encrypted-but-present.
|
||||
|
||||
## Critical Files
|
||||
|
||||
| File | Purpose | Relevance |
|
||||
|------|---------|-----------|
|
||||
| IDEA.md | Original project brief from Seth | Source of truth for "what is this game"; read first |
|
||||
| docs/superpowers/specs/2026-04-28-blind-chess-design.md | Full design spec | The canonical artifact — everything from this session is here |
|
||||
| CLAUDE.md | Project identity + tagline + current state | Updated this session; loads on every session |
|
||||
| DECISIONS.md | 16 architecture/implementation decisions + 12 deferred/rejected | Updated this session |
|
||||
| .claude/handoffs/2026-04-28-kickoff.md | Previous handoff (project scaffold) | Read second |
|
||||
| .gitignore | Excludes `.superpowers/` and `.backup/` | Created this session |
|
||||
| .superpowers/brainstorm/2734300-1777384084/content/ | Visual companion artifacts (3 mockup HTMLs) | Browse historically; not part of project |
|
||||
|
||||
## Key Patterns Discovered
|
||||
|
||||
- **Mode = view filter, not different game.** Vanilla and blind share one `chess.js` instance; the difference is what `buildView()` returns to each player.
|
||||
- **Moderator vocabulary is an enum, never a string.** Display text lives client-side. Tests assert against `ModeratorText` values like `'wont_help'`.
|
||||
- **Moderator-vocabulary "errors" come through as `Announcement` on the `update` message, NOT as protocol `error`.** They're game events. `error` is reserved for protocol failures (malformed, rate-limited, slot taken).
|
||||
- **Highlights in blind+ON are purely geometric.** Function of `(piece type, position, own-piece set)`. Rays extend through unseen opponents. Stop at own pieces. Off-board excluded. **Zero opponent info leak.**
|
||||
- **Touch-move FSM is server-side.** `game.armed: { color, from }` is the entire state. `no_legal_moves` and `wont_help` checks fire only on first commit with a piece; once committed, all subsequent failed attempts are `illegal_move` with the touch staying.
|
||||
- **Single `commit { from, to? }` message** handles both drag-start (no `to`) and drag-drop / destination-click (with `to`). Uniform server FSM through one handler.
|
||||
|
||||
## Tasks Finished
|
||||
|
||||
- Read kickoff handoff and IDEA.md
|
||||
- Offered visual companion, Seth accepted (URL was http://steel141.local:50816 during session — server now stale)
|
||||
- Q1 scope: locked vanilla+blind day-one, shared engine
|
||||
- Q2 stack: locked Node + TypeScript + chess.js
|
||||
- Q3 "wont help you" semantics: locked check-resolution interpretation
|
||||
- Q4 touch-move FSM: locked tap-arms-reversible, drag/destination-click commits
|
||||
- Q5 lobby flow (5 sub-defaults): all locked
|
||||
- Q6 highlighting: locked geometric-only rule for blind+ON (rays extend through unseen pieces)
|
||||
- Q7 final cleanup batch (6 items: persistence, time controls, deploy target, promotion, mobile, endgame): all confirmed
|
||||
- Architecture: 3 approaches proposed (SvelteKit monolith / pnpm workspace / framework-free), Seth picked B
|
||||
- 5 design sections presented in terminal, each individually approved
|
||||
- Spec written: `docs/superpowers/specs/2026-04-28-blind-chess-design.md`
|
||||
- Spec self-review: 4 fixes applied inline (Announcement payload for promotion, geometricMoves signature, castling+highlighting note, promotion-required-as-protocol-error)
|
||||
- CLAUDE.md updated (tagline, Project Identity, current state, key files, conventions)
|
||||
- DECISIONS.md updated (16 settled decisions, 12 deferred/rejected)
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes | Rationale |
|
||||
|------|---------|-----------|
|
||||
| CLAUDE.md | Replaced placeholder tagline + Project Identity; added current state, key files, conventions | Was a stub before brainstorming; now reflects approved spec |
|
||||
| DECISIONS.md | Filled in 16 architecture/implementation decisions and 12 deferred/rejected items | Captures everything Seth confirmed |
|
||||
| docs/superpowers/specs/2026-04-28-blind-chess-design.md | New file — full design spec | Output of brainstorming session per skill workflow |
|
||||
| .gitignore | New file — excludes `.superpowers/` and `.backup/` | `.superpowers/` is the visual-companion working directory |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
See the full list in `DECISIONS.md` (16 architecture + implementation, 12 deferred/rejected). Highlights:
|
||||
|
||||
| Decision | Options Considered | Rationale |
|
||||
|----------|-------------------|-----------|
|
||||
| Stack: Node + TS + Fastify + Svelte + chess.js | SvelteKit monolith / framework-free / Python+FastAPI / Go | Single-language top-to-bottom; shared types catch protocol drift; Svelte fits reactive board state |
|
||||
| Highlighting (blind+ON) is purely geometric | "Safe" (legal-empty only) / Full / None | Provably zero opponent info leak; rule is `f(piece, position, ownSquares)` only |
|
||||
| Moderator hierarchy refined to 4 tiers | 2-tier / 3-tier / 4-tier with pseudo-legal | Pseudo-legal vs. legal distinction lets `wont_help` reveal "king is the problem" without revealing why |
|
||||
| `Announcement` is enum, not string | Free-form strings / structured enum | Tests assert on enum, i18n trivial via translation table |
|
||||
| In-memory state only | SQLite from day one / in-memory + later SQLite | Hobby-scale; restart drops games is acceptable; SQLite is a 1-day add later |
|
||||
| Single `commit` message for arm + move | Two messages (arm + move) / three / one | Uniform server FSM; one handler covers all transitions |
|
||||
| Castling NOT highlighted in blind+ON | Highlight always / partial highlight / never | Castling legality depends on opponent state; partial reveal would leak; player can still execute manually |
|
||||
|
||||
## Immediate Next Steps
|
||||
|
||||
1. **Have Seth review the written spec.** It's at `docs/superpowers/specs/2026-04-28-blind-chess-design.md`. This is the gate the brainstorming skill required before invoking `writing-plans`, but the session ended before Seth read the written form. Ask: "I've got the spec written at `<path>`. Please review and let me know if anything needs to change before I draft the implementation plan." If Seth requests changes, edit inline and re-ask.
|
||||
|
||||
2. **Propose Gitea repo creation, with Seth's OK.** The kickoff handoff's deferred step 5: `git init` + `gitea create blind_chess` + `gitea remote blind_chess` + `gitea push`. The session ended before this happened. **Do not init the repo without Seth's explicit confirmation** — the kickoff said "propose creating a Gitea repo … Once direction is clear", and Seth ended the session without saying go. Use the `gitea` CLI from `~/bin/gitea`.
|
||||
|
||||
3. **Invoke the `superpowers:writing-plans` skill.** This is the terminal state of the brainstorming process. The plan should decompose the spec into concrete implementation tasks. Skill location: per CLAUDE.md plugin list.
|
||||
|
||||
## Blockers/Open Questions
|
||||
|
||||
- **Spec review by Seth not yet done.** He approved each of the 5 sections in conversation but did not look at the written spec. Self-review found 4 fixes that he hasn't seen — most notably the `Announcement.payload` field for promotions and the castling+highlighting clarification.
|
||||
- **No Gitea repo yet.** Project is on disk only. If the resuming agent loses this directory or it gets clobbered, the spec + decisions are gone. Backup: `.backup/` is gitignored but currently empty (no edits to backup).
|
||||
|
||||
## Deferred Items
|
||||
|
||||
All deferred/rejected items are in `DECISIONS.md` "Deferred / Rejected" section. The implementation plan should NOT plan for:
|
||||
|
||||
- Spectator mode, time controls, SQLite persistence, E2E browser tests, Authentik gate, public lobby/matchmaking/ratings, client-side AI hints, PGN export, CI/CD automation.
|
||||
- Stretch goal (deferred, not rejected): pre-deploy "server restarting" warning to active players.
|
||||
|
||||
## Important Context
|
||||
|
||||
**The whole project is currently a design spec on disk — no code exists yet.** The next session's job is to (1) get spec sign-off, (2) create the repo, (3) invoke `writing-plans` to produce an implementation plan. Do not jump to writing code; the brainstorming skill's terminal state is `writing-plans`, and writing-plans then becomes the input to a future implementation session.
|
||||
|
||||
**Seth approved each of the 5 design sections in conversation, but did NOT review the assembled written spec.** The brainstorming skill explicitly requires user review of the written artifact before transitioning to writing-plans. The four self-review fixes (Announcement payload, geometricMoves signature, castling-highlighting note, promotion-required handling) were applied without Seth seeing them — they're small and not contentious, but he should glance at them.
|
||||
|
||||
**The visual companion server (port 50816, `http://steel141.local:50816`) is no longer running.** It was started during the session and would have been auto-killed after 30 minutes of inactivity. Three mockup HTMLs are persisted in `.superpowers/brainstorm/2734300-1777384084/content/`: `welcome.html`, `highlighting.html` (v1, superseded), `highlighting-v2.html` (the corrected geometric rule, the one Seth approved), `waiting.html`. Don't restart the server unless the next session does more visual work.
|
||||
|
||||
**`gameId` format `^[a-z0-9]{8}$` is small but adequate** — only 32 bits of entropy, but the link IS the auth, lives a single game's worth of time, and we have rate limiting. If at any point the link becomes long-lived (e.g., post-game replay URLs), bump to 12+ chars.
|
||||
|
||||
**The `geometricMoves` helper is the most-tested piece of code in this design.** Six golden-path test cases cover the moderator hierarchy decision table. Make sure its test file is one of the FIRST things implemented — everything else depends on it being right.
|
||||
|
||||
## Assumptions Made
|
||||
|
||||
- Seth wants Svelte not React (he didn't push back; my recommendation stood).
|
||||
- Seth wants Fastify not Express (same — accepted by silence).
|
||||
- Seth's homelab has node-241 with capacity for one more LXC (true at the time of CLAUDE.md last-update; verify with `pct list` on node-241 if uncertain).
|
||||
- "Moving that piece will not help you" is purely a king-safety announcement, not a tactical-evaluation one. Seth confirmed explicitly during the session.
|
||||
- 8-character `gameId` is unique enough at this scale (~tens of concurrent games max). Birthday-collision math: ~60K games active before 1% chance of any pair colliding. Way more than this project will see.
|
||||
|
||||
## Potential Gotchas
|
||||
|
||||
- **chess.js does not expose pseudo-legal moves directly.** Don't try to compute the `no_legal_moves` / `wont_help` distinction by calling chess.js's `moves()` alone — that returns *legal* moves only. You need both the geometric set and the legal set. The spec's `geometricMoves` helper is the missing piece.
|
||||
- **chess.js `inCheck()` checks the side-to-move.** After a move is applied, the side-to-move is the opponent. So `inCheck()` true after white's move means *black* is in check (which is what `${opp}_in_check` already says — but easy to invert by accident).
|
||||
- **`Announcement.payload.promotedTo` was added in self-review.** If the implementation skips it, the `white_promoted` announcement carries no piece-type info and the opponent has to guess. Don't drop it.
|
||||
- **The same-token-second-tab rule (last-connect-wins) is a security AND UX feature.** Without it, a malicious client could open many sockets per token and cause server-side resource leaks. Don't be tempted to "be more permissive" here.
|
||||
- **Castling is NOT in `geometricMoves`.** This is intentional. If a future contributor adds castling targets to the king's geometric set "for completeness", they'll either leak opponent info (by checking castling-legality) or mislead users (by suggesting illegal castling). Comment in the helper explaining this.
|
||||
- **The visual companion's `.superpowers/` dir is gitignored.** If you want to share mockups (e.g., commit them for posterity), copy them to `docs/` first.
|
||||
- **Files referenced in this handoff that don't exist yet:** `deploy/Caddyfile.snippet` and the test files (e.g., `packages/server/test/unit/geometric.test.ts`) are forward references — they'll be created during implementation. Don't be alarmed when the validator flags them as missing.
|
||||
|
||||
## Environment State
|
||||
|
||||
### Tools/Services Used
|
||||
|
||||
- **chess.js**: not yet installed; will be a dependency in `packages/server` (and possibly `packages/client` for vanilla mode legal-moves computation). Pin via `package.json` once init'd.
|
||||
- **gitea CLI**: `~/bin/gitea`. Used for repo create + push. Token at `~/.config/gitea/token`. Documented in `~/bin/CLAUDE.md`.
|
||||
- **pnpm**: required for workspace. Version 9+ recommended.
|
||||
- **Node 22 LTS**: deployment target. Local dev should match.
|
||||
- **Caddy CT 600** (192.168.0.185): existing. Add a `chess.sethpc.xyz` block to the Caddyfile when deploying. Spec includes a `deploy/Caddyfile.snippet` to be created during implementation.
|
||||
|
||||
### Active Processes
|
||||
|
||||
- **None.** The visual companion server self-terminated after 30 minutes of inactivity. No long-running tasks were started.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `HOMELAB_PASSWORD`: set globally, used for SSH/SMB/etc. Not needed for blind_chess work directly.
|
||||
- (No project-specific env vars yet.)
|
||||
|
||||
## Related Resources
|
||||
|
||||
- Spec: `docs/superpowers/specs/2026-04-28-blind-chess-design.md`
|
||||
- Project identity: `CLAUDE.md`
|
||||
- Decisions: `DECISIONS.md`
|
||||
- Original brief: `IDEA.md`
|
||||
- Kickoff handoff: `.claude/handoffs/2026-04-28-kickoff.md`
|
||||
- Visual companion mockups (gitignored): `.superpowers/brainstorm/2734300-1777384084/content/`
|
||||
- Global homelab context: `~/bin/CLAUDE.md`
|
||||
- Project scaffold recipe: `~/bin/CREATE_PROJECT.md`
|
||||
- Gitea CLI: `~/bin/gitea`
|
||||
- chess.js (intended dep): https://github.com/jhlywa/chess.js
|
||||
|
||||
---
|
||||
|
||||
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
|
||||
@@ -0,0 +1,181 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Handoff: blind_chess Kickoff (2026-04-28)
|
||||
|
||||
## What was done
|
||||
|
||||
Project scaffolded per `~/bin/CREATE_PROJECT.md`:
|
||||
- `~/bin/blind_chess/` directory created
|
||||
- `.claude/handoffs/` initialized (this is the first entry)
|
||||
- `GITEA_API.md` symlinked to `~/bin/GITEA_API.md`
|
||||
- `IDEA.md` created with empty template (Seth populates manually before next session)
|
||||
- `DECISIONS.md` created from local template
|
||||
- `CLAUDE.md` created with minimal stub pointing at IDEA.md as source of truth
|
||||
|
||||
No code, no repo, no deploy target yet. Project is in pure ideation.
|
||||
|
||||
## Current State
|
||||
|
||||
- **Phase:** ideation, pre-IDEA.md
|
||||
- **Repo:** none
|
||||
- **Branch:** n/a
|
||||
- **What's running:** nothing
|
||||
|
||||
## Next Steps (ordered)
|
||||
|
||||
**1. Read `IDEA.md` first.**
|
||||
Seth has populated it manually with the project brief between this session and the next. It is the authoritative source for what blind_chess is and what it should do. Do not skip — `CLAUDE.md`'s "Project Identity" section is intentionally empty and will be filled in *after* IDEA.md is read.
|
||||
|
||||
**2. Update `CLAUDE.md`.**
|
||||
Distill IDEA.md into:
|
||||
- A real one-line tagline (replace "Tagline TBD")
|
||||
- A one-paragraph "Project Identity" section (stable, won't change session-to-session)
|
||||
- Initial entries in "Current State" if any decisions emerged from IDEA.md (e.g., language choice, deploy target)
|
||||
|
||||
**3. Capture early decisions in `DECISIONS.md`.**
|
||||
Anything Seth committed to in IDEA.md (language, framework, deployment, naming, scope boundaries) goes into `DECISIONS.md` under Architecture or Implementation. Format: `YYYY-MM-DD: <decision> — <why>`. Include rejected options under "Deferred / Rejected".
|
||||
|
||||
**4. Brainstorm before building.**
|
||||
If IDEA.md leaves architecture or approach open, do not jump to implementation. Use the `superpowers:brainstorming` skill — explore intent and design first. Get Seth's confirmation on direction before code.
|
||||
|
||||
**5. Decide on Gitea repo.**
|
||||
The scaffold's optional step 6 (git init + Gitea push) was skipped. Once direction is clear, propose creating a Gitea repo via the `gitea` CLI:
|
||||
```bash
|
||||
cd ~/bin/blind_chess
|
||||
git init
|
||||
gitea create blind_chess
|
||||
gitea remote blind_chess
|
||||
echo "GITEA_API.md" >> .gitignore
|
||||
git add -A && git commit -m "init: scaffold project" && gitea push
|
||||
```
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None yet — IDEA.md will surface these.
|
||||
|
||||
## Notes for the next session
|
||||
|
||||
- Project name spelled `blind_chess` (underscore, not hyphen). Convention in `~/bin/` is hyphen, but underscore was Seth's explicit choice — likely for Python module compatibility. Don't auto-rename.
|
||||
- Inherits all global homelab conventions from `~/bin/CLAUDE.md`. No need to duplicate them here.
|
||||
Reference in New Issue
Block a user