feat: Profil-Kategorien + Begrüßung in Zielsprache

languages.greeting (de/en/sv geseedet), neue pair_categories-Tabelle
(abgeleitet aus statement- und objektverknüpften Wörtern via
word_categories) inkl. Backfill für bereits veröffentlichte Pairs.
derivePairCategories() wird beim Publish (pairs + pipeline) aufgerufen.
/auth/me liefert language_target_greeting, /auth/stats liefert
categories[] mit Punkten je Kategorie fürs Profil.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:55:57 +02:00
parent 508d6993ee
commit 9738d3e35a
7 changed files with 268 additions and 5 deletions

View File

@@ -141,7 +141,8 @@ router.get('/me', requireJwt, async (req, res, next) => {
COALESCE(up.daily_goal_ep, 30) AS daily_goal_ep,
up.last_practice_at,
ln.id AS language_native_id, ln.short_en AS language_native_short, ln.titel_de AS language_native_titel,
lt.id AS language_target_id, lt.short_en AS language_target_short, lt.titel_de AS language_target_titel
lt.id AS language_target_id, lt.short_en AS language_target_short, lt.titel_de AS language_target_titel,
lt.greeting AS language_target_greeting
FROM users u
LEFT JOIN users_public up ON up.user_id = u.id
LEFT JOIN user_names un ON un.id = up.username_id
@@ -275,6 +276,22 @@ router.get('/stats', requireJwt, async (req, res, next) => {
return { label, value: acc && acc.seen > 0 ? acc.correct / acc.seen : 0, seen: acc?.seen || 0 };
});
// Punkte je Kategorie (Lebensmittel/Tiere/Beruf …) — abgeleitet über pair_categories.
// Mehrfach-Kategorien eines Pairs zählen bewusst zu jeder Kategorie.
const categoryRows = await query(
`SELECT c.id, c.titel_de AS label,
COALESCE(SUM(upp.earned_points), 0)::int AS points,
COALESCE(SUM(upp.seen_count), 0)::int AS seen
FROM user_pair_progress upp
JOIN pair_categories pc ON pc.pair_id = upp.pair_id
JOIN categories c ON c.id = pc.category_id
WHERE upp.user_id = $1
GROUP BY c.id, c.titel_de
HAVING SUM(upp.earned_points) > 0
ORDER BY points DESC`,
[userId]
);
const t = totals.rows[0] || { pairs_practiced: 0, total_seen: 0, total_correct: 0 };
const td = today.rows[0] || { ep: 0, cards: 0, daily_goal_ep: 30 };
@@ -288,6 +305,7 @@ router.get('/stats', requireJwt, async (req, res, next) => {
accuracy: t.total_seen > 0 ? t.total_correct / t.total_seen : 0,
},
skills,
categories: categoryRows.rows,
});
} catch (err) { next(err); }
});