lila/documentation/roadmap.md
2026-04-15 05:16:29 +02:00

242 lines
9.8 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.

# lila — Roadmap
Each phase produces a working increment. Nothing is built speculatively.
---
## Phase 0 — Foundation ✅
**Goal:** Empty repo that builds, lints, and runs end-to-end.
**Done when:** `pnpm dev` starts both apps; `GET /api/health` returns 200; React renders a hello page.
- [x] Initialise pnpm workspace monorepo: `apps/web`, `apps/api`, `packages/shared`, `packages/db`
- [x] Configure TypeScript project references across packages
- [x] Set up ESLint + Prettier with shared configs in root
- [x] Set up Vitest in `api` and `web` and both packages
- [x] Scaffold Express app with `GET /api/health`
- [x] Scaffold Vite + React app with TanStack Router (single root route)
- [x] Configure Drizzle ORM + connection to local PostgreSQL
- [x] Write first migration (empty — validates the pipeline works)
- [x] `docker-compose.yml` for local dev: `api`, `web`, `postgres`, `valkey`
- [x] `.env.example` files for `apps/api` and `apps/web`
---
## Phase 1 — Vocabulary Data + API ✅
**Goal:** Word data lives in the DB and can be queried via the API.
**Done when:** API returns quiz sessions with distractors, error handling and tests in place.
### Data pipeline
- [x] Run `extract-en-it-nouns.py` locally → generates JSON
- [x] Write Drizzle schema: `terms`, `translations`, `term_glosses`, `decks`, `deck_terms`
- [x] Write and run migration (includes CHECK constraints)
- [x] Write `packages/db/src/seeding-datafiles.ts` (imports all terms + translations)
- [x] Write `packages/db/src/generating-deck.ts` (idempotent deck generation)
- [x] CEFR enrichment pipeline complete for English and Italian
- [x] Expand data pipeline — import all OMW languages and POS
### Schemas
- [x] Define `GameRequestSchema` in `packages/shared`
- [x] Define `AnswerOption`, `GameQuestion`, `GameSession`, `AnswerSubmission`, `AnswerResult` schemas
- [x] Derived types exported from constants (`SupportedLanguageCode`, `SupportedPos`, `DifficultyLevel`)
### Model layer
- [x] `getGameTerms()` with POS / language / difficulty / limit filters
- [x] Double join on `translations` (source + target language)
- [x] Gloss left join
- [x] `getDistractors()` with POS / difficulty / language / excludeTermId / excludeText filters
- [x] Models correctly placed in `packages/db`
### Service layer
- [x] `createGameSession()` — fetches terms, fetches distractors, shuffles options, stores session
- [x] `evaluateAnswer()` — looks up session, compares submitted optionId to stored correct answer
- [x] `GameSessionStore` interface + `InMemoryGameSessionStore` (swappable to Valkey)
### API endpoints
- [x] `POST /api/v1/game/start` — route, controller, service
- [x] `POST /api/v1/game/answer` — route, controller, service
- [x] End-to-end pipeline verified with test script
### Error handling
- [x] Typed error classes: `AppError`, `ValidationError` (400), `NotFoundError` (404)
- [x] Central error middleware in `app.ts`
- [x] Controllers cleaned up: validate → call service → `next(error)` on failure
### Tests
- [x] Unit tests for `createGameSession` (question shape, options, distractors, gloss)
- [x] Unit tests for `evaluateAnswer` (correct, incorrect, missing session, missing question)
- [x] Integration tests for both endpoints via supertest (200, 400, 404)
---
## Phase 2 — Singleplayer Quiz UI ✅
**Goal:** A user can complete a full quiz in the browser.
**Done when:** User visits `/play`, configures settings, answers questions, sees score screen, can play again.
- [x] `GameSetup` component (language, POS, difficulty, rounds)
- [x] `QuestionCard` component (prompt word + 4 answer buttons)
- [x] `OptionButton` component (idle / correct / wrong states)
- [x] `ScoreScreen` component (final score + play again)
- [x] Vite proxy configured for dev
- [x] `selectedOptionId` added to `AnswerResult` (discovered during frontend work)
---
## Phase 3 — Auth ✅
**Goal:** Users can log in via Google or GitHub and stay logged in.
**Done when:** Better Auth session is validated on protected routes; unauthenticated users are redirected to login; user row is created on first social login.
- [x] Install `better-auth` and configure with Drizzle adapter + PostgreSQL
- [x] Mount Better Auth handler on `/api/auth/*` in `app.ts`
- [x] Configure Google and GitHub social providers
- [x] Run Better Auth CLI to generate and migrate auth tables (user, session, account, verification)
- [x] Add session validation middleware for protected API routes
- [x] Frontend: install `better-auth/react` client
- [x] Frontend: login page with Google + GitHub buttons
- [x] Frontend: TanStack Router auth guard using `useSession`
- [x] Frontend: TanStack Query `api.ts` sends credentials with every request
- [x] Unit tests for session middleware
---
## Phase 6 — Production Deployment ✅
**Goal:** App is live on Hetzner, accessible via HTTPS on all subdomains.
**Done when:** `https://lilastudy.com` loads; `https://api.lilastudy.com` responds; auth flow works end-to-end; CI/CD deploys on push to main.
_Note: Deployment was moved ahead of multiplayer — the app is useful without multiplayer but not without deployment._
### Infrastructure
- [x] Hetzner VPS provisioned (Debian 13, ARM64, 4GB RAM)
- [x] SSH hardening, ufw firewall, fail2ban
- [x] Docker + Docker Compose installed
- [x] Domain DNS: A record + wildcard `*.lilastudy.com` pointing to VPS
### Reverse proxy
- [x] Caddy container with automatic HTTPS (Let's Encrypt)
- [x] Subdomain routing: `lilastudy.com` → web, `api.lilastudy.com` → API, `git.lilastudy.com` → Forgejo
### Docker stack
- [x] Production `docker-compose.yml` with all services on shared network
- [x] No ports exposed on internal services — only Caddy (80/443) and Forgejo SSH (2222)
- [x] Production Dockerfile stages for API (runner) and frontend (nginx:alpine)
- [x] Monorepo package exports fixed for production (dist/src paths)
- [x] Production `.env` with env-driven CORS, auth URLs, cookie domain
### Git server + container registry
- [x] Forgejo running with built-in container registry
- [x] SSH on port 2222, dev laptop `~/.ssh/config` configured
- [x] Repository created, code pushed
### CI/CD
- [x] Forgejo Actions enabled
- [x] Forgejo Runner container on VPS with Docker socket access
- [x] `.forgejo/workflows/deploy.yml` — build, push, deploy via SSH on push to main
- [x] Registry and SSH secrets configured in Forgejo
### Database
- [x] Initial seed via pg_dump from dev laptop
- [x] Seeding script is idempotent (onConflictDoNothing) for future data additions
- [x] Schema migrations via Drizzle (migrate first, deploy second)
### OAuth
- [x] Google and GitHub OAuth redirect URIs configured for production
- [x] Cross-subdomain cookies via COOKIE_DOMAIN=.lilastudy.com
### Backups
- [x] Daily cron job (3 AM) with pg_dump, 7-day retention
- [x] Dev laptop auto-syncs backups on login via rsync
### Documentation
- [x] `deployment.md` covering full infrastructure setup
---
## Phase 4 — Multiplayer Lobby
**Goal:** Players can create and join rooms; the host sees all joined players in real time.
**Done when:** Two browser tabs can join the same room and see each other's display names update live via WebSocket.
- [ ] Write Drizzle schema: `rooms`, `room_players`
- [ ] Write and run migration
- [ ] `POST /rooms` and `POST /rooms/:code/join` REST endpoints
- [ ] `RoomService`: create room with short code, join room, enforce max player limit
- [ ] WebSocket server: attach `ws` upgrade handler to Express HTTP server
- [ ] WS auth middleware: validate JWT on upgrade
- [ ] WS message router: dispatch by `type`
- [ ] `room:join` / `room:leave` handlers → broadcast `room:state`
- [ ] Room membership tracked in Valkey (ephemeral) + PostgreSQL (durable)
- [ ] Define all WS event Zod schemas in `packages/shared`
- [ ] Frontend: `/multiplayer/lobby` — create room + join-by-code
- [ ] Frontend: `/multiplayer/room/:code` — player list, room code, "Start Game" (host only)
- [ ] Frontend: WS client singleton with reconnect
---
## Phase 5 — Multiplayer Game
**Goal:** Host starts a game; all players answer simultaneously in real time; a winner is declared.
**Done when:** 24 players complete a 10-round game with correct live scores and a winner screen.
- [ ] `GameService`: generate question sequence, enforce 15s server timer
- [ ] `room:start` WS handler → broadcast first `game:question`
- [ ] `game:answer` WS handler → collect per-player answers
- [ ] On all-answered or timeout → evaluate, broadcast `game:answer_result`
- [ ] After N rounds → broadcast `game:finished`, update DB (transactional)
- [ ] Frontend: `/multiplayer/game/:code` route
- [ ] Frontend: reuse `QuestionCard` + `OptionButton`; add countdown timer
- [ ] Frontend: `ScoreBoard` component — live per-player scores
- [ ] Frontend: `GameFinished` screen — winner highlight, final scores, play again
- [ ] Unit tests for `GameService` (round evaluation, tie-breaking, timeout)
---
## Phase 7 — Polish & Hardening
**Goal:** Production-ready for real users.
- [x] CI/CD pipeline (Forgejo Actions → SSH deploy)
- [x] Database backups (cron → dev laptop sync)
- [ ] Rate limiting on API endpoints
- [ ] Graceful WS reconnect with exponential back-off
- [ ] React error boundaries
- [ ] `GET /users/me/stats` endpoint + profile page
- [ ] Accessibility pass (keyboard nav, ARIA on quiz buttons)
- [ ] Favicon, page titles, Open Graph meta
- [ ] Offsite backup storage (Hetzner Object Storage)
- [ ] Monitoring/logging (uptime, centralized logs)
- [ ] Valkey for game session store (replace in-memory)
---
## Dependency Graph
```text
Phase 0 (Foundation) ✅
└── Phase 1 (Vocabulary Data + API) ✅
└── Phase 2 (Singleplayer UI) ✅
├── Phase 3 (Auth) ✅
│ └── Phase 6 (Deployment + CI/CD) ✅
└── Phase 4 (Multiplayer Lobby)
└── Phase 5 (Multiplayer Game)
└── Phase 7 (Hardening)
```