# 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] Root `.env.example` for local dev (`docker-compose.yml` + API)

---

## 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.

- [x] Write Drizzle schema: `lobbies`, `lobby_players`
- [x] Write and run migration
- [x] `POST /api/v1/lobbies` and `POST /api/v1/lobbies/:code/join` REST endpoints
- [x] `LobbyService`: create lobby with Crockford Base32 code, join lobby, enforce max player limit
- [x] WebSocket server: attach `ws` upgrade handler to Express HTTP server
- [x] WS auth middleware: validate Better Auth session on upgrade
- [x] WS message router: dispatch by `type` via Zod discriminated union
- [x] `lobby:join` / `lobby:leave` handlers → broadcast `lobby:state`
- [x] Lobby membership tracked in PostgreSQL (durable), game state in-memory (Valkey deferred)
- [x] Define all WS event Zod schemas in `packages/shared`
- [x] Frontend: `/multiplayer` — create lobby + join-by-code
- [x] Frontend: `/multiplayer/lobby/:code` — player list, lobby code, "Start Game" (host only)
- [x] Frontend: WS client class with typed message handlers

---

## 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 3-round game with correct live scores and a winner screen.

- [x] `MultiplayerGameService`: generate question sequence, enforce 15s server timer
- [x] `lobby:start` WS handler → broadcast first `game:question`
- [x] `game:answer` WS handler → collect per-player answers
- [x] On all-answered or timeout → evaluate, broadcast `game:answer_result`
- [x] After N rounds → broadcast `game:finished`, update DB (transactional)
- [x] Frontend: `/multiplayer/game/:code` route
- [x] Frontend: reuse `QuestionCard` + `OptionButton`; round results per player
- [x] Frontend: `MultiplayerScoreScreen` — winner highlight, final scores, play again
- [x] Unit tests for `LobbyService`, WS auth, WS router

---

## 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)
```
