184 lines
8 KiB
Markdown
184 lines
8 KiB
Markdown
# 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:** 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 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)
|
||
```
|