High-level data flow
┌─────────────────────────┐ ┌──────────────────────────┐
│ Garmin watch (anh) │ daily │ garmin-sync GH Actions │
│ │ ─sync──▶│ workflow │
└─────────────────────────┘ │ pulls API → JSON commit │
└────────────┬─────────────┘
│ git push
▼
┌──────────────────────────┐
│ GitHub repo data lake │
│ marcng-study/garmin-sync │
└────────────┬─────────────┘
│ git pull (every 30 min)
▼
┌─────────────────────────────────────────────────┐
│ GCP VM 34.21.163.141 │
│ ┌────────────────────────────────────────────┐ │
│ │ Python pipeline │ │
│ │ ├─ loader.py → DataFrame │ │
│ │ ├─ stats.py → baseline + anomaly │ │
│ │ ├─ correlation.py→ Tier 1 lag-1 priors │ │
│ │ ├─ weighting.py → Tier 2 literature │ │
│ │ ├─ planner.py → daily plan + smart cap │ │
│ │ ├─ chart.py → matplotlib PNG │ │
│ │ ├─ digest.py → Haiku LLM (weekly) │ │
│ │ ├─ retro.py → descriptive stats │ │
│ │ ├─ manual_log.py → Option C log loop │ │
│ │ └─ delivery.py → Telegram + fallback │ │
│ └────────┬───────────────┬───────────────────┘ │
│ │ │ │
│ ┌────────▼─────┐ ┌──────▼──────────────┐ │
│ │ Haiku 4.5 │ │ Telegram bot API │ │
│ │ (weekly LLM) │ │ @marc_healthbot │ │
│ └──────────────┘ └──────────┬──────────┘ │
│ │ │
│ systemd user timers (linger):│ │
│ morning-ping (04:00 UTC = 11:00 VN) │
│ anomaly-check (01:00 UTC = 08:00 VN) │
│ fire-due (15 min) │
│ weekly-digest (Sat 14:00 UTC = T7 21:00 VN) │
│ weekly-chart (Sat 14:05 UTC = T7 21:05 VN) │
│ log-prompt (Sat 14:30 UTC = T7 21:30 VN) │
│ garmin-pull (30 min) │
│ │ │
│ ┌──────────────────────────▼─────────────────┐ │
│ │ Google Calendar API (OAuth, fire-due reads)│ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────┼──────────────────┘
│
▼
┌────────────────────────────┐
│ User devices │
│ • iPhone Telegram │
│ • iPad Telegram │
│ • Mac Telegram │
│ • Web telegram.org │
└────────────────────────────┘
Goal-weighted action ranking (3 tiers)
Each action carries an expected_sleep_score_delta (positive or negative). Planner picks top-N for today, ranked by |delta|.
| Tier | Source | When used |
|---|---|---|
| Tier 1 | Personal lag-1 correlation from anh’s data (n≥10, | effect |
| Tier 2 | Sleep-science literature priors (caffeine half-life, screen-blue light, late workout) | Cold start (current default) |
| Tier 3 | Haiku LLM gut estimate | Last resort for actions with no data + no literature |
Each priors row carries a citation (e.g. Drake et al. 2013, J Clin Sleep Med). Citations surface in nudge body, e.g. Goal: sleep ≥80. Expected impact: -8 score. (Drake et al. 2013).
Smart cap (no fatigue)
- Normal day (no anomaly): ≤2 nudges/day.
- Anomaly day (sleep score <60 / sleep <5h / RHR z≥1.5 / HRV z≤-1.5): ≤4 nudges, escalated.
- Hard cap 5/day — never exceeded.
- Pre-meeting breath is in addition to the daily cap (it’s reactive, not scheduled).
Morning-ping format (ADHD-friendly)
Multi-line Telegram with sections:
- Title — traffic light + headline (
🟢 Ổn,🟡 Recovery,🔴 Đêm qua tệ) - Sub — natural Vietnamese sentence with the key metric (“Sáng nay anh ngủ được 78 điểm — ổn nhé.”)
- Quick metrics inline —
⚡ sleep 78 · HRV 34 · RHR 55 - Weekly goal progress —
🎯 Tuần này: 2/7 đêm ≥80 điểm - Today’s actions (max 2) — bullet list with expected impact
- Calendar context —
📅 N meeting căng trong 14h tới(if any)
Weekly digest format (Saturday 21:00 VN)
Compressed by Haiku system prompt: ≤2 insights, ≤25 words each, Vietnamese weekday (Thứ 5, Chủ nhật) instead of YYYY-MM-DD because ADHD users find calendar dates hard to recall.
📊 Tuần qua
💤 sleep avg X (Δ vs baseline)
❤️ RHR X · 💗 HRV X · 🏃 load X
💡 Top:
• Thứ X [metric] (z=X) — 1-sentence hypothesis + action
• Thứ X ... (only if second insight)
🎯 Tuần này: 1-sentence physical action.
Manual log loop (Option C)
- Default — 0 prompts.
- Anomaly day — prompt morning after with: cafe sau 14h / rượu / late dinner / phone bed.
- Saturday batch (21:30) — recall the week.
- Streak-aware — 3 consecutive no-replies → pause 7 days.
- Reply parsing — Haiku NLP for natural-language replies (“2 ly cafe sáng” →
caffeine: 2).
After ≥10 manual log replies, correlation.compute_personal_priors() produces Tier 1 priors specific to anh, overriding Tier 2 literature defaults.
Calendar pre-meeting nudge
fire-due (every 15 min) calls calendar_reader.upcoming_stressful_events(window_min=10):
- Reads next 10 minutes of Google Calendar primary calendar via OAuth.
- “Stressful” heuristic: title matches keywords (
review,interview,present,board,exec,1on1,q[1-4] review, etc.) OR duration ≥60 min on a weekday. - If hit → fire
🫁 5min breathTelegram with event name + minutes-until.
Privacy boundaries
| Layer | What stays local | What can leave |
|---|---|---|
| Garmin data lake | Full JSON (raw HR, GPS, sleep stages, activity GPS) | Never sent to cloud LLM |
| LLM input | – | Aggregate metrics: avg sleep score, HRV, RHR, anomaly flags + Vietnamese weekday |
| Calendar | – | Event title + duration (read-only OAuth) |
| Telegram | – | Final nudge text only |
| API keys | ~/.config/health-coach/* chmod 600 | Never logged |