Files
premier-gunner/src/routes/stats.js
T

94 lines
3.4 KiB
JavaScript

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,
};
});
}