diff --git a/documentation/roadmap.md b/documentation/roadmap.md new file mode 100644 index 0000000..7739770 --- /dev/null +++ b/documentation/roadmap.md @@ -0,0 +1,185 @@ +# Glossa — 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:** 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`) +- [ ] Write and run migration +- [ ] Implement JWT validation middleware in `apps/api` +- [ ] Implement `GET /api/auth/me` (validate token, upsert user row, return user) +- [ ] Define auth Zod schemas in `packages/shared` +- [ ] Frontend: login page with Google + GitHub buttons +- [ ] Frontend: redirect to auth service → receive JWT → store in memory + HttpOnly cookie +- [ ] Frontend: TanStack Router auth guard +- [ ] Frontend: TanStack Query `api.ts` attaches token to every request +- [ ] Unit tests for JWT middleware + +--- + +## 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:** 2–4 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 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` +- [ ] Production `.env` files on VPS +- [ ] Drizzle migration runs on `api` container start +- [ ] Seed production DB +- [ ] Smoke test: login → solo game → multiplayer game end-to-end + +--- + +## Phase 7 — Polish & Hardening + +**Goal:** Production-ready for real users. + +- [ ] 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 +- [ ] CI/CD pipeline (GitHub Actions → SSH deploy) +- [ ] Database backups (cron → Hetzner Object Storage) + +--- + +## Dependency Graph + +```text +Phase 0 (Foundation) ✅ +└── Phase 1 (Vocabulary Data + API) ✅ + └── Phase 2 (Singleplayer UI) ✅ + └── Phase 3 (Auth) + ├── Phase 4 (Multiplayer Lobby) + │ └── Phase 5 (Multiplayer Game) + │ └── Phase 6 (Deployment) + └── Phase 7 (Hardening) +``` diff --git a/documentation/spec.md b/documentation/spec.md index a358d62..0236c01 100644 --- a/documentation/spec.md +++ b/documentation/spec.md @@ -308,37 +308,7 @@ All are new tables referencing existing `terms` rows via FK. No existing schema ## 13. Roadmap -### Phase 0 — Foundation ✅ - -Empty repo that builds, lints, and runs end-to-end. `pnpm dev` starts both apps; `GET /api/health` returns 200; React renders a hello page. - -### Phase 1 — Vocabulary Data + API ✅ - -Word data lives in the DB. API returns quiz sessions with distractors. CEFR enrichment pipeline complete. Global error handler and tests implemented. - -### Phase 2 — Singleplayer Quiz UI ✅ - -User can complete a full quiz in the browser. Settings UI, question cards, answer feedback, score screen. - -### Phase 3 — Auth - -Users can log in via Google or GitHub and stay logged in. JWT validated by API. User row created on first login. - -### Phase 4 — Multiplayer Lobby - -Players can create and join rooms. Two browser tabs can join the same room and see each other via WebSocket. - -### Phase 5 — Multiplayer Game - -Host starts a game. All players answer simultaneously in real time. Winner declared. - -### Phase 6 — Production Deployment - -App is live on Hetzner with HTTPS. Auth flow works end-to-end. - -### Phase 7 — Polish & Hardening - -Rate limiting, reconnect logic, error boundaries, CI/CD, DB backups. +See `roadmap.md` for the full roadmap with task-level checkboxes. ### Dependency Graph