Health Dashboard — Design Doc
Overview
A new page at /health in Omni/Web showing Ben’s metabolic health gamification
dashboard. Tracks daily scores, weekly streaks, CGM trends, and food/exercise
correlations. Designed for daily glanceability — this is the primary motivational
surface for the insulin sensitivity improvement protocol.
Route
/health — new top-level route in Omni.Web.Core, delegating to Omni.Health.Web.
Add "health" to the nav bar alongside fund, agents, tasks, etc.
Architecture
Interactivity Pattern
Pattern 1: Server-rendered + Chart.js (per CONVENTIONS.md).
This is a read-only dashboard. Server computes everything from data files. No mutations from the web UI — meal logging happens via Telegram, CGM via CSV export, exercise via intervals.icu API.
Data Flow
Telegram meals ──→ meal-log.md ──→
CGM CSV export ──→ cgm.csv ──→ Omni.Health.Analyze ──→ /health page
intervals.icu ──→ API pull ──→
All data lives in /var/health/ (or configurable via HEALTH_DATA_DIR env var):
/var/health/meal-log.md— markdown meal log (current format)/var/health/cgm.csv— CGM export (Stelo format: Date/Time, Blood Glucose)- Exercise data pulled live from intervals.icu API on page load (cached 1h)
Module Structure
Omni/Health/
Web.hs -- WAI app, route dispatch, page rendering (Lucid + Chart.js)
Analyze.hs -- Pure analysis: parse data, compute scores, correlations
Score.hs -- Daily/weekly scoring logic
Style.hs -- Clay CSS for health pages
Page Layout
Single scrollable page with these sections top-to-bottom:
1. Hero: Today’s Score + Streak
Big centered display:
┌──────────────────────────────────┐
│ TODAY: 8/10 │
│ 🔥 Streak: 4 weeks │
│ Weekly mean: 112 → 109 │
└──────────────────────────────────┘
- Today’s score: large number (0-10), color-coded (green ≥7, yellow 4-6, red ≤3)
- Streak counter: consecutive weeks where weekly mean BG dropped ≥1 point
- Weekly mean with arrow showing direction vs prior week
If today’s score isn’t calculable yet (no CGM data processed), show yesterday’s score dimmed with “today pending” indicator.
2. Score Breakdown
Shows which scoring criteria were hit today:
✅ +3 All meals protein-dominant
✅ +2 No spike above 140
✅ +1 No spike above 160
✅ +2 Fasted exercise (Z2 52min)
⬜ +0 First meal >1h after exercise
✅ +1 No reactive hypo
──────
9/10
Checkmarks for earned points, empty boxes for missed. Each line is a single scoring rule with its point value.
3. Weekly Trend Chart
Line chart (Chart.js) showing:
- Daily mean BG (primary line, accent color)
- Daily score (secondary y-axis, bar chart overlay)
- 7-day rolling mean BG (smoothed line)
- Week boundaries marked
X-axis: dates. Y-axis left: BG (80-160). Y-axis right: score (0-10). Hover shows exact values.
4. Streak History
Horizontal bar or calendar-heatmap showing weeks:
Week 8: ████████ mean 119
Week 9: ████████ mean 119 ← streak broken (no drop)
Week 10: ███████ mean 117 🔥
Week 11: ██████ mean 115 🔥🔥
Week 12: ████ mean 109 🔥🔥🔥
Color intensity = weekly mean (lower = more vivid green). Fire emojis or visual streak indicator for consecutive-drop weeks.
5. Food Rankings
Table showing food categories ranked by average spike:
Category Avg Spike N Verdict
─────────────────────────────────────────
Rice +84 2 🔴 AVOID
Fruit/sweet +31 10 🟡 CAUTION
Pasta +28 5 🟡 CAUTION
Bread/toast +21 14 🟡 BUFFER
Protein-heavy +18 7 🟢 GOOD
Yogurt/protein +15 7 🟢 GOOD
Mixed/balanced +13 11 🟢 GOOD
Eggs +12 2 🟢 GOOD
Re-computed live from the full dataset on each page load. As more meals are logged, the sample sizes grow and rankings may shift.
6. Correlations Panel
Key correlations, updated live as data grows:
Predictor r Status
────────────────────────────────────────
Time trend -0.56 ✅ Strong
Exercise duration -0.57 ✅ Strong
Fasting hours -0.46 🟡 Moderate
Carb meals/day +0.38 🟡 Moderate
GLUT4 window meal -0.20 ⬜ Weak
Each correlation recalculated from the full dataset. Include n and confidence interval. Flag when a correlation changes significance (e.g., goes from weak to moderate as more data arrives).
7. Key Metrics Cards
Four KPI cards in a grid (reuse Omni.Fund.Web.Components.renderKpiGrid):
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Fasting BG │ │ TIR (70-140)│ │ Reactive │ │ TTGNG │
│ 114 │ │ 97% │ │ Hypos: 2 │ │ ~12h │
│ ↓9 vs wk1 │ │ ↑5% vs wk1│ │ ↓14 vs wk1 │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
Scoring Rules
Daily Score (0-10)
+3 All meals protein-dominant (>50% protein/fat estimated from description)
+2 No CGM reading above 140
+1 No CGM reading above 160 (cumulative with above; max +3 for no spikes)
+2 Fasted exercise ≥30min (any activity on intervals.icu before first meal)
+1 First meal within 1h of exercise end
+1 No reactive hypoglycemia (<90 within 4h of a spike >125)
Penalties:
-2 Any meal that is isolated carbs (carbs without protein/fat context)
-1 Per spike above 160
-3 Any spike above 180
Score floor is 0 (no negatives displayed, but calculate raw for analytics).
Meal Classification
Meals are classified from the text description in meal-log.md:
- Protein-dominant: contains meat, fish, eggs, greek yogurt, protein powder, cheese as primary component, with carbs only as minor side
- Balanced: protein + carb in roughly equal portions (e.g., tacos, stir fry)
- Carb-dominant: pasta, rice, bread, pastry, fruit as primary component without significant protein buffer
- Isolated carb: carbs with no protein/fat (plain toast, fruit alone, candy)
This classification can be heuristic (keyword matching) initially. Ava (via Telegram) can override/correct classifications when logging meals.
Weekly Streak
Streak = consecutive weeks where weekly_mean_BG[w] < weekly_mean_BG[w-1].
Display as a counter. Streak resets to 0 when weekly mean increases or stays flat.
Milestone badges at weekly mean thresholds: 120, 115, 110, 105, 100, 95. Display earned badges on the hero section.
Fasting Day Handling
On days with no meals (full fasting days), scoring adapts:
- Meal-related criteria (+3 protein-dominant) → automatically satisfied (no meals = no bad meals)
- Spike criteria → evaluated normally from CGM
- Exercise criteria → evaluated normally
- Max possible on a rest + fast day: 3 + 3 + 0 + 1 = 7/10
Data Ingestion
Meal Log
Parse meal-log.md format:
# March 16
12:55 White bread toast, scrambled eggs, salmon, macadamia nuts
13:40 Greek yogurt with frozen blueberries
17:10 Cheese, meat sticks, olipop
18:25 Spaghetti with shrimp and cream sauce, roasted broccoli and mushrooms
Parser extracts: date, time (HH:MM), description text.
CGM Data
Parse Stelo CSV format:
Date/Time,Blood Glucose (mg/dL),Transmitter ID
2026-02-19 22:21:59,110,4LXXXXXXXXXX
Columns: timestamp, glucose value. 5-minute intervals.
Exercise Data
Pull from intervals.icu API (see skills/intervals-icu.md):
- Activities endpoint: date, type, duration, calories
- Filter to relevant types (Ride, VirtualRide, WeightTraining, Walk, Run)
Cache responses for 1 hour to avoid hammering the API.
Technical Notes
Adding the Route
In Omni.Web.Core:
import qualified Omni.Health.Web as HealthWeb
-- in app function, add case:
"health" : _ ->
HealthWeb.app healthDataDir (stripPrefixRequest "health" req) respond
Add healthDataDir parameter (from env var HEALTH_DATA_DIR, default /var/health/).
Style
Use Omni.Web.Style shared shell with a new healthSubnav (or no subnav
initially — single page). Dark theme per existing design system.
Charts use Omni.Web.Palette colors:
- BG line:
P.cyan(#6fb3c0) - Score bars:
P.green(#51b04f) - Spike markers:
P.red(#ff6f6f) - Rolling mean:
P.magentaCooler(#d0b0ff) - Week boundaries:
P.fgFaint
Dependencies
lucid(HTML generation)clay(CSS)aeson(JSON for Chart.js data)time(date parsing)http-client/http-client-tls(intervals.icu API)- Chart.js via CDN (already used in Fund pages)
Mobile / PWA
This page is designed to be added to iOS home screen:
- Viewport meta tag for mobile
- Touch-friendly tap targets (≥44px)
- The hero section should be visible without scrolling on iPhone
- Consider
<meta name="apple-mobile-web-app-capable" content="yes">for standalone PWA mode
Data Freshness
Page shows a “Last updated” timestamp based on the most recent CGM reading. If CGM data is >24h stale, show a warning banner: “CGM data is X days old — export from Stelo app.”
Future Extensions (Not MVP)
- Ava auto-scoring: Ava calculates and sends daily score via Telegram each evening (scheduled task)
- Meal photo logging: Send a photo of food to Ava, who describes and logs it automatically
- Apple Health integration: Direct CGM pull without CSV export
- Experiment tracker: Log planned food experiments (rice + protein test) and auto-analyze results
- Weight overlay: Add weight trend line from intervals.icu wellness data
- HRV/RHR correlation: Check if resting HR and HRV correlate with CGM improvements
- Multi-page: Split into /health (dashboard), /health/meals (food diary with CGM overlay), /health/experiments (A/B food tests)
Open Questions
-
Should meal classification use LLM (call Ava’s model to classify each meal description) or keyword heuristics? LLM is more accurate but adds latency and API cost on every page load. Recommendation: pre-classify at log time (when Ava receives the Telegram message) and store classification in the meal log.
-
How to handle CGM gaps? The Stelo sensor expires every 15 days and there’s a gap between sensors. Score criteria that depend on CGM should be marked “incomplete” rather than penalized.
-
Should the intervals.icu API key be a build-time secret or runtime env var? Recommendation: runtime env var (already used by existing scripts).