import type Database from 'better-sqlite3'; import type { WeeklyContinuity } from '@shared/types'; import { KST_OFFSET_MS } from '../../shared/util/kstDate.js'; const ONE_DAY_MS = 24 * 60 * 60 * 1000; const WEEK_TARGET = 7; const RECOVERY_GAP_DAYS = 7; function toKstDateKey(d: Date): string { const k = new Date(d.getTime() + KST_OFFSET_MS); return k.toISOString().slice(0, 10); } function kstMondayOf(d: Date): string { const k = new Date(d.getTime() + KST_OFFSET_MS); const dayIdx = (k.getUTCDay() + 6) % 7; k.setUTCDate(k.getUTCDate() - dayIdx); return k.toISOString().slice(0, 10); } function addDaysIso(iso: string, days: number): string { const d = new Date(iso + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() + days); return d.toISOString().slice(0, 10); } export class ContinuityService { constructor( private db: Database.Database, private now: () => Date = () => new Date() ) {} get(): WeeklyContinuity { const rows = this.db .prepare( `SELECT created_at FROM notes WHERE deleted_at IS NULL ORDER BY created_at ASC` ) .all() as Array<{ created_at: string }>; const dates = rows.map((r) => new Date(r.created_at)); if (dates.length === 0) { return { weekStart: kstMondayOf(this.now()), weekCount: 0, weekTarget: WEEK_TARGET, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null }; } const byWeek = new Map(); for (const d of dates) { const wk = kstMondayOf(d); byWeek.set(wk, (byWeek.get(wk) ?? 0) + 1); } const currentWeek = kstMondayOf(this.now()); const weekCount = byWeek.get(currentWeek) ?? 0; let consecutive = 0; let cursor = weekCount >= WEEK_TARGET ? currentWeek : addDaysIso(currentWeek, -7); while ((byWeek.get(cursor) ?? 0) >= WEEK_TARGET) { consecutive += 1; cursor = addDaysIso(cursor, -7); } const last = dates[dates.length - 1]!; const todayKst = toKstDateKey(this.now()); const lastIsToday = toKstDateKey(last) === todayKst; let showRecoveryToast = false; if (lastIsToday && dates.length >= 2) { const prev = dates[dates.length - 2]!; const gapMs = last.getTime() - prev.getTime(); if (gapMs >= RECOVERY_GAP_DAYS * ONE_DAY_MS) showRecoveryToast = true; } return { weekStart: currentWeek, weekCount, weekTarget: WEEK_TARGET, consecutiveCompleteWeeks: consecutive, showRecoveryToast, lastNoteAt: last.toISOString() }; } }