Merge branch 'dev'
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m12s

This commit is contained in:
lila 2026-04-19 19:26:25 +02:00
commit d2314168f8
28 changed files with 1005 additions and 463 deletions

5
documentation/design.md Normal file
View file

@ -0,0 +1,5 @@
# design
## notes
break points

View file

@ -4,6 +4,7 @@
- pinning dependencies in package.json files
- rethink organisation of datafiles and wordlists
- admin dashboard for user management, also overview of words and languages and all their stats
## problems+thoughts

View file

@ -18,7 +18,7 @@ Each phase produces a working increment. Nothing is built speculatively.
- [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`
- [x] Root `.env.example` for local dev (`docker-compose.yml` + API)
---
@ -176,37 +176,36 @@ _Note: Deployment was moved ahead of multiplayer — the app is useful without m
**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
- [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:** 24 players complete a 10-round game with correct live scores and a winner screen.
**Done when:** 24 players complete a 3-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)
- [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
---
@ -236,7 +235,7 @@ Phase 0 (Foundation) ✅
└── Phase 2 (Singleplayer UI) ✅
├── Phase 3 (Auth) ✅
│ └── Phase 6 (Deployment + CI/CD) ✅
└── Phase 4 (Multiplayer Lobby)
└── Phase 5 (Multiplayer Game)
└── Phase 4 (Multiplayer Lobby)
└── Phase 5 (Multiplayer Game)
└── Phase 7 (Hardening)
```

View file

@ -7,7 +7,7 @@
## 1. Project Overview
A vocabulary trainer for EnglishItalian words. The quiz format is Duolingo-style: one word is shown as a prompt, and the user picks the correct translation from four choices (1 correct + 3 distractors of the same part-of-speech). The long-term vision is a multiplayer competitive game, but the MVP is a polished singleplayer experience.
A vocabulary trainer for EnglishItalian words. The quiz format is Duolingo-style: one word is shown as a prompt, and the user picks the correct translation from four choices (1 correct + 3 distractors of the same part-of-speech). The app supports both singleplayer and real-time multiplayer game modes.
**The core learning loop:**
Show word → pick answer → see result → next word → final score
@ -29,13 +29,13 @@ The vocabulary data comes from WordNet + the Open Multilingual Wordnet (OMW). A
- Multiplayer mode: create a room, share a code, 24 players answer simultaneously in real time, live scores, winner screen
- 1000+ EnglishItalian nouns seeded from WordNet
This is the full vision. The MVP deliberately ignores most of it.
This is the full vision. The current implementation already covers most of it; remaining items are captured in the roadmap and the Post-MVP ladder below.
---
## 3. MVP Scope
**Goal:** A working, presentable singleplayer quiz that can be shown to real people.
**Goal:** A working, presentable vocabulary trainer that can be shown to real people (singleplayer and multiplayer), with a production deployment.
### What is IN the MVP
@ -45,16 +45,14 @@ This is the full vision. The MVP deliberately ignores most of it.
- Clean, mobile-friendly UI (Tailwind + shadcn/ui)
- Global error handler with typed error classes
- Unit + integration tests for the API
- Local dev only (no deployment for MVP)
- Authentication via Better Auth (Google + GitHub)
- Multiplayer lobby + game over WebSockets
- Production deployment (Docker Compose + Caddy + Hetzner) and CI/CD (Forgejo Actions)
### What is CUT from the MVP
| Feature | Why cut |
| ------------------------------- | -------------------------------------- |
| Authentication (Better Auth) | No user accounts needed for a demo |
| Multiplayer (WebSockets, rooms) | Core quiz works without it |
| Valkey / Redis cache | Only needed for multiplayer room state |
| Deployment to Hetzner | Ship to people locally first |
| User stats / profiles | Needs auth |
These are not deleted from the plan — they are deferred. The architecture is already designed to support them. See Section 11 (Post-MVP Ladder).
@ -80,15 +78,15 @@ The monorepo structure and tooling are already set up. This is the full stack.
| Auth | Better Auth (Google + GitHub) | ✅ |
| Deployment | Docker Compose, Caddy, Hetzner | ✅ |
| CI/CD | Forgejo Actions | ✅ |
| Realtime | WebSockets (`ws` library) | ❌ post-MVP |
| Cache | Valkey | ❌ post-MVP |
| Realtime | WebSockets (`ws` library) | |
| Cache | Valkey | ⚠️ optional (used locally; production/state hardening) |
---
## 5. Repository Structure
```text
vocab-trainer/
lila/
├── .forgejo/
│ └── workflows/
│ └── deploy.yml — CI/CD pipeline (build, push, deploy)
@ -154,7 +152,6 @@ vocab-trainer/
├── scripts/ — Python extraction/comparison/merge scripts
├── documentation/ — project docs
├── docker-compose.yml — local dev stack
├── docker-compose.prod.yml — production config reference
├── Caddyfile — reverse proxy routing
└── pnpm-workspace.yaml
```
@ -307,8 +304,8 @@ After completing a task: share the code, ask what to refactor and why. The LLM s
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
| User Stats | Games played, score history, profile page | ❌ |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | |
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
>>>>>>> dev
@ -323,12 +320,20 @@ After completing a task: share the code, ask what to refactor and why. The LLM s
All are new tables referencing existing `terms` rows via FK. No existing schema changes required.
### Multiplayer Architecture (deferred)
### Multiplayer Architecture (current + deferred)
- WebSocket protocol: `ws` library, Zod discriminated union for message types
- Room model: human-readable codes (e.g. `WOLF-42`), not matchmaking queue
- Game mechanic: simultaneous answers, 15-second server timer, all players see same question
- Valkey for ephemeral room state, PostgreSQL for durable records
**Implemented now:**
- WebSocket protocol uses the `ws` library with a Zod discriminated union for message types (defined in `packages/shared`)
- Room model uses human-readable codes (no matchmaking queue)
- Lobby flow (create/join/leave) is real-time over WS, backed by PostgreSQL for durable membership/state
- Multiplayer game flow is real-time: host starts, all players see the same question, answers are collected simultaneously, with a server-enforced 15s timer and live scoring
- WebSocket connections are authenticated (Better Auth session validation on upgrade)
**Deferred / hardening:**
- Valkey-backed ephemeral state (room/game/session store) where in-memory state becomes a bottleneck
- Graceful reconnect/resume flows and more robust failure handling (tracked in Phase 7)
### Infrastructure (current)
@ -343,7 +348,7 @@ See `deployment.md` for full infrastructure documentation.
---
## 12. Definition of Done (MVP)
## 12. Definition of Done (Current Baseline)
- [x] API returns quiz terms with correct distractors
- [x] User can complete a quiz without errors
@ -352,6 +357,9 @@ See `deployment.md` for full infrastructure documentation.
- [x] No hardcoded data — everything comes from the database
- [x] Global error handler with typed error classes
- [x] Unit + integration tests for API
- [x] Auth works end-to-end (Google + GitHub via Better Auth)
- [x] Multiplayer works end-to-end (lobby + real-time game over WebSockets)
- [x] Production deployment is live behind HTTPS (Caddy) with CI/CD deploys via Forgejo Actions
---
@ -367,8 +375,8 @@ Phase 0 (Foundation) ✅
└── Phase 2 (Singleplayer UI) ✅
├── Phase 3 (Auth) ✅
│ └── Phase 6 (Deployment + CI/CD) ✅
└── Phase 4 (Multiplayer Lobby)
└── Phase 5 (Multiplayer Game)
└── Phase 4 (Multiplayer Lobby)
└── Phase 5 (Multiplayer Game)
└── Phase 7 (Hardening)
```