feat: verbessere initialTotalDue und begrenze tägliche Übungsanzahl auf MAX_DAILY_DUE
All checks were successful
Deploy to production / deploy (push) Successful in 1m59s

This commit is contained in:
Torsten Schulz (local)
2026-06-04 18:52:20 +02:00
parent 64cc360dbd
commit bfec885a1f
2 changed files with 18 additions and 23 deletions

View File

@@ -193,14 +193,16 @@ export default class VocabService {
} }
// Determine safe default nextDueAt for newly created items: // Determine safe default nextDueAt for newly created items:
// - default: next calendar day at 08:00 // - prefer a calendar time (08:00) at least one day after creation
// - ensure at least 24 hours after creation // - ensure at least 48 hours after creation so freshly practiced items
// don't immediately appear as due the next day
const createdAt = new Date(now); const createdAt = new Date(now);
const nextCalendar08 = new Date(createdAt); const nextCalendar08 = new Date(createdAt);
nextCalendar08.setHours(8, 0, 0, 0); nextCalendar08.setHours(8, 0, 0, 0);
nextCalendar08.setDate(nextCalendar08.getDate() + 1); // consider the day after tomorrow as earliest calendar slot
const min24h = new Date(createdAt.getTime() + 24 * 60 * 60 * 1000); nextCalendar08.setDate(nextCalendar08.getDate() + 2);
const safeNextDue = nextCalendar08.getTime() < min24h.getTime() ? min24h : nextCalendar08; const min48h = new Date(createdAt.getTime() + 48 * 60 * 60 * 1000);
const safeNextDue = nextCalendar08.getTime() < min48h.getTime() ? min48h : nextCalendar08;
const created = await VocabSrsItem.create({ const created = await VocabSrsItem.create({
userId, userId,

View File

@@ -161,6 +161,7 @@ const PRACTICE_MIN_EXPOSURES = 3;
const SRS_SESSION_STORAGE_VERSION = 2; const SRS_SESSION_STORAGE_VERSION = 2;
const HARD_REQUIRED_CONSECUTIVE_CORRECT = 5; const HARD_REQUIRED_CONSECUTIVE_CORRECT = 5;
const SRS_AGAIN_REINSERT_OFFSET = 3; const SRS_AGAIN_REINSERT_OFFSET = 3;
const MAX_DAILY_DUE = 50;
export default { export default {
name: 'VocabPracticeDialog', name: 'VocabPracticeDialog',
@@ -514,13 +515,14 @@ export default {
} }
if (!this.srsSession) { if (!this.srsSession) {
const initialTotal = Math.min(MAX_DAILY_DUE, Number.isFinite(Number(this.srsServerTotalDue)) ? Number(this.srsServerTotalDue) : dueIds.length);
this.srsSession = { this.srsSession = {
version: SRS_SESSION_STORAGE_VERSION, version: SRS_SESSION_STORAGE_VERSION,
dateKey: this.getLocalDateKey(), dateKey: this.getLocalDateKey(),
courseId: this.openParams.courseId, courseId: this.openParams.courseId,
// Prefer server-reported total due count when available (shows full-course due count) // For the user's session we cap the number practiced per day to MAX_DAILY_DUE
initialTotalDue: Number.isFinite(Number(this.srsServerTotalDue)) ? Number(this.srsServerTotalDue) : dueIds.length, initialTotalDue: initialTotal,
initialDueIds: dueIds, initialDueIds: dueIds.slice(0, initialTotal),
doneIds: [], doneIds: [],
correctCount: 0, correctCount: 0,
wrongCount: 0, wrongCount: 0,
@@ -536,26 +538,22 @@ export default {
// If the stored session has an invalid initialTotalDue (e.g. 0), repair it from server/pool data. // If the stored session has an invalid initialTotalDue (e.g. 0), repair it from server/pool data.
if (this.srsSession && (!Number.isFinite(Number(this.srsSession.initialTotalDue)) || Number(this.srsSession.initialTotalDue) <= 0)) { if (this.srsSession && (!Number.isFinite(Number(this.srsSession.initialTotalDue)) || Number(this.srsSession.initialTotalDue) <= 0)) {
const repairedTotal = Number.isFinite(Number(this.srsServerTotalDue)) && Number(this.srsServerTotalDue) > 0 ? Number(this.srsServerTotalDue) : dueIds.length; const repairedTotal = Number.isFinite(Number(this.srsServerTotalDue)) && Number(this.srsServerTotalDue) > 0 ? Number(this.srsServerTotalDue) : dueIds.length;
try { console.debug('[VocabPracticeDialog] repair srsSession.initialTotalDue', { before: this.srsSession.initialTotalDue, repairedTotal }); } catch (_) {} const capped = Math.min(MAX_DAILY_DUE, repairedTotal);
this.srsSession.initialTotalDue = repairedTotal; this.srsSession.initialTotalDue = capped;
this.srsSession.initialDueIds = Array.isArray(this.srsSession.initialDueIds) && this.srsSession.initialDueIds.length ? this.srsSession.initialDueIds : dueIds; this.srsSession.initialDueIds = Array.isArray(this.srsSession.initialDueIds) && this.srsSession.initialDueIds.length ? this.srsSession.initialDueIds.slice(0, capped) : dueIds.slice(0, capped);
try { this.saveSrsSession(); } catch (_) {} try { this.saveSrsSession(); } catch (_) {}
} }
const doneSet = new Set(Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : []); const doneSet = new Set(Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : []);
this.srsQueueIds = dueIds.filter((id) => !doneSet.has(id)); this.srsQueueIds = dueIds.filter((id) => !doneSet.has(id));
try {
console.debug('[VocabPracticeDialog] initSrsSessionFromPool', { courseId: this.openParams?.courseId, dueIdsLen: dueIds.length, srsQueueLen: this.srsQueueIds.length, stored: !!this.srsSession });
} catch (_) {}
// Fallback: if SRS mode but queue is empty (e.g. mismatch between stored session and current pool), fall back to pool order // Fallback: if SRS mode but queue is empty (e.g. mismatch between stored session and current pool), fall back to pool order
if (this.srsMode && Array.isArray(this.srsQueueIds) && this.srsQueueIds.length === 0) { if (this.srsMode && Array.isArray(this.srsQueueIds) && this.srsQueueIds.length === 0) {
const fallbackIds = (this.pool || []).map((it) => it.id).filter(Boolean); const fallbackIds = (this.pool || []).map((it) => it.id).filter(Boolean);
if (fallbackIds.length > 0) { if (fallbackIds.length > 0) {
try { console.debug('[VocabPracticeDialog] initSrsSessionFromPool: srsQueueIds empty, falling back to pool ids', { fallbackLen: fallbackIds.length }); } catch (_) {} const limited = fallbackIds.slice(0, MAX_DAILY_DUE);
this.srsQueueIds = fallbackIds; this.srsQueueIds = limited;
// keep initialTotalDue stable if possible // keep initialTotalDue stable if possible
if (!this.srsSession) this.srsSession = { version: SRS_SESSION_STORAGE_VERSION, dateKey: this.getLocalDateKey(), courseId: this.openParams.courseId, initialTotalDue: fallbackIds.length, initialDueIds: fallbackIds, doneIds: [], correctCount: 0, wrongCount: 0 }; if (!this.srsSession) this.srsSession = { version: SRS_SESSION_STORAGE_VERSION, dateKey: this.getLocalDateKey(), courseId: this.openParams.courseId, initialTotalDue: limited.length, initialDueIds: limited, doneIds: [], correctCount: 0, wrongCount: 0 };
} }
} }
this.saveSrsSession(); this.saveSrsSession();
@@ -595,10 +593,6 @@ export default {
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
}); });
this.loadHardVocabMap(); this.loadHardVocabMap();
// Expose instance for temporary debugging in browser console
try {
window.__vocabPracticeDialog = this;
} catch (_) {}
this.reloadPool(); this.reloadPool();
}, },
close() { close() {
@@ -747,7 +741,6 @@ export default {
}); });
const result = mapped.filter(Boolean); const result = mapped.filter(Boolean);
try { console.debug('[VocabPracticeDialog] normalizePool', { inputLen, outLen: result.length, sample: result.slice(0,5) }); } catch (_) {}
return result; return result;
} catch (e) { } catch (e) {
console.warn('[VocabPracticeDialog] normalizePool failed', e); console.warn('[VocabPracticeDialog] normalizePool failed', e);