diff --git a/backend/scripts/fix-srs-immediate-due.js b/backend/scripts/fix-srs-immediate-due.js new file mode 100644 index 0000000..f735f65 --- /dev/null +++ b/backend/scripts/fix-srs-immediate-due.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +import { sequelize } from '../utils/sequelize.js'; + +/** + * Fix SRS items that were created with next_due_at equal-or-immediately-before created_at + * - Detect items created recently whose next_due_at is within a small window of created_at + * - Move next_due_at to the next calendar day at 08:00 local (safe default) + * + * Run on the server after a backup. Prints counts and example rows before/after. + */ + +const DRY_RUN = process.env.DRY_RUN !== 'false'; + +const run = async () => { + try { + await sequelize.authenticate(); + console.log('✅ DB connection OK'); + + // Conservative selector: items created in the last 3 days where next_due_at is within 2 minutes of created_at + const selector = ` + SELECT id, item_key, course_id, stage, next_due_at, created_at + FROM community.vocab_srs_item + WHERE created_at >= now() - interval '3 days' + AND abs(extract(epoch from (next_due_at - created_at))) < 120 + ORDER BY created_at ASC + LIMIT 500 + `; + + const recent = await sequelize.query(selector, { type: sequelize.QueryTypes.SELECT }); + console.log('\nFound items matching heuristic: ', recent.length); + console.table(recent.slice(0, 20)); + + if (recent.length === 0) { + console.log('\nNo candidate rows found — nothing to do.'); + return; + } + + // Compute update target: next calendar day at 08:00 based on created_at + // Use an UPDATE with an expression so each row gets next_due_at = date_trunc('day', created_at + '1 day') + '08:00' + const updateQuery = ` + UPDATE community.vocab_srs_item + SET next_due_at = date_trunc('day', created_at + interval '1 day') + interval '8 hours' + WHERE created_at >= now() - interval '3 days' + AND abs(extract(epoch from (next_due_at - created_at))) < 120 + RETURNING id, item_key, course_id, stage, next_due_at, created_at; + `; + + if (DRY_RUN) { + console.log('\nDRY RUN: to apply changes set ENV var DRY_RUN=false'); + console.log('Preview of UPDATE SQL:\n', updateQuery); + } else { + console.log('\nApplying update...'); + const updated = await sequelize.query(updateQuery, { type: sequelize.QueryTypes.SELECT }); + console.log('\nUpdated rows: ', updated.length); + console.table(updated.slice(0, 20)); + } + + console.log('\nRecommendation: rerun `backend/scripts/diag-srs-stats.js` to verify totals afterwards.'); + } catch (err) { + console.error('Error running fix script:', err); + process.exitCode = 2; + } finally { + try { await sequelize.close(); } catch (_) {} + } +}; + +run(); diff --git a/backend/scripts/fix-srs-queries.sql b/backend/scripts/fix-srs-queries.sql new file mode 100644 index 0000000..d1ce4bc --- /dev/null +++ b/backend/scripts/fix-srs-queries.sql @@ -0,0 +1,23 @@ +-- Fix items that were created with next_due_at ~= created_at (within 2 minutes) +-- Run this after a backup. Adjust the WHERE clause if you want a different window. + +BEGIN; + +-- Preview rows to be changed +SELECT id, item_key, course_id, stage, next_due_at, created_at +FROM community.vocab_srs_item +WHERE created_at >= now() - interval '3 days' + AND abs(EXTRACT(EPOCH FROM (next_due_at - created_at))) < 120 +ORDER BY created_at ASC +LIMIT 200; + +-- Conservative update: set next_due_at to next calendar day at 08:00 +UPDATE community.vocab_srs_item +SET next_due_at = date_trunc('day', created_at + interval '1 day') + interval '8 hours' +WHERE created_at >= now() - interval '3 days' + AND abs(EXTRACT(EPOCH FROM (next_due_at - created_at))) < 120 +RETURNING id, item_key, course_id, stage, next_due_at, created_at; + +COMMIT; + +-- After running, re-run backend/scripts/diag-srs-stats.js to validate counts.