feat: Status-Pipeline (reviewed), Audio-Verknüpfung+Coverage, EP-Fortschritt, Wort-Generierung

- reviewed-Status für objects/questions/statements/pairs (Constraints)
- feed: nur fertige Inhalte (published + Bild + Audio-Gate), audio_url
- pairs: Publish-Gating (draft→published = 409)
- audios: source_table/source_id/source_field/language + Unique-Index;
  generate-for, generate-batch, GET /coverage; voices.js (Voice je Sprache)
- auth: POST /auth/progress, /auth/me mit total_ep/streak/level;
  users_public EP-Spalten + user_pair_progress.earned_points
- claude: POST /generate-words; words POST akzeptiert status

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 21:29:48 +02:00
parent 75f05f45f2
commit 9bfd5e8dba
9 changed files with 457 additions and 74 deletions

View File

@@ -136,6 +136,9 @@ router.get('/me', requireJwt, async (req, res, next) => {
const r = await query(
`SELECT u.id, u.email, u.role,
un.username,
COALESCE(up.total_ep, 0) AS total_ep,
COALESCE(up.streak_days, 0) AS streak_days,
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
FROM users u
@@ -147,7 +150,54 @@ router.get('/me', requireJwt, async (req, res, next) => {
[req.user.userId]
);
if (!r.rows.length) return res.status(404).json({ error: 'User not found' });
res.json(r.rows[0]);
const row = r.rows[0];
row.level = Math.floor((row.total_ep || 0) / 500);
res.json(row);
} catch (err) { next(err); }
});
// POST /auth/progress — eine gelöste Karte verbuchen (EP + Streak + Detail pro Pair)
router.post('/progress', requireJwt, async (req, res, next) => {
try {
const userId = req.user.userId;
const { pair_id, correct, points } = req.body;
if (!pair_id) return res.status(400).json({ error: 'pair_id is required' });
const pts = Math.max(0, parseInt(points) || 0);
const isCorrect = correct === true || correct === 'true';
// Detail pro Pair upserten
await query(
`INSERT INTO user_pair_progress (user_id, pair_id, seen_count, correct_count, wrong_count, earned_points)
VALUES ($1, $2, 1, $3, $4, $5)
ON CONFLICT (user_id, pair_id) DO UPDATE SET
seen_count = user_pair_progress.seen_count + 1,
correct_count = user_pair_progress.correct_count + $3,
wrong_count = user_pair_progress.wrong_count + $4,
earned_points = user_pair_progress.earned_points + $5`,
[userId, pair_id, isCorrect ? 1 : 0, isCorrect ? 0 : 1, pts]
);
// EP + Streak auf users_public; Streak: +1 bei neuem Tag, Reset bei Lücke > 1 Tag
const upd = await query(
`UPDATE users_public SET
total_ep = total_ep + $2,
streak_days = CASE
WHEN last_practice_at IS NULL THEN 1
WHEN last_practice_at::date = CURRENT_DATE THEN streak_days
WHEN last_practice_at::date = CURRENT_DATE - INTERVAL '1 day' THEN streak_days + 1
ELSE 1
END,
last_practice_at = NOW()
WHERE user_id = $1
RETURNING total_ep, streak_days`,
[userId, pts]
);
if (!upd.rows.length)
return res.status(409).json({ error: 'Kein Profil vorhanden. Bitte zuerst Profil anlegen.' });
const { total_ep, streak_days } = upd.rows[0];
res.json({ total_ep, streak_days, level: Math.floor(total_ep / 500) });
} catch (err) { next(err); }
});