Initial commit: Premier Gunner tracker + StartOS 0.4.0 s9pk package
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user