diff --git a/index.html b/index.html
index a84a99e..deb494f 100644
--- a/index.html
+++ b/index.html
@@ -1725,6 +1725,25 @@
Life Events
return cutoff.toISOString().slice(0, 10);
}
+// SUMMARY-STATS-START (extracted verbatim by tests/js/summary_stats.test.mjs)
+function computeSummaryStats(allDays) {
+ const avgStat = vals => { const c = vals.filter(v => v != null); return c.length ? +(c.reduce((a,b)=>a+b,0)/c.length).toFixed(1) : null; };
+ // Seed-free min/max: a null seed would coerce to 0 in Math.min/Math.max.
+ const extreme = (vals, pick) => { const c = vals.filter(v => v != null); return c.length ? pick(...c) : null; };
+ const bestStepsDay = allDays.reduce((best, d) => (!best || (d.steps||0) > (best.steps||0)) ? d : best, null);
+ return {
+ avg_steps: avgStat(allDays.map(d => d.steps)),
+ avg_sleep_hours: avgStat(allDays.map(d => d.sleep_hours)),
+ avg_resting_hr: avgStat(allDays.map(d => d.resting_hr)),
+ avg_hrv: avgStat(allDays.map(d => d.hrv)),
+ best_sleep: extreme(allDays.map(d => d.sleep_hours), Math.max),
+ worst_sleep: extreme(allDays.map(d => d.sleep_hours), Math.min),
+ best_steps_day: bestStepsDay ? bestStepsDay.date : null,
+ total_days: allDays.length,
+ };
+}
+// SUMMARY-STATS-END
+
async function parseHealthXML(file, onProgress) {
onProgress(0, 'Reading file...');
const text = await file.text();
@@ -1812,20 +1831,8 @@ Life Events
onProgress(90, 'Computing summary...');
await new Promise(r => setTimeout(r, 0));
- // Summary stats
const allDays = Object.values(filtered);
- const avgStat = vals => { const c = vals.filter(v => v != null); return c.length ? +(c.reduce((a,b)=>a+b,0)/c.length).toFixed(1) : null; };
- const bestStepsDay = allDays.reduce((best, d) => (!best || (d.steps||0) > (best.steps||0)) ? d : best, null);
- const summary = {
- avg_steps: avgStat(allDays.map(d => d.steps)),
- avg_sleep_hours: avgStat(allDays.map(d => d.sleep_hours)),
- avg_resting_hr: avgStat(allDays.map(d => d.resting_hr)),
- avg_hrv: avgStat(allDays.map(d => d.hrv)),
- best_sleep: allDays.map(d=>d.sleep_hours).filter(v=>v!=null).reduce((a,b)=>Math.max(a,b), null),
- worst_sleep: allDays.map(d=>d.sleep_hours).filter(v=>v!=null).reduce((a,b)=>Math.min(a,b), null),
- best_steps_day: bestStepsDay ? bestStepsDay.date : null,
- total_days: allDays.length,
- };
+ const summary = computeSummaryStats(allDays);
return {
daily: filtered,
diff --git a/tests/js/summary_stats.test.mjs b/tests/js/summary_stats.test.mjs
new file mode 100644
index 0000000..3f5ad45
--- /dev/null
+++ b/tests/js/summary_stats.test.mjs
@@ -0,0 +1,42 @@
+// Run: node --test tests/js/summary_stats.test.mjs
+// Extracts computeSummaryStats from the single-file frontend so the real code is tested.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+
+const html = readFileSync(fileURLToPath(new URL('../../index.html', import.meta.url)), 'utf8');
+const m = html.match(/\/\/ SUMMARY-STATS-START[^\n]*\n([\s\S]*?)\/\/ SUMMARY-STATS-END/);
+assert.ok(m, 'summary-stats markers not found in index.html');
+const computeSummaryStats = new Function(`${m[1]}; return computeSummaryStats;`)();
+
+const days = sleeps => sleeps.map((s, i) => ({ date: `2025-01-0${i + 1}`, steps: 1000 * (i + 1), sleep_hours: s }));
+
+test('best/worst sleep over positive values', () => {
+ const s = computeSummaryStats(days([7, 6.5, 8]));
+ assert.equal(s.worst_sleep, 6.5);
+ assert.equal(s.best_sleep, 8);
+});
+
+test('nulls are ignored', () => {
+ const s = computeSummaryStats(days([null, 7, null, 5.5]));
+ assert.equal(s.worst_sleep, 5.5);
+ assert.equal(s.best_sleep, 7);
+});
+
+test('no sleep data stays null, not 0', () => {
+ for (const input of [[], days([null, null])]) {
+ const s = computeSummaryStats(input);
+ assert.equal(s.worst_sleep, null);
+ assert.equal(s.best_sleep, null);
+ assert.equal(s.avg_sleep_hours, null);
+ }
+});
+
+test('other stats unchanged', () => {
+ const s = computeSummaryStats(days([7, 6.5, 8]));
+ assert.equal(s.best_steps_day, '2025-01-03');
+ assert.equal(s.total_days, 3);
+ assert.equal(s.avg_steps, 2000);
+ assert.equal(computeSummaryStats([]).best_steps_day, null);
+});