Fitness Stack Audit, 2026-06-10¶
Read-only audit of the fitness/wellness pipelines: Hevy, Garmin, Eight Sleep, Notion, Context API. Verified end to end via logs, live API calls, and data/context.db.
TLDR¶
The wellness data layer has been silently dead for 29 days. HA_URL in /home/justinwieb/forge/.env still points to Home Assistant's old IP 192.168.86.70; HA moved to 192.168.86.180 around 2026-05-12. Every pipeline that touches HA from Console has produced zero data since then, while the Hevy pipeline (which never touches HA) is fully healthy. One line in .env is the root-cause fix.
Status by pipeline¶
| Pipeline | Status | Evidence |
|---|---|---|
Hevy -> raw jsonl + Notion Workouts DB (forge_hevy_poll.py, cron 3x/day) |
WORKING | sync ok created=1 on 6/09 and 6/10; raw_hevy_workouts.jsonl mtime Jun 10 03:00 |
| Eight Sleep -> HA (HACS native integration) | WORKING | sensor.justin_s_eight_sleep_side_* updated 2026-06-10 21:08 UTC, 64 entities live |
Garmin -> HA (forge_garmin_poll.py, cron every 30 min) |
BROKEN since 2026-05-12 | Garmin Connect fetch still succeeds; crash on post_sensor to 192.168.86.70 ("No route to host"), 510+ errors in current log alone; last "kind": "ok" 2026-05-12T08:30Z |
HA -> Context API (ha_poller.py, cron every 15 min) |
BROKEN since 2026-05-12, fully silent | facts_wellness max ts = 2026-05-12 for BOTH eight_sleep and garmin; context-poller.log empty since May 17 rotation |
/context?about=wellness (Context API) |
EMPTY | Returns {"eight_sleep": {}, "garmin": {}} for 48h window; service itself healthy (up 5 days) |
Wellness Daily Notion DB (forge_wellness_daily_summary.py, 03:00) |
RUNNING BUT HOLLOW | Creates a Notion page daily (6/07-6/10 confirmed) from an empty context feed, so rows are nulls |
| Workout recovery stamp (06:30 cron) | RUNNING BUT NO-OP | Last run: "reason": "no recovery values", hrv/sleep/readiness all null |
| Weekly fitness retro (Sun 03:30) | PARTIAL | Report file written; notion_ok: false on 6/08. Known cause: n8n subpage webhook never existed; switched to forge_notion_api direct 2026-06-09, next Sunday run should confirm |
| Training recommendation engine | WIRED BUT STARVED | Bot tool in forge_telegram_inbox_brain.py, no cron; reads /context wellness, which is empty, so any output since May 12 is null-driven |
/sync-workouts |
EXISTS as Telegram bot command (coordinator + inbox brains), not a CLI skill | OK by design |
Eight Sleep cron poller (scripts/integrations/eight-sleep/poll.py) |
RETIRED by design 2026-04-28 | Replaced by HA native integration; cron entries commented, archived |
Root cause¶
/home/justinwieb/forge/.env line: HA_URL=http://192.168.86.70:8123. HA is at 192.168.86.180 (fleet.md; ~/.forge-secrets/home-assistant.env already has the correct value). ha_poller.py and forge_garmin_poll.py (via scripts/integrations/_lib/ha.py, whose default is even the correct .180, overridden by the stale env) both load forge .env. Single source of truth violation: HA_URL lives in two files, one stale.
Why it stayed invisible for 29 days¶
ha_poller.ha_state()hasexcept Exception: return Noneper entity, no logging, exits 0. A 64-entity run takes 10+ min of connect timeouts and writes nothing anywhere. Doctrine fail-loud violation.forge_wellness_daily_summary.pyhappily writes an all-null Notion page daily, so the surface "looked alive."- No freshness monitor exists on
facts_wellnessorsensor.garmin_last_poll. The daily canary covers spawn/bots/reaper only. - Garmin entities in HA were wiped on HA restart 2026-06-08 (REST-posted states do not survive restarts);
sensor.garmin_watch_sync_ageetc. nowunavailable.
Gaps between sources (data captured but never surfaced)¶
- Eight Sleep is real-time in HA but reaches nothing downstream right now; even when healthy, only the 15-min poller snapshot lands in context.db.
- Hevy data is rich and current (raw jsonl + Notion) but the training recommendation engine is on-demand-bot-only; the PUSH/MAINTAIN/PULL_BACK/REST signal never surfaces proactively (no cron, no morning notify).
- Weekly retro had its Notion leg dead since creation (n8n webhook never registered); local md files were the only output until the 6/09 fix.
forge_hevy_health.pyexists as a health checker but is not scheduled anywhere.
Fixes (ALL APPLIED same day, commit f2ca3df + follow-up; #7 still pending its Sunday run)¶
Status 2026-06-10 evening: 1-6 done. Backfill recovered +572 facts; 28 Notion rows rebuilt via forge_wellness_gap_repair.py; wellness domain added to forge_monitor; dashboard shipped at fitness.justinsforge.com (CF Access, forge-fitness-app.service, Console:8093).
- One-line root fix: set
HA_URL=http://192.168.86.180:8123inforge/.env. Heals garmin_poll and ha_poller on their next cron ticks. Consider deleting HA_URL from.enventirely so_lib/ha.py's correct default and~/.forge-secrets/home-assistant.envare the only sources (single source of truth). - Backfill the 29-day gap:
scripts/integrations/eight-sleep/backfill.pyandscripts/integrations/garmin/backfill.pyboth exist. Eight Sleep API and Garmin Connect hold history server-side, so the gap is recoverable. Then re-runforge_wellness_daily_summary.pyfor the gap dates to repair hollow Notion rows (it updates-in-place by date property). - Fail loud: ha_poller should log per-entity failures and exit nonzero when 0 facts ingested; garmin_poll already crashes loudly but nothing reads its log. Route both through
/notifywarning on sustained failure per the alert dedup pattern. - Freshness monitor: add a wellness check to the forge monitor or daily canary: alert if
MAX(ts)infacts_wellnessper source is older than 24h, and ifraw_hevy_workouts.jsonlmtime is older than 48h. - Hollow-write guard: wellness daily summary should notify instead of writing a Notion page when all core metrics are null.
- Surface the training recommendation: once data flows, add it to the morning coordinator check-in or a 07:00 cron notify; it is the highest-leverage unsurfaced asset in the stack.
- Verify Sunday retro Notion write on 2026-06-14 (first scheduled run after the 6/09 forge_notion_api switch).
Verification steps used¶
crontab -l,journalctl -u cron(ha_poller fires every 15 min)- Live HA API at .180 (API running, Eight Sleep fresh, Garmin entities unavailable)
- Live Context API with bearer token (wellness empty over 48h)
sqlite3 data/context.dbfacts_wellness MAX(ts) per source- Log archaeology:
logs/integrations/cron.log*,garmin.log*,logs/hevy/cron.log,fitness-retro.log,workout-recovery-stamp.log
[Claude Code, fitness-audit session]
URL: https://mkdocs.justinsforge.com/memory/handoffs/fitness-stack-audit-2026-06-10/