lila/documentation/roadmap.md
lila be7a7903c5 refactor: migrate to deck-based vocabulary curation
Database Schema:
- Add decks table for curated word lists (A1, Most Common, etc.)
- Add deck_terms join table with position ordering
- Link rooms to decks via rooms.deck_id FK
- Remove frequency_rank from terms (now deck-scoped)
- Change users.id to uuid, add openauth_sub for auth mapping
- Add room_players.left_at for disconnect tracking
- Add rooms.updated_at for stale room recovery
- Add CHECK constraints for data integrity (pos, status, etc.)

Extraction Script:
- Rewrite extract.py to mirror complete OMW dataset
- Extract all 25,204 bilingual noun synsets (en-it)
- Remove frequency filtering and block lists
- Output all lemmas per synset for full synonym support
- Seed data now uncurated; decks handle selection

Architecture:
- Separate concerns: raw OMW data in DB, curation in decks
- Enables user-created decks and multiple difficulty levels
- Rooms select vocabulary by choosing a deck
2026-03-27 16:53:26 +01:00

150 lines
7.9 KiB
Markdown
Raw 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.

# Vocabulary Trainer — Roadmap
Each phase produces a working, deployable 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 — just 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`
[x] update decisions.md
## Phase 1 — Vocabulary Data
Goal: Word data lives in the DB and can be queried via the API.
Done when: `GET /api/decks/1/terms?limit=10` returns 10 terms from a specific deck.
[x] Run `extract-en-it-nouns.py` locally → generates `datafiles/en-it-nouns.json`
-- Import ALL available OMW noun synsets (no frequency filtering)
[ ] Write Drizzle schema: `terms`, `translations`, `language_pairs`, `term_glosses`, `decks`, `deck_terms`
[ ] Write and run migration (includes CHECK constraints for `pos`, `gloss_type`)
[ ] Write `packages/db/src/seed.ts` (imports ALL terms + translations, NO decks)
[ ] Write `scripts/build_decks.ts` (reads external CEFR lists, matches to DB, creates decks)
[ ] Download CEFR A1/A2 noun lists (from GitHub repos)
[ ] Run `pnpm db:seed` → populates terms
[ ] Run `pnpm db:build-decks` → creates curated decks
[ ] Implement `DeckRepository.getTerms(deckId, limit, offset)`
[ ] Implement `QuizService.attachDistractors(terms)` — same POS, server-side, no duplicates
[ ] Implement `GET /language-pairs`, `GET /decks`, `GET /decks/:id/terms` endpoints
[ ] Define Zod response schemas in `packages/shared`
[ ] Unit tests for `QuizService` (correct POS filtering, never includes the answer)
[ ] update decisions.md
## Phase 2 — Auth
Goal: Users can log in via Google or GitHub and stay logged in.
Done when: JWT from OpenAuth is validated by the API; protected routes redirect unauthenticated users; user row is created on first login.
[ ] Add OpenAuth service to `docker-compose.yml`
[ ] Write Drizzle schema: `users` (uuid `id`, text `openauth_sub`, no games_played/won columns)
[ ] Write and run migration (includes `updated_at` + triggers)
[ ] Implement JWT validation middleware in `apps/api`
[ ] Implement `GET /api/auth/me` (validate token, upsert user row via `openauth_sub`, return user)
[ ] Define auth Zod schemas in `packages/shared`
[ ] Frontend: login page with "Continue with Google" + "Continue with GitHub" buttons
[ ] Frontend: redirect to `auth.yourdomain.com` → receive JWT → store in memory + HttpOnly cookie
[ ] Frontend: TanStack Router auth guard (redirects unauthenticated users)
[ ] Frontend: TanStack Query `api.ts` attaches token to every request
[ ] Unit tests for JWT middleware
[ ] update decisions.md
## Phase 3 — Single-player Mode
Goal: A logged-in user can complete a full solo quiz session.
Done when: User sees 10 questions, picks answers, sees their final score.
[ ] Frontend: `/singleplayer` route
[ ] `useQuizSession` hook: fetch terms, manage question index + score state
[ ] `QuestionCard` component: prompt word + 4 answer buttons
[ ] `OptionButton` component: idle / correct / wrong states
[ ] `ScoreScreen` component: final score + play-again button
[ ] TanStack Query integration for `GET /terms`
[ ] RTL tests for `QuestionCard` and `OptionButton`
[ ] update decisions.md
## Phase 4 — Multiplayer Rooms (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` (add `deck_id` FK to rooms)
[ ] Write and run migration (includes CHECK constraints: `code=UPPER(code)`, `status`, `max_players`)
[ ] Add indexes: `idx_rooms_host`, `idx_room_players_score`
[ ] `POST /rooms` and `POST /rooms/:code/join` REST endpoints
[ ] `RoomService`: create room with short code, join room, enforce max player limit
[ ] `POST /rooms` accepts `deck_id` (which vocabulary deck to use)
[ ] WebSocket server: attach `ws` upgrade handler to the Express HTTP server
[ ] WS auth middleware: validate OpenAuth JWT on upgrade
[ ] WS message router: dispatch incoming messages by `type`
[ ] `room:join` / `room:leave` handlers → broadcast `room:state` to all room members
[ ] Room membership tracked in Valkey (ephemeral) + `room_players` in PostgreSQL (durable)
[ ] Define all WS event Zod schemas in `packages/shared`
[ ] Frontend: `/multiplayer/lobby` — create room form + join-by-code form
[ ] Frontend: `/multiplayer/room/:code` — player list, room code display, "Start Game" (host only)
[ ] Frontend: `ws.ts` singleton WS client with reconnect on drop
[ ] Frontend: Zustand `gameStore` handles incoming `room:state` events
[ ] update decisions.md
## 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 for a room, enforce server-side 15 s timer
[ ] `room:start` WS handler → begin question loop, 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 `rooms.status` + `room_players.score` in DB (transactional)
[ ] Frontend: `/multiplayer/game/:code` route
[ ] Frontend: extend Zustand store with `currentQuestion`, `roundAnswers`, `scores`
[ ] Frontend: reuse `QuestionCard` + `OptionButton`; add countdown timer ring
[ ] Frontend: `ScoreBoard` component — live per-player scores after each round
[ ] Frontend: `GameFinished` screen — winner highlight, final scores, "Play Again" button
[ ] Unit tests for `GameService` (round evaluation, tie-breaking, timeout auto-advance)
[ ] update decisions.md
## Phase 6 — Production Deployment
Goal: App is live on Hetzner, accessible via HTTPS on all subdomains.
Done when: `https://app.yourdomain.com` loads; `wss://api.yourdomain.com` connects; auth flow works end-to-end.
[ ] `docker-compose.prod.yml`: all services + `nginx-proxy` + `acme-companion`
[ ] Nginx config per container: `VIRTUAL_HOST` + `LETSENCRYPT_HOST` env vars
[ ] Production `.env` files on VPS (OpenAuth secrets, DB credentials, Valkey URL)
[ ] Drizzle migration runs on `api` container start (includes CHECK constraints + triggers)
[ ] Seed production DB (run `seed.ts` once)
[ ] Smoke test: login → solo game → create room → multiplayer game end-to-end
[ ] update decisions.md
## Phase 7 — Polish & Hardening (post-MVP)
Not required to ship, but address before real users arrive.
[ ] Rate limiting on API endpoints (`express-rate-limit`)
[ ] Graceful WS reconnect with exponential back-off
[ ] React error boundaries
[ ] `GET /users/me/stats` endpoint (aggregates from `room_players`) + profile page
[ ] Accessibility pass (keyboard nav, ARIA on quiz buttons)
[ ] Favicon, page titles, Open Graph meta
[ ] CI/CD pipeline (GitHub Actions → SSH deploy on push to `main`)
[ ] Database backups (cron → Hetzner Object Storage)
[ ] update decisions.md
Dependency Graph
Phase 0 (Foundation)
└── Phase 1 (Vocabulary Data)
└── Phase 2 (Auth)
├── Phase 3 (Singleplayer) ← parallel with Phase 4
└── Phase 4 (Room Lobby)
└── Phase 5 (Multiplayer Game)
└── Phase 6 (Deployment)