Compare commits

..

4 commits
dev ... main

Author SHA1 Message Date
lila
e5595b5039 updating documentation
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m3s
2026-04-14 19:35:49 +02:00
lila
201f462447 cleaning up
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m7s
2026-04-14 19:19:07 +02:00
lila
3b2ecf6ee3 adding debugging step
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m10s
2026-04-14 18:56:59 +02:00
lila
46fb7dbdd2 adding docker and openssh client installation
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 20s
2026-04-14 18:33:30 +02:00
8 changed files with 156 additions and 372 deletions

View file

@ -8,6 +8,9 @@ jobs:
build-and-deploy:
runs-on: docker
steps:
- name: Install tools
run: apt-get update && apt-get install -y docker.io openssh-client
- name: Checkout code
uses: https://data.forgejo.org/actions/checkout@v4

View file

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm --filter shared build && pnpm --filter db build && tsx watch src/server.ts",
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/src/server.js",
"test": "vitest"

91
docker-compose.prod.yml Normal file
View file

@ -0,0 +1,91 @@
services:
caddy:
container_name: lila-caddy
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
depends_on:
api:
condition: service_healthy
networks:
- lila-network
api:
container_name: lila-api
build:
context: .
dockerfile: ./apps/api/Dockerfile
target: runner
env_file:
- .env
restart: unless-stopped
healthcheck:
test:
["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]
interval: 5s
timeout: 3s
retries: 5
depends_on:
database:
condition: service_healthy
networks:
- lila-network
web:
container_name: lila-web
build:
context: .
dockerfile: ./apps/web/Dockerfile
target: production
args:
VITE_API_URL: https://api.lilastudy.com
restart: unless-stopped
networks:
- lila-network
database:
container_name: lila-database
image: postgres:18.3-alpine3.23
env_file:
- .env
environment:
- PGDATA=/var/lib/postgresql/data
volumes:
- lila-db:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- lila-network
forgejo:
container_name: lila-forgejo
image: codeberg.org/forgejo/forgejo:11
volumes:
- forgejo-data:/data
environment:
- USER_UID=1000
- USER_GID=1000
ports:
- "2222:22"
restart: unless-stopped
networks:
- lila-network
networks:
lila-network:
volumes:
lila-db:
caddy_data:
caddy_config:
forgejo-data:

View file

@ -42,12 +42,11 @@ services:
volumes:
- ./apps/api:/app/apps/api # Hot reload API code
- ./packages/shared:/app/packages/shared # Hot reload shared
- ./packages/db:/app/packages/db
- /app/node_modules
restart: unless-stopped
healthcheck:
test:
["CMD-SHELL", "wget -qO- http://localhost:3000/api/v1/health || exit 1"]
["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]
interval: 5s
timeout: 3s
retries: 5
@ -67,7 +66,6 @@ services:
- "5173:5173"
volumes:
- ./apps/web:/app/apps/web # Hot reload: local edits reflect immediately
- ./packages/shared:/app/packages/shared
- /app/node_modules # Protect container's node_modules from being overwritten
environment:
- VITE_API_URL=http://localhost:3000

View file

@ -225,9 +225,59 @@ Host git.lilastudy.com
This allows standard git commands without specifying the port.
## CI/CD Pipeline
Automated build and deploy via Forgejo Actions. On every push to `main`, the pipeline builds ARM64 images natively on the VPS, pushes them to the Forgejo registry, and restarts the app containers.
### Components
- **Forgejo Actions** — enabled by default, workflow files in `.forgejo/workflows/`
- **Forgejo Runner** — runs as a container (`lila-ci-runner`) on the VPS, uses the host's Docker socket to build images natively on ARM64
- **Workflow file**`.forgejo/workflows/deploy.yml`
### Pipeline Steps
1. Install Docker CLI and SSH client in the job container
2. Checkout the repository
3. Login to the Forgejo container registry
4. Build API image (target: `runner`)
5. Build Web image (target: `production`, with `VITE_API_URL` baked in)
6. Push both images to `git.lilastudy.com`
7. SSH into the VPS, pull new images, restart `api` and `web` containers, prune old images
### Secrets (stored in Forgejo repo settings → Actions → Secrets)
| Secret | Value |
|---|---|
| REGISTRY_USER | Forgejo username |
| REGISTRY_PASSWORD | Forgejo password |
| SSH_PRIVATE_KEY | Contents of `~/.ssh/ci-runner` on the VPS |
| SSH_HOST | VPS IP address |
| SSH_USER | `lila` |
### Runner Configuration
The runner config is at `/data/config.yml` inside the `lila-ci-runner` container. Key settings:
- `docker_host: "automount"` — mounts the host Docker socket into job containers
- `valid_volumes: ["/var/run/docker.sock"]` — allows the socket mount
- `privileged: true` — required for Docker access from job containers
- `options: "--group-add 989"` — adds the host's docker group (GID 989) to job containers
The runner command must explicitly reference the config file:
```yaml
command: '/bin/sh -c "sleep 5; forgejo-runner -c /data/config.yml daemon"'
```
### Deploy Cycle
Push to main → pipeline runs automatically (~2-5 min) → app is updated. No manual steps required.
To manually trigger a re-run: go to the repo's Actions tab, click on the latest run, and use the re-run button.
## Known Issues and Future Work
- **CI/CD**: Currently manual build-push-pull cycle. Plan: Forgejo Actions with a runner on the VPS building ARM images natively (eliminates QEMU cross-compilation)
- **Backups**: Offsite backup storage (Hetzner Object Storage or similar) should be added
- **Valkey**: Not in the production stack yet. Will be added when multiplayer requires session/room state
- **Monitoring/logging**: No centralized logging or uptime monitoring configured

View file

@ -1,83 +0,0 @@
# Game Modes
This document describes the planned game modes for lila. Each mode uses the same lobby system and vocabulary data but differs in how answers are submitted, scored, and how a winner is determined.
The first multiplayer mode to implement is TBD. The lobby infrastructure (create, join, WebSocket connection) is mode-agnostic — adding a new mode means adding new game logic, not changing the lobby.
---
## TV Quiz Show
**Type:** Multiplayer
**Answer model:** Buzzer — first to press gets to answer
**Rounds:** Fixed (e.g. 10)
A question appears for all players. The first player to buzz in gets to answer. If correct, they score a point. If wrong, other players may get a chance to answer (TBD: whether the question passes to the next buzzer or the round ends). The host or a timer controls the pace.
Key difference from other modes: only one player answers per question. Speed of reaction matters as much as knowledge.
---
## Race to the Top
**Type:** Multiplayer
**Answer model:** Simultaneous — all players answer independently
**Rounds:** None — play until target score reached
All players see the same question and answer independently. No fixed round count. The first player to reach a target number of correct answers wins (e.g. 20). Fast-paced and competitive.
Open questions: what happens if two players hit the target on the same question? Tiebreaker by speed? Shared win?
---
## Chain Link
**Type:** Multiplayer
**Answer model:** Turn-based — one player at a time, in rotation
**Rounds:** None — play until a player fails
Players answer in a fixed rotation: Player 1, Player 2, Player 3, then back to Player 1. Each player gets one question per turn. The game continues until a player answers incorrectly — that player is out (or the game ends). Last correct answerer wins, or the game simply ends on the first wrong answer.
Key difference from other modes: turn-based, not simultaneous. Pressure builds as you wait for your turn.
Open questions: does the player who answers wrong lose, or does the game just end? If the game continues, does it become elimination?
---
## Elimination Round
**Type:** Multiplayer
**Answer model:** Simultaneous — all players answer independently
**Rounds:** Continue until one player remains
All players see the same question and answer simultaneously. Players who answer incorrectly are eliminated. Rounds continue until only one player is left standing.
Open questions: what if everyone gets it wrong in the same round? Reset that round? Eliminate nobody? What if it comes down to two players and both get it wrong repeatedly?
---
## Cooperative Challenge
**Type:** Multiplayer
**Answer model:** TBD
**Rounds:** TBD
Players work together rather than competing. Concept not yet defined. Possible ideas: shared team score with a target, each player contributes answers to a collective pool, or players take turns and the team survives as long as the chain doesn't break.
---
## Single Player Extended
**Type:** Singleplayer
**Answer model:** TBD
**Rounds:** TBD
An expanded version of the current singleplayer quiz. Concept not yet defined. Possible ideas: longer sessions with increasing difficulty, mixed POS/language rounds, streak bonuses, progress tracking across sessions, or timed challenge mode.
---
## Schema Impact
The `lobbies` table includes a `game_mode` column (varchar) with values like `tv_quiz`, `race_to_top`, `chain_link`, `elimination`. Mode-specific settings (e.g. target score for Race to the Top) can be stored in a `settings` jsonb column if needed.
The singleplayer modes (Single Player Extended) don't require a lobby — they extend the existing singleplayer flow.

View file

@ -21,13 +21,18 @@ WARNING! Your credentials are stored unencrypted in '/home/languagedev/.docker/c
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
### docker containers on startup?
laptop: verify if docker containers run on startup (they shouldnt)
### vps setup
- monitoring and logging (eg via chrootkit or rkhunter, logwatch/monit => mails daily with summary)
- ~~keep the vps clean (e.g. old docker images/containers)~~ ✅ CI/CD pipeline runs `docker image prune -f` after deploy
### ~~cd/ci pipeline~~ ✅ RESOLVED
Forgejo Actions with runner on VPS, Forgejo built-in container registry. See `deployment.md`.
### ~~postgres backups~~ ✅ RESOLVED
Daily pg_dump cron job, 7-day retention, dev laptop auto-sync via rsync. See `deployment.md`.
### try now option

View file

@ -1,280 +0,0 @@
#!/bin/bash
# Forgejo batch issue creator for lila
# Usage: FORGEJO_TOKEN=your_token ./create-issues.sh
FORGEJO_URL="https://git.lilastudy.com"
OWNER="forgejo-lila"
REPO="lila"
TOKEN="${FORGEJO_TOKEN:?Set FORGEJO_TOKEN environment variable}"
API="${FORGEJO_URL}/api/v1/repos/${OWNER}/${REPO}"
# Helper: create a label (ignores if already exists)
create_label() {
local name="$1" color="$2" description="$3"
curl -s -X POST "${API}/labels" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${name}\",\"color\":\"${color}\",\"description\":\"${description}\"}" > /dev/null
echo "Label: ${name}"
}
# Helper: create an issue with labels
create_issue() {
local title="$1" body="$2"
shift 2
local labels="$*"
# Build labels JSON array
local label_ids=""
for label in $labels; do
local id
id=$(curl -s "${API}/labels" \
-H "Authorization: token ${TOKEN}" | \
python3 -c "import sys,json; [print(l['id']) for l in json.load(sys.stdin) if l['name']=='${label}']")
if [ -n "$label_ids" ]; then
label_ids="${label_ids},${id}"
else
label_ids="${id}"
fi
done
curl -s -X POST "${API}/issues" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"title\":$(echo "$title" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))'),\"body\":$(echo "$body" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))'),\"labels\":[${label_ids}]}" > /dev/null
echo "Issue: ${title}"
}
echo "=== Creating labels ==="
create_label "feature" "#0075ca" "New user-facing functionality"
create_label "infra" "#e4e669" "Infrastructure, deployment, DevOps"
create_label "debt" "#d876e3" "Technical cleanup, refactoring"
create_label "security" "#b60205" "Security improvements"
create_label "ux" "#1d76db" "User experience, accessibility, polish"
create_label "multiplayer" "#0e8a16" "Multiplayer lobby and game features"
echo ""
echo "=== Creating issues ==="
# ── feature ──
create_issue \
"Add guest/try-now option — play without account" \
"Allow users to play a quiz without signing in so they can see what the app offers before creating an account. Make auth middleware optional on game routes, add a 'Try without account' button on the login/landing page." \
feature
create_issue \
"Add Apple login provider" \
"Add Apple as a social login option via Better Auth. Requires Apple Developer account and Sign in with Apple configuration." \
feature
create_issue \
"Add email+password login" \
"Add traditional email and password authentication as an alternative to social login. Configure via Better Auth." \
feature
create_issue \
"User stats endpoint + profile page" \
"Add GET /users/me/stats endpoint returning games played, score history, etc. Build a frontend profile page displaying the stats." \
feature
# ── infra ──
create_issue \
"Google OAuth app verification and publishing" \
"Currently only test users can log in via Google. Publish the OAuth consent screen so any Google user can sign in. Requires branding verification through Google Cloud Console." \
infra
create_issue \
"Set up Docker credential helper on dev laptop" \
"Docker credentials are stored unencrypted in ~/.docker/config.json. Set up a credential helper to store them securely. See https://docs.docker.com/go/credential-store/" \
infra
create_issue \
"VPS monitoring and logging" \
"Set up monitoring and centralized logging on the VPS. Options: chkrootkit/rkhunter for security, logwatch/monit for daily summaries, uptime monitoring for service health." \
infra
create_issue \
"Move to offsite backup storage" \
"Currently database backups live on the same VPS. Add offsite copies to Hetzner Object Storage or similar S3-compatible service to protect against VPS failure." \
infra
create_issue \
"Replace in-memory game session store with Valkey" \
"Add Valkey container to the production Docker stack. Implement ValkeyGameSessionStore using the existing GameSessionStore interface. Required before multiplayer." \
infra
create_issue \
"Modern env management approach" \
"Evaluate replacing .env files with a more robust approach (e.g. dotenvx, infisical, or similar). Current setup works but .env files are error-prone and not versioned." \
infra
create_issue \
"Pin dependencies in package.json files" \
"Pin all dependency versions in package.json files to exact versions to prevent unexpected updates from breaking builds." \
infra
# ── debt ──
create_issue \
"Rethink organization of datafiles and wordlists" \
"The current layout of data-sources/, scripts/datafiles/, scripts/data-sources/, and packages/db/src/data/ is confusing with overlapping content. Consolidate into a clear structure." \
debt
create_issue \
"Resolve eslint peer dependency warning" \
"eslint-plugin-react-hooks 7.0.1 expects eslint ^3.0.0-^9.0.0 but found 10.0.3. Resolve the peer dependency mismatch." \
debt
# ── security ──
create_issue \
"Rate limiting on API endpoints" \
"Add rate limiting to prevent abuse. At minimum: auth endpoints (brute force prevention), game endpoints (spam prevention). Consider express-rate-limit or similar." \
security
# ── ux ──
create_issue \
"404/redirect handling for unknown routes and subdomains" \
"Unknown routes return raw errors. Add a catch-all route on the frontend for client-side 404s. Consider Caddy fallback for unrecognized subdomains." \
ux
create_issue \
"React error boundaries" \
"Add error boundaries to catch and display runtime errors gracefully instead of crashing the entire app." \
ux
create_issue \
"Accessibility pass" \
"Keyboard navigation for quiz buttons, ARIA labels on interactive elements, focus management during quiz flow." \
ux
create_issue \
"Favicon, page titles, Open Graph meta" \
"Add favicon, set proper page titles per route, add Open Graph meta tags for link previews when sharing." \
ux
# ── multiplayer ──
create_issue \
"Drizzle schema: lobbies, lobby_players + migration" \
"Create lobbies table (id, code, host_user_id, status, is_private, game_mode, settings, created_at) and lobby_players table (lobby_id, user_id, score, joined_at). Run migration. See game-modes.md for game_mode values." \
multiplayer
create_issue \
"REST endpoints: POST /lobbies, POST /lobbies/:code/join" \
"Create lobby (generates short code, sets host) and join lobby (validates code, adds player, enforces max limit)." \
multiplayer
create_issue \
"LobbyService: create lobby, join lobby, enforce player limit" \
"Service layer for lobby management. Generate human-readable codes, validate join requests, track lobby state. Public lobbies are browsable, private lobbies require code." \
multiplayer
create_issue \
"WebSocket server: attach ws upgrade to Express" \
"Attach ws library upgrade handler to the existing Express HTTP server. Handle connection lifecycle." \
multiplayer
create_issue \
"WS auth middleware: validate session on upgrade" \
"Validate Better Auth session on WebSocket upgrade request. Reject unauthenticated connections." \
multiplayer
create_issue \
"WS message router: dispatch by type" \
"Route incoming WebSocket messages by their type field to the appropriate handler. Use Zod discriminated union for type safety." \
multiplayer
create_issue \
"Lobby join/leave handlers + broadcast lobby state" \
"Handle lobby:join and lobby:leave WebSocket events. Broadcast updated player list to all connected players in the lobby." \
multiplayer
create_issue \
"Lobby state in Valkey (ephemeral) + PostgreSQL (durable)" \
"Store live lobby state (connected players, current question, timer) in Valkey. Store durable records (who played, final scores) in PostgreSQL." \
multiplayer
create_issue \
"WS event Zod schemas in packages/shared" \
"Define all WebSocket message types as Zod discriminated unions in packages/shared. Covers lobby events (join, leave, start) and game events (question, answer, result, finished)." \
multiplayer
create_issue \
"Frontend: lobby browser + create/join lobby" \
"Lobby list showing public open lobbies. Create lobby form (game mode, public/private). Join-by-code input for private lobbies." \
multiplayer
create_issue \
"Frontend: lobby view (player list, code, start game)" \
"Show lobby code, connected players, game mode. Host sees Start Game button. Players see waiting state. Real-time updates via WebSocket." \
multiplayer
create_issue \
"Frontend: WS client singleton with reconnect" \
"WebSocket client that maintains a single connection, handles reconnection on disconnect, and dispatches incoming messages to the appropriate state handlers." \
multiplayer
create_issue \
"GameService: question sequence + server timer" \
"Generate question sequence for a lobby game. Enforce per-question timer (e.g. 15s). Timer logic varies by game mode — see game-modes.md." \
multiplayer
create_issue \
"lobby:start WS handler — broadcast first question" \
"When host starts the game, generate questions, change lobby status to in_progress, broadcast first question to all players." \
multiplayer
create_issue \
"game:answer WS handler — collect answers" \
"Receive player answers via WebSocket. Track who has answered. Behavior varies by game mode (simultaneous vs turn-based vs buzzer)." \
multiplayer
create_issue \
"Answer evaluation + broadcast results" \
"On all-answered or timeout: evaluate answers, calculate scores, broadcast game:answer_result to all players. Then send next question or end game." \
multiplayer
create_issue \
"Game finished: broadcast results, update DB" \
"After final round: broadcast game:finished with final scores and winner. Write game results to PostgreSQL (transactional). Change lobby status to finished." \
multiplayer
create_issue \
"Frontend: multiplayer game route" \
"Route for active multiplayer games. Receives questions and results via WebSocket. Reuses QuestionCard and OptionButton components." \
multiplayer
create_issue \
"Frontend: countdown timer component" \
"Visual countdown timer synchronized with server timer. Shows remaining seconds per question." \
multiplayer
create_issue \
"Frontend: ScoreBoard component (live per-player scores)" \
"Displays live scores for all players during a multiplayer game. Updates in real-time via WebSocket." \
multiplayer
create_issue \
"Frontend: GameFinished screen" \
"Winner highlight, final scores, play again option. Returns to lobby on play again." \
multiplayer
create_issue \
"Multiplayer GameService unit tests" \
"Unit tests for round evaluation, scoring, tie-breaking, timeout handling across different game modes." \
multiplayer
create_issue \
"Graceful WS reconnect with exponential back-off" \
"Handle WebSocket disconnections gracefully. Reconnect with exponential back-off. Restore game state on reconnection if game is still in progress." \
multiplayer
echo ""
echo "=== Done ==="