felt/.planning/phases/01-tournament-engine/01-PLAN-A.md
Mikkel Georgsen 21ff95068e docs(01): create Phase 1 plans (A-N) with research and feedback
14 plans in 6 waves covering all 68 requirements for the Tournament
Engine phase. Includes research (go-libsql, NATS JetStream, Svelte 5
runes, ICM complexity), plan verification (2 iterations), and user
feedback (hand-for-hand UX, SEAT-06 reword, re-entry semantics,
integration test, DKK defaults, JWT 7-day expiry, clock tap safety).

Wave structure:
  1: A (scaffold), B (schema)
  2: C (auth/audit), D (clock), E (templates), J (frontend scaffold)
  3: F (financial), H (seating), M (layout shell)
  4: G (player management)
  5: I (tournament lifecycle)
  6: K (overview/financials), L (players), N (tables/more)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:58:22 +01:00

148 lines
6.8 KiB
Markdown

# Plan A: Project Scaffold + Core Infrastructure
---
wave: 1
depends_on: []
files_modified:
- go.mod
- go.sum
- cmd/leaf/main.go
- internal/server/server.go
- internal/server/ws/hub.go
- internal/server/middleware/auth.go
- internal/server/middleware/role.go
- internal/nats/embedded.go
- internal/nats/publisher.go
- internal/store/db.go
- frontend/embed.go
- Makefile
autonomous: true
requirements: [ARCH-01, ARCH-04, ARCH-05, ARCH-06, ARCH-07]
---
## Goal
A Go binary starts in the LXC container, embeds NATS JetStream (sync_interval: always) and LibSQL, serves HTTP via chi, hosts a WebSocket hub that broadcasts to connected clients within 100ms, and serves a SvelteKit SPA stub via go:embed. This is the skeleton that every subsequent plan builds on.
## Context
- **Greenfield project** — no existing code
- **Dev environment:** x86_64 LXC container (Debian/Ubuntu), Go 1.23+
- **go-libsql has no tagged releases** — pin to commit hash in go.mod with comment
- **NATS JetStream sync_interval: always** is mandatory (Jepsen 2025 finding for single-node)
- **coder/websocket** v1.8.14 for WebSocket hub (not gorilla/websocket)
- See 01-RESEARCH.md Pattern 1 (Embedded NATS), Pattern 2 (WebSocket Hub), Pattern 3 (SvelteKit Embed)
## User Decisions (from CONTEXT.md)
- The operator is the **Tournament Director (TD)** — use this term consistently
- Multi-tournament support means all state must be tournament-scoped from day one (MULTI-01)
- WebSocket hub must support tournament-scoped broadcasting
## Tasks
<task id="A1" title="Initialize Go module and dependency tree">
Create the Go module at the project root. Install all core dependencies pinned to specific versions:
- `github.com/tursodatabase/go-libsql` — pin to commit hash (check latest stable commit, add comment in go.mod)
- `github.com/nats-io/nats-server/v2` @ v2.12.4
- `github.com/nats-io/nats.go` @ latest
- `github.com/coder/websocket` @ v1.8.14
- `github.com/go-chi/chi/v5` @ v5.2.5
- `github.com/golang-jwt/jwt/v5` @ latest
- `golang.org/x/crypto` @ latest
- `github.com/google/uuid` @ latest
- `github.com/skip2/go-qrcode` @ latest
Create the directory structure per 01-RESEARCH.md Recommended Project Structure. Include empty `.go` files with package declarations where needed to make the module compile.
Create a Makefile with targets:
- `make build` — builds `cmd/leaf` binary
- `make run` — builds and runs with default data dir
- `make test` — runs all tests
- `make frontend` — builds the SvelteKit frontend (placeholder — just creates build dir with stub index.html)
- `make all` — frontend + build
The binary must compile and run (serving HTTP on :8080) with `make run`.
**Verification:** `go build ./cmd/leaf/...` succeeds. `go vet ./...` passes. Directory structure matches the recommended layout.
</task>
<task id="A2" title="Implement core infrastructure: NATS, LibSQL, WebSocket hub, HTTP server">
Wire up all core infrastructure in `cmd/leaf/main.go` and the `internal/` packages:
**1. Embedded NATS Server** (`internal/nats/embedded.go`):
- Start NATS server in-process with `DontListen: true` (no TCP listener)
- JetStream enabled with `JetStreamSyncInterval: 0` (sync_interval: always — MANDATORY per Jepsen finding)
- Memory limit: 64MB, Disk limit: 1GB
- Wait for ready with 5-second timeout
- Clean shutdown on context cancellation
- Create initial JetStream streams: `AUDIT` (subject: `tournament.*.audit`), `STATE` (subject: `tournament.*.state.*`)
**2. NATS Publisher** (`internal/nats/publisher.go`):
- Publish function that writes to JetStream with subject routing
- Helper to publish tournament-scoped events: `Publish(ctx, tournamentID, subject, data)`
**3. LibSQL Database** (`internal/store/db.go`):
- Open LibSQL with `sql.Open("libsql", "file:"+dbPath)`
- Enable WAL mode: `PRAGMA journal_mode=WAL`
- Enable foreign keys: `PRAGMA foreign_keys=ON`
- Configurable data directory (flag or env var)
- Close on context cancellation
**4. WebSocket Hub** (`internal/server/ws/hub.go`):
- Connection registry with mutex protection
- Client struct: conn, tournamentID (subscription scope), send channel (buffered 256)
- `Register(client)` / `Unregister(client)`
- `Broadcast(tournamentID, messageType, data)` — sends to all clients subscribed to that tournament (or all if tournamentID is empty)
- On connect: accept WebSocket, register client, send initial state snapshot (stub for now — just a "connected" message)
- Read pump and write pump goroutines per client
- Drop messages for slow consumers (non-blocking channel send)
- Graceful shutdown: close all connections
**5. HTTP Server** (`internal/server/server.go`):
- chi router with middleware: logger, recoverer, request ID, CORS (permissive for dev)
- Route groups:
- `/api/v1/` — REST API endpoints (stub 200 responses for now)
- `/ws` — WebSocket upgrade endpoint → Hub.HandleConnect
- `/*` — SvelteKit SPA fallback handler
- Configurable listen address (default `:8080`)
- Graceful shutdown with context
**6. SvelteKit Embed Stub** (`frontend/embed.go`):
- `//go:embed all:build` directive
- `Handler()` function that serves static files with SPA fallback
- For now, create a `frontend/build/index.html` stub file with minimal HTML that says "Felt — Loading..."
**7. Main** (`cmd/leaf/main.go`):
- Parse flags: `--data-dir` (default `./data`), `--addr` (default `:8080`)
- Startup order: LibSQL → NATS → Hub → HTTP Server
- Signal handling (SIGINT, SIGTERM) with graceful shutdown in reverse order
- Log startup and shutdown events
**Verification:**
- `make run` starts the binary, prints startup log showing LibSQL opened, NATS ready, HTTP listening on :8080
- `curl http://localhost:8080/` returns the stub HTML
- `curl http://localhost:8080/api/v1/health` returns 200 with JSON showing all subsystems operational (LibSQL: `SELECT 1` succeeds, NATS server ready, WebSocket hub accepting connections)
- A WebSocket client connecting to `ws://localhost:8080/ws` receives a connected message
- CTRL+C triggers graceful shutdown with log messages
</task>
## Verification Criteria
1. `go build ./cmd/leaf/...` succeeds with zero warnings
2. `go vet ./...` passes
3. `make run` starts the binary and serves HTTP on :8080
4. LibSQL database file created in data directory with WAL mode
5. NATS JetStream streams (AUDIT, STATE) exist after startup
6. WebSocket connection to `/ws` succeeds and receives initial message
7. SPA fallback serves index.html for unknown routes
8. Graceful shutdown on SIGINT/SIGTERM completes without errors
## Must-Haves (Goal-Backward)
- [ ] Go binary compiles and runs as a single process with all infrastructure embedded
- [ ] NATS JetStream uses sync_interval: always (data durability guarantee)
- [ ] WebSocket hub supports tournament-scoped broadcasting
- [ ] SvelteKit SPA served via go:embed with fallback routing
- [ ] All state is tournament-scoped from day one (no global singletons)