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):

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       │
└──────────────────────────────────┘

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:

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:

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:

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):

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:

Dependencies

Mobile / PWA

This page is designed to be added to iOS home screen:

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)

Open Questions

  1. 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.

  2. 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.

  3. Should the intervals.icu API key be a build-time secret or runtime env var? Recommendation: runtime env var (already used by existing scripts).