import { db } from '../db.js'; function todayISO() { return new Date().toISOString().slice(0, 10); } function goalProgress(goal) { let current = 0; if (goal.kind === 'session_count') { current = goal.scope === 'overall' ? db.prepare('SELECT COUNT(*) AS n FROM entries').get().n : db.prepare('SELECT COUNT(*) AS n FROM entries WHERE category_id = ?').get(goal.category_id).n; } else if (goal.kind === 'metric_best' && goal.metric_id) { current = db.prepare('SELECT COALESCE(MAX(value), 0) AS v FROM entry_values WHERE metric_id = ?') .get(goal.metric_id).v; } else if (goal.kind === 'metric_total' && goal.metric_id) { current = db.prepare('SELECT COALESCE(SUM(value), 0) AS v FROM entry_values WHERE metric_id = ?') .get(goal.metric_id).v; } const pct = goal.target > 0 ? Math.min(100, Math.round((current / goal.target) * 100)) : 0; return { ...goal, current, pct }; } function streaks(days) { // days: ISO strings sorted ascending, distinct if (!days.length) return { current: 0, longest: 0 }; const set = new Set(days); let longest = 0; for (const d of days) { const prev = new Date(d); prev.setDate(prev.getDate() - 1); if (!set.has(prev.toISOString().slice(0, 10))) { // start of a run let len = 0; const cur = new Date(d); while (set.has(cur.toISOString().slice(0, 10))) { len++; cur.setDate(cur.getDate() + 1); } if (len > longest) longest = len; } } // current streak ending today or yesterday let current = 0; const cur = new Date(todayISO()); if (!set.has(cur.toISOString().slice(0, 10))) cur.setDate(cur.getDate() - 1); while (set.has(cur.toISOString().slice(0, 10))) { current++; cur.setDate(cur.getDate() - 1); } return { current, longest }; } export default async function statsRoutes(app) { app.get('/api/stats', async () => { const totalSessions = db.prepare('SELECT COUNT(*) AS n FROM entries').get().n; const dayRows = db.prepare('SELECT DISTINCT day FROM entries ORDER BY day').all().map((r) => r.day); const totalDays = dayRows.length; // Heatmap: entries per day. const heatmap = db.prepare( 'SELECT day, COUNT(*) AS count FROM entries GROUP BY day ORDER BY day' ).all(); // Radar: sessions per category (all-time + last 30 days). const since = new Date(); since.setDate(since.getDate() - 30); const since30 = since.toISOString().slice(0, 10); const radar = db.prepare( `SELECT c.id AS category_id, c.name, c.emoji, c.color, COUNT(e.id) AS sessions, SUM(CASE WHEN e.day >= ? THEN 1 ELSE 0 END) AS sessions30 FROM categories c LEFT JOIN entries e ON e.category_id = c.id WHERE c.archived = 0 GROUP BY c.id ORDER BY c.sort_order, c.id` ).all(since30); // Per-metric time series for line charts. const seriesRows = db.prepare( `SELECT ev.metric_id, e.day, ev.value, e.id AS entry_id FROM entry_values ev JOIN entries e ON e.id = ev.entry_id ORDER BY e.day, e.id` ).all(); const series = {}; for (const r of seriesRows) { (series[r.metric_id] ||= []).push({ day: r.day, value: r.value, entry_id: r.entry_id }); } const goals = db.prepare('SELECT * FROM goals ORDER BY is_main DESC, sort_order, id').all() .map(goalProgress); return { totalSessions, totalDays, ...streaks(dayRows), today: todayISO(), heatmap, radar, series, goals, }; }); }