feat: Feed-Pagination – erledigte und vom Client gelieferte Pairs ausschließen

GET /auth/feed schließt jetzt Pairs aus user_pair_progress (cross-session)
sowie per ?exclude=<uuids> übergebene, bereits geladene Pairs (In-Session)
aus. Leere Antwort signalisiert dem Client: keine weiteren Karten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 11:53:05 +02:00
parent e44d896f9e
commit 508d6993ee

View File

@@ -41,10 +41,18 @@ router.get('/', requireJwt, async (req, res, next) => {
try { try {
const lang = ['de', 'en', 'sv'].includes(req.query.lang) ? req.query.lang : 'de'; const lang = ['de', 'en', 'sv'].includes(req.query.lang) ? req.query.lang : 'de';
const limit = Math.min(parseInt(req.query.limit) || 20, 100); const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const userId = req.user.userId;
// Vom Client schon geladene Pairs (In-Session-Dedupe) nur gültige UUIDs übernehmen.
const exclude = String(req.query.exclude || '')
.split(',')
.map(s => s.trim())
.filter(s => /^[0-9a-f-]{36}$/i.test(s));
// 1. Random pairs — only fully ready content: // 1. Random pairs — only fully ready content:
// pair published + linked question/statements published + a published picture exists. // pair published + linked question/statements published + a published picture exists.
// (Audio coverage is additionally enforced in Phase 2.) // (Audio coverage is additionally enforced in Phase 2.)
// Pagination: bereits abgeschlossene (user_pair_progress) und vom Client
// geladene Pairs werden ausgeschlossen; leere Antwort = keine weiteren Karten.
const pairsRes = await query( const pairsRes = await query(
`SELECT p.id, p.answer_type, p.status, p.difficulty_level, `SELECT p.id, p.answer_type, p.status, p.difficulty_level,
p.question_id, p.positive_statement_id, p.negative_statement_id p.question_id, p.positive_statement_id, p.negative_statement_id
@@ -61,9 +69,13 @@ router.get('/', requireJwt, async (req, res, next) => {
JOIN object_pictures pic ON pic.object_id = op.object_id JOIN object_pictures pic ON pic.object_id = op.object_id
JOIN pictures pp ON pp.id = pic.picture_id JOIN pictures pp ON pp.id = pic.picture_id
WHERE op.pair_id = p.id AND pp.status = 'published') WHERE op.pair_id = p.id AND pp.status = 'published')
AND NOT EXISTS (
SELECT 1 FROM user_pair_progress upp
WHERE upp.pair_id = p.id AND upp.user_id = $2)
AND p.id <> ALL($3::uuid[])
ORDER BY random() ORDER BY random()
LIMIT $1`, LIMIT $1`,
[limit] [limit, userId, exclude]
); );
if (!pairsRes.rows.length) return res.json([]); if (!pairsRes.rows.length) return res.json([]);
const pairs = pairsRes.rows; const pairs = pairsRes.rows;