Files
snakkimo-API/CLAUDE.md

4.6 KiB
Raw Blame History

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.

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 — registriert alle Routen, jede /api/*-Route ist mit der auth-Middleware geschützt.
  • Migrationen laufen automatisch beim Boot (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 startet ~30 s nach dem Boot und stündlich runCategorizationTick() (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 geseedet). pair_categories wird daraus abgeleitet (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)

  • Level-Kurve = Single Source of Truth: 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 5500).
  • Erfolge (Achievements): 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)

  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). Deployment via Coolify/Docker.