4.6 KiB
4.6 KiB
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 derauth-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.jsexportiertquery(text, params)undpool. 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.jsfü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 materialisiertpair_categories. ⚠️ BrauchtANTHROPIC_API_KEYund verursacht echte LLM-Kosten — auch lokal beinpm 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_categorieswird daraus abgeleitet (src/lib/pairCategories.jsderivePairCategories) — beim Pair-Publish (routes/pairs.js,routes/pipeline.js) und im Job.GET /auth/statsliefert daraus die Punkte je Kategorie fürs Profil;GET /auth/meliefertlanguage_target_greeting(Spaltelanguages.greeting, de/en/sv geseedet). Async-Batch-Status liegt incategory_batches.
Fortschritt / Gamification (src/routes/auth.js)
- Level-Kurve = Single Source of Truth: src/lib/leveling.js (
levelForEp/levelInfo, progressive Kurve — kumulativ5·n·(n+3)EP, Level 1 bei 20 EP). Wird inGET /auth/me(liefertlevel+ep_into_level+ep_to_next_level) undPOST /auth/progressgenutzt. Das Frontend spiegelt dieselbe Kurve nur als Fallback — Kurvenänderungen hier vornehmen. POST /auth/progressbucht 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_epviaPUT /auth/goal(geklemmt 5–500).- 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 Tabelleuser_achievements(Migration indb-migrate.js)./auth/progressruftevaluateAchievements(defensiv gekapselt, darf die Buchung nicht kippen);GET /auth/achievementslistet 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 inindex.jsdurchreichen, nicht selbst 500en. - Listen-Endpoints:
limit/offsetaus Query,limithart begrenzen (z. B.Math.min(parseInt(limit), 500)). - Status-Felder gegen eine
STATUSES-Whitelist prüfen → bei Verstoß400. - Sprachen-Suffixe:
_de,_en,_sv._seist veraltet (falscher ISO-639-1-Code) und wird beim Boot zu_svumbenannt — niemals neue_se-Spalten anlegen.
Auth (zwei Pfade, siehe src/middleware/auth.js)
- Statische Tokens aus
API_TOKENS(komma-separiert) → Server-zu-Server / Admin, keine Rollenprüfung. - JWT aus
/auth/login·/auth/register. Rolleend-userbekommt auf allen/api/*bewusst 403 (App-Gating).
Öffentlich (ohne Auth): GET /health, /auth/*.
Konfig über .env (siehe .env.example). Deployment via Coolify/Docker.