38 lines
4.6 KiB
Markdown
38 lines
4.6 KiB
Markdown
# CLAUDE.md
|
||
|
||
REST-API für das snakkimo-Projekt. Node/Express + PostgreSQL (`pg`, kein ORM), Bild-Assets auf Hetzner Object Storage (S3-kompatibel). Ausführliche API-Doku in [README.md](README.md).
|
||
|
||
## Befehle
|
||
- `npm run dev` — lokaler Server mit nodemon (Hot-Reload)
|
||
- `npm start` — Produktion (`node src/index.js`)
|
||
- Keine Tests / kein Linter konfiguriert.
|
||
|
||
## Architektur
|
||
- Einstieg: [src/index.js](src/index.js) — registriert alle Routen, jede `/api/*`-Route ist mit der `auth`-Middleware geschützt.
|
||
- **Migrationen laufen automatisch beim Boot** ([src/db-migrate.js](src/db-migrate.js)), bevor der Server lauscht. Idempotent halten: `CREATE TABLE IF NOT EXISTS`, Spalten-Renames mit `.catch(() => {})`. Es gibt **kein** separates Migrations-Tool — Schema-Änderungen hier eintragen.
|
||
- `src/db.js` exportiert `query(text, params)` und `pool`. Immer parametrisierte Queries (`$1, $2 …`), nie String-Interpolation von User-Input.
|
||
- `src/routes/` — eine Datei pro Entität. `src/lib/`, `src/middleware/`, `src/s3.js`, `src/voices.js` für geteilte Logik.
|
||
- **Hintergrund-Job (Auto-Kategorisierung):** [src/index.js](src/index.js) startet ~30 s nach dem Boot und stündlich `runCategorizationTick()` ([src/lib/classifyWords.js](src/lib/classifyWords.js)). Er klassifiziert in Pairs verwendete Wörter ohne Kategorie per **Anthropic Message Batches API** (Haiku, asynchron, ~50 % günstiger) gegen die feste Taxonomie und materialisiert `pair_categories`. ⚠️ Braucht `ANTHROPIC_API_KEY` und verursacht echte LLM-Kosten — **auch lokal bei `npm run dev`**. Manuell anstoßen: `POST /api/categories/auto-assign` (`?sync=true` = sofort/synchron statt Batch, `&reset=true` = bestehende Zuordnungen verwerfen und neu klassifizieren).
|
||
- **Kategorie-Datenfluss:** Kategorien hängen an Wörtern (`word_categories`, feste Taxonomie wird in [src/db-migrate.js](src/db-migrate.js) geseedet). `pair_categories` wird daraus abgeleitet ([src/lib/pairCategories.js](src/lib/pairCategories.js) `derivePairCategories`) — beim Pair-Publish (`routes/pairs.js`, `routes/pipeline.js`) und im Job. `GET /auth/stats` liefert daraus die Punkte je Kategorie fürs Profil; `GET /auth/me` liefert `language_target_greeting` (Spalte `languages.greeting`, de/en/sv geseedet). Async-Batch-Status liegt in `category_batches`.
|
||
|
||
## Fortschritt / Gamification ([src/routes/auth.js](src/routes/auth.js))
|
||
|
||
- **Level-Kurve = Single Source of Truth:** [src/lib/leveling.js](src/lib/leveling.js) (`levelForEp`/`levelInfo`, progressive Kurve — kumulativ `5·n·(n+3)` EP, Level 1 bei 20 EP). Wird in `GET /auth/me` (liefert `level` + `ep_into_level` + `ep_to_next_level`) **und** `POST /auth/progress` genutzt. Das Frontend spiegelt dieselbe Kurve nur als Fallback — Kurvenänderungen hier vornehmen.
|
||
- **`POST /auth/progress`** bucht EP/Streak/Pair-Statistik und liefert den **Milestone-Vertrag**: `{ total_ep, level, prev_level, streak_days, streak_increased, daily_ep, daily_goal_ep, goal_just_reached, unlocked_achievements }`. Ein CTE fängt die **Pre-Update-Werte** mit, damit Level-Up/Streak-Up atomar erkennbar sind. `daily_goal_ep` via `PUT /auth/goal` (geklemmt 5–500).
|
||
- **Erfolge (Achievements):** [src/lib/achievements.js](src/lib/achievements.js) definiert die Erfolge und schaltet sie **dedup-sicher** frei (`INSERT … ON CONFLICT DO NOTHING RETURNING` → nur Neues). Persistenz in Tabelle `user_achievements` (Migration in `db-migrate.js`). `/auth/progress` ruft `evaluateAchievements` (defensiv gekapselt, darf die Buchung nicht kippen); `GET /auth/achievements` listet alle mit Status fürs Profil.
|
||
|
||
## Konventionen
|
||
- **Code-Kommentare auf Deutsch**, Code/Bezeichner auf Englisch (dem Bestand folgen).
|
||
- Route-Handler-Muster: `async (req, res, next) => { try { … } catch (err) { next(err); } }`. Fehler an den zentralen Error-Handler in `index.js` durchreichen, nicht selbst 500en.
|
||
- Listen-Endpoints: `limit`/`offset` aus Query, `limit` hart begrenzen (z. B. `Math.min(parseInt(limit), 500)`).
|
||
- Status-Felder gegen eine `STATUSES`-Whitelist prüfen → bei Verstoß `400`.
|
||
- **Sprachen-Suffixe: `_de`, `_en`, `_sv`.** `_se` ist veraltet (falscher ISO-639-1-Code) und wird beim Boot zu `_sv` umbenannt — niemals neue `_se`-Spalten anlegen.
|
||
|
||
## Auth (zwei Pfade, siehe [src/middleware/auth.js](src/middleware/auth.js))
|
||
1. Statische Tokens aus `API_TOKENS` (komma-separiert) → Server-zu-Server / Admin, keine Rollenprüfung.
|
||
2. JWT aus `/auth/login` · `/auth/register`. Rolle `end-user` bekommt auf allen `/api/*` bewusst **403** (App-Gating).
|
||
|
||
Öffentlich (ohne Auth): `GET /health`, `/auth/*`.
|
||
|
||
Konfig über `.env` (siehe [.env.example](.env.example)). Deployment via Coolify/Docker.
|