feat: implement neue SRS-Logik zur Berechnung von Intervallen und füge Diagnoseskript für fällige Items hinzu
All checks were successful
Deploy to production / deploy (push) Successful in 2m10s
All checks were successful
Deploy to production / deploy (push) Successful in 2m10s
This commit is contained in:
47
backend/scripts/diag-srs-stats.js
Normal file
47
backend/scripts/diag-srs-stats.js
Normal file
@@ -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();
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user