Skip to content

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

  1. ha_poller.ha_state() has except Exception: return None per entity, no logging, exits 0. A 64-entity run takes 10+ min of connect timeouts and writes nothing anywhere. Doctrine fail-loud violation.
  2. forge_wellness_daily_summary.py happily writes an all-null Notion page daily, so the surface "looked alive."
  3. No freshness monitor exists on facts_wellness or sensor.garmin_last_poll. The daily canary covers spawn/bots/reaper only.
  4. Garmin entities in HA were wiped on HA restart 2026-06-08 (REST-posted states do not survive restarts); sensor.garmin_watch_sync_age etc. now unavailable.

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.py exists 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).

  1. One-line root fix: set HA_URL=http://192.168.86.180:8123 in forge/.env. Heals garmin_poll and ha_poller on their next cron ticks. Consider deleting HA_URL from .env entirely so _lib/ha.py's correct default and ~/.forge-secrets/home-assistant.env are the only sources (single source of truth).
  2. Backfill the 29-day gap: scripts/integrations/eight-sleep/backfill.py and scripts/integrations/garmin/backfill.py both exist. Eight Sleep API and Garmin Connect hold history server-side, so the gap is recoverable. Then re-run forge_wellness_daily_summary.py for the gap dates to repair hollow Notion rows (it updates-in-place by date property).
  3. 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 /notify warning on sustained failure per the alert dedup pattern.
  4. Freshness monitor: add a wellness check to the forge monitor or daily canary: alert if MAX(ts) in facts_wellness per source is older than 24h, and if raw_hevy_workouts.jsonl mtime is older than 48h.
  5. Hollow-write guard: wellness daily summary should notify instead of writing a Notion page when all core metrics are null.
  6. 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.
  7. 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.db facts_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/