From bc8d63058a55ee55a9a9b868d1380149aadc3eeb Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 3 Jun 2026 17:13:50 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20neue=20SRS-Logik=20zur=20Be?= =?UTF-8?q?rechnung=20von=20Intervallen=20und=20f=C3=BCge=20Diagnoseskript?= =?UTF-8?q?=20f=C3=BCr=20f=C3=A4llige=20Items=20hinzu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/scripts/diag-srs-stats.js | 47 +++++++++++++++++++++++++++++++ backend/services/vocabService.js | 35 ++++++++++++++--------- 2 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 backend/scripts/diag-srs-stats.js diff --git a/backend/scripts/diag-srs-stats.js b/backend/scripts/diag-srs-stats.js new file mode 100644 index 0000000..e635b98 --- /dev/null +++ b/backend/scripts/diag-srs-stats.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node +import { sequelize } from '../utils/sequelize.js'; + +const run = async () => { + try { + await sequelize.authenticate(); + console.log('✅ DB connection OK'); + + const queries = { + totalDue: `SELECT count(*)::int AS total_due FROM community.vocab_srs_item WHERE next_due_at <= now()`, + byStage: `SELECT stage, count(*)::int AS cnt FROM community.vocab_srs_item WHERE next_due_at <= now() GROUP BY stage ORDER BY stage`, + perDay30: `SELECT date(next_due_at) AS day, count(*)::int AS cnt FROM community.vocab_srs_item WHERE next_due_at <= now() GROUP BY day ORDER BY day DESC LIMIT 30`, + createdToday: `SELECT count(*)::int AS created_today FROM community.vocab_srs_item WHERE date(created_at) = current_date`, + recentNextDue: `SELECT item_key, course_id, stage, next_due_at, created_at FROM community.vocab_srs_item WHERE next_due_at > now() - interval '1 day' ORDER BY next_due_at ASC LIMIT 200` + }; + + const [totalRes] = await sequelize.query(queries.totalDue, { type: sequelize.QueryTypes.SELECT }); + console.log('\nTotal due items: ', totalRes?.total_due ?? 'N/A'); + + const byStage = await sequelize.query(queries.byStage, { type: sequelize.QueryTypes.SELECT }); + console.log('\nDue by stage:'); + console.table(byStage); + + const perDay = await sequelize.query(queries.perDay30, { type: sequelize.QueryTypes.SELECT }); + console.log('\nDue per day (last 30):'); + console.table(perDay); + + const [createdRes] = await sequelize.query(queries.createdToday, { type: sequelize.QueryTypes.SELECT }); + console.log('\nCreated today: ', createdRes?.created_today ?? 'N/A'); + + const recent = await sequelize.query(queries.recentNextDue, { type: sequelize.QueryTypes.SELECT }); + console.log('\nExamples of items with next_due_at in the last 24h:'); + console.table(recent.slice(0, 50)); + + // Summary recommendation + console.log('\nNotes:'); + console.log('- If many items are in stage 0/1 and created_today > 0, check code paths that call _ensureSrsItems.'); + console.log('- If many next_due_at are very recent, items may be created with next_due_at = now() causing large due counts.'); + } catch (err) { + console.error('Error running diagnostics:', err); + process.exitCode = 2; + } finally { + try { await sequelize.close(); } catch (_) {} + } +}; + +run(); diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index acd920b..1d11509 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -127,7 +127,6 @@ export default class VocabService { const previousInterval = Math.max(0, Number(item?.intervalDays) || 0); const normalizedRating = String(rating || '').toLowerCase(); const isCorrect = Boolean(correct) && normalizedRating !== 'again'; - if (!isCorrect) { return { stage: Math.max(0, previousStage - 1), @@ -137,25 +136,35 @@ export default class VocabService { }; } - const intervals = [0, 1, 3, 7, 14, 30, 60, 120, 240]; - let nextStage = Math.min(intervals.length - 1, previousStage + 1); - - if (normalizedRating === 'hard') { - nextStage = Math.max(1, previousStage); - } + // Neue einfache Policy: + // - 'easy' -> 7 Tage + // - 'good'/'normal' -> 4 Tage + // - 'hard' -> 1 Tag + // Außerdem: nextDueAt darf nicht mehr am gleichen Kalendertag liegen. + let intervalDays; if (normalizedRating === 'easy') { - nextStage = Math.min(intervals.length - 1, previousStage + 2); + intervalDays = 7; + } else if (normalizedRating === 'hard') { + intervalDays = 1; + } else { + // default / 'good' / unspecified + intervalDays = 4; } - let intervalDays = intervals[nextStage] ?? Math.max(1, previousInterval * 2); - if (normalizedRating === 'hard') { - intervalDays = Math.max(1, Math.ceil(Math.max(previousInterval, 1) * 1.2)); - } + // Bestimme nextDueAt als Start des Tages (00:00) nach intervalDays + const nextDueAt = new Date(now); + // Setze auf Mitternacht heute + nextDueAt.setHours(0, 0, 0, 0); + // Gehe vorwärts: morgen + (intervalDays - 1) + nextDueAt.setDate(nextDueAt.getDate() + 1 + Math.max(0, intervalDays - 1)); + + // Stage-Logik: einfache Fortschrittsstufe basierend auf intervalDays + let nextStage = Math.min(8, Math.max(0, Math.floor(Math.log2(intervalDays + 1)))); return { stage: nextStage, intervalDays, - nextDueAt: new Date(now.getTime() + intervalDays * 24 * 60 * 60 * 1000), + nextDueAt, lapseDelta: 0 }; }