From 48a433cd913cd34f4a49b551f5d1a7bdaad0b9d8 Mon Sep 17 00:00:00 2001 From: Injoon Oh Date: Thu, 6 Aug 2026 17:19:12 +0900 Subject: [PATCH] Give the home page a quiet health section, sparklines and all - Add HomeHealth.svelte between the marquee and photos: today's score ring plus four metric tiles, subscribing to the same healthPublic.page window /health already caches - Add Sparkline.svelte, a lightweight two-path SVG line+wash so the home page doesn't pull in layerchart just to draw four 36px trends - Give PAGE_METRICS a `short` label so "Walking + running distance" fits a narrow column - Ignore locally installed third-party agent skills in .gitignore Co-Authored-By: Claude Opus 5 --- .gitignore | 3 + src/lib/health/Sparkline.svelte | 161 ++++++++++++++++ src/lib/health/metrics.js | 25 ++- src/lib/home/HomeHealth.svelte | 313 ++++++++++++++++++++++++++++++++ src/routes/+page.svelte | 8 + 5 files changed, 502 insertions(+), 8 deletions(-) create mode 100644 src/lib/health/Sparkline.svelte create mode 100644 src/lib/home/HomeHealth.svelte diff --git a/.gitignore b/.gitignore index ed56252c..b9483f7b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ vite.config.ts.timestamp-* .convex .cache .turbo + +# Third-party agent skills, installed locally (jakubkrehel/skills, emilkowalski/skills) +.claude/skills/ diff --git a/src/lib/health/Sparkline.svelte b/src/lib/health/Sparkline.svelte new file mode 100644 index 00000000..ebdb4944 --- /dev/null +++ b/src/lib/health/Sparkline.svelte @@ -0,0 +1,161 @@ + + +{#if geometry} +
+ + + + + +
+{/if} + + diff --git a/src/lib/health/metrics.js b/src/lib/health/metrics.js index 759946bc..aee800b2 100644 --- a/src/lib/health/metrics.js +++ b/src/lib/health/metrics.js @@ -15,12 +15,23 @@ import { PUBLIC_METRICS, PUBLIC_RANGES, shiftDateKey } from '$convex/lib/health. * The key list itself lives in Convex, which enforces it. `goal` is a personal * target, not a medical one — it exists so five metrics in five different units * can be averaged into one number. + * + * `short` is the same metric named for a column rather than a row: the home + * page puts four of these side by side in ~200px each, where "Walking + running + * distance" wraps to three lines and stops being a label. It defaults to + * `label`, so a metric only needs one when the full name doesn't fit. */ const PRESENTATION = { steps: { label: 'Steps', unit: '', decimals: 0, goal: 10000 }, - activeEnergy: { label: 'Active energy', unit: 'kcal', decimals: 0, goal: 500 }, + activeEnergy: { label: 'Active energy', short: 'Energy', unit: 'kcal', decimals: 0, goal: 500 }, exerciseMinutes: { label: 'Exercise', unit: 'min', decimals: 0, goal: 30 }, - distance: { label: 'Walking + running distance', unit: 'km', decimals: 1, goal: 7 } + distance: { + label: 'Walking + running distance', + short: 'Distance', + unit: 'km', + decimals: 1, + goal: 7 + } }; const FALLBACK = { label: '', unit: '', decimals: 0, goal: 0 }; @@ -30,12 +41,10 @@ const FALLBACK = { label: '', unit: '', decimals: 0, goal: 0 }; * the page and the public query can't drift apart — adding a section means * widening what the query is permitted to serve, deliberately. */ -export const PAGE_METRICS = PUBLIC_METRICS.map((key) => ({ - key, - ...FALLBACK, - label: key, - ...PRESENTATION[key] -})); +export const PAGE_METRICS = PUBLIC_METRICS.map((key) => { + const metric = { key, ...FALLBACK, label: key, ...PRESENTATION[key] }; + return { ...metric, short: metric.short ?? metric.label }; +}); /** Range picker steps. Kept coarse so every view hits the same Convex query cache entries. */ export const RANGES = PUBLIC_RANGES; diff --git a/src/lib/home/HomeHealth.svelte b/src/lib/home/HomeHealth.svelte new file mode 100644 index 00000000..d2221757 --- /dev/null +++ b/src/lib/home/HomeHealth.svelte @@ -0,0 +1,313 @@ + + +
+
+
+ +

+ Health +

+
+ + +
+ +
+ + + {ready && score.score !== null ? score.score : ''} + +
+ +
+ {#if ready} +

+ {scoreLabel(score.score)} +

+

+ {caption} +

+ {:else if !health.error} + + + {/if} +
+
+
+
+ +
+ {#if health.error} +
+

Couldn't load Health

+
+ {health.error.message ?? 'Unknown error'} +
+
+ {:else} + + +
+ {#each PAGE_METRICS as metric, i (metric.key)} + {@const series = seriesFor.get(metric.key)} + {@const value = series ? valueAt(series.values, shown) : null} +
+

+ {metric.short} +

+ +

+ {#if series} + + {formatValue(value, metric.decimals)} + + {#if metric.unit && isFilled(value)} + + {metric.unit} + + {/if} + {:else} + + {/if} +

+ + +
+ {#if series} + + {/if} +
+
+ {/each} +
+ + + {/if} +
+
+ + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 990f5413..16d63b76 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,6 +5,7 @@ import { heroNameVisible } from '$lib/heroNav.js'; import { marqueePauseWhenOffscreen, marqueeConstantSpeed } from '$lib/actions/marquee.js'; import TechStack from '$lib/TechStack.svelte'; + import HomeHealth from '$lib/home/HomeHealth.svelte'; import HomeDialsMount from '$lib/home/HomeDialsMount.svelte'; import { homeSettings, homeStyle } from '$lib/home/home-settings.svelte.js'; import { techstack } from '$lib/techstack-data.js'; @@ -341,6 +342,13 @@ + + + +