Skip to content

claude-hub (hub.lan:8090) — Garmin dashboard + coach

Source ~/claude-hub/app.py (stdlib HTTP, sqlite hub.db, 91 days of Garmin via the localhost MCP) and static/index.html (hand-rolled SVG, dark only, tabs Health/Coach/Build). Service claude-hub.service (user unit). Map card: map/objects/containers/garmin-mcp.md.

Current: Race day + Training direction LIVE — 2026-10-22

Owner’s “Continue with recommendations” authorized the prioritized live rollout, easier confirmed-result onboarding and actual race setup. Live Race day and Training direction are active; no remaining phone-approval gate from the earlier preview. Runtime commit 3d1606f; source branch sandbox/race-lab-20261022 in ~/claude-hub-sandbox (documentation checkpoint 7f24444). Sandbox :8091 remains available with automatic Garmin polling disabled.

Built: continuous workout / threshold-HR / critical-speed comparisons; confirmed race calibration; source dates and pre-race scoring; explicit weather/course allowances and exact-location forecasts within 15 days; run splits and four event execution plans; cached adaptive Coach week, recovery context, consistency, sport gaps and proof of improvement. Native FIT sport sessions replace heuristic triathlon lap labels. Models remain provisional: personal heat effects, bike physics and Olympic endurance are not validated; no hard targets from stale FTP or unverified max HR. Installed wrapper still lacks native event-specific Garmin predictions.

Latest additions: saved event name/date/place/start and official run/bike/swim distances; exact meters retained when rounded imperial inputs are unedited; optional local run-course GPX with elevation-aware split distribution (selected finish target unchanged). Custom strategy distances are separate from standard-distance predictions/scoring. GPX mismatches, missing elevation and steep/noisy profiles fall back with explicit notes. Result confirmation is prominent, offers distance-match shortcuts, and requires official time plus explicit maximal-effort confirmation. Zero real results were auto-confirmed; calibration awaits owner-confirmed evidence. Garmin course read found two saved Sandling bike courses (512962555 International, 512961452 Sprint), no run course for the unnamed Nov 14 10K; actual run GPX is still an input, not a fabricated route.

Live updates: existing :8090 sync loop runs every ~15 minutes, captures missing details, saves forecasts and refreshes Coach at most every four hours/new local day. Open Race day pages refresh cached data every five minutes and on foreground return, retaining form drafts; latest sync/error state is visible. Automatic sync completed after rollout (23:49 ET, error None). Preview stays frozen.

Verified: 58 deterministic tests plus original hub checks; 375/390/768/1280 browser checks; isolated DB end-to-end setup, GPX, result confirmation/removal and custom triathlon tests; live four race views + Train at 375 px, no overflow/JS errors; SQLite integrity. Migration fingerprints preserved 74 activities, 8,131 Apple metrics, 442 Apple workouts, 406 daily rows, 356 legacy prediction rows and all original tables. Existing live feature rows win. No full DB replacement, new timer, paid API, external messages or workout mutations. Independent subscription reviewers actually ran GPT-6 Luna and GPT-6.1 Sol with enforced read-only tools.

Release/rollback: ~/claude-hub-sandbox/deploy_race_lab.py validates source/previous release ancestry and file hashes, copies only eight owned code assets, merges feature tables with INSERT OR IGNORE, and has code-only rollback. Latest backup /home/angel/claude-hub-releases/20261023T034805Z-3d1606f4 restores the preceding feature release; prior full-release backup /home/angel/claude-hub-releases/20261023T034219Z-417d0209 restores the original hub after that. Live .race-lab-release.json is the release pointer. Simulated failed rollout restored code while preserving a newly added workout. Full procedure in repo RACE-LAB.md. Never replace the current live DB with the frozen sandbox or an old backup when rolling back code.

Plan 2026-10-17 (owner: live dashboard, all three sports, tips, video-ad look, touch sliders)

Why "Maintaining": 23 runs in 3 months, no swim/bike logged since late Sept, VO2max flat at 40, ACWR 1.2 (optimal). Steady load = Garmin says maintaining. Goal event is probably a triathlon (RACE PRACTICE "Sandling Beach S…" 2026-09-13) — confirm with owner. 1. Sandbox copy on its own port, copy of hub.db, sync loop OFF (same Garmin account; do not double-poll). 2. Per-sport view: run / bike (cycling, indoor_cycling, virtual_ride, road_biking) / swim (lap + open water). Split multi_sport into legs via get_activity. Raise sync_activities(limit=60). 3. Rule-based tips (no new LLM job; budget is ahead of pace) reading Garmin adaptive plan + constraints memory (7 h sleep floor, swim/bike after 17:00, earned mornings). 4. "What-if" sliders: PMC impulse-response load (fit our load to Garmin acute/chronic, show error) + Riegel/VDOT for race times, cross-checked against Garmin race predictions. Label as estimate; read-only; never writes workouts. 5. Restyle: HUD look from the Lummox "Mechanical Foundry" video (cyan/orange on near-black, big mono numerals, segmented bars, per-unit status grid).

Motion reference (2026-10-17, Asteri_eth 20-repo "motion stack" post, 30 s 4:5 video)

Video = HUD dashboard (list of 20 repos on both sides, mascot centre, connector line to the active item, command-log, timeline/easing/camera/audio gauges, 01/20 counter, segmented progress bar). Motion = one active item at a time, a line that draws to it, counters that tick, bars that fill, a final "20/20 stack ready" lock-in. Useful free/MIT pieces for the hub: Motion (motiondivision/motion, MIT) for slider/layout gestures; Anime.js or GSAP (SVG path draw, count-up); Three.js only for the optional 3D scene; Remotion only if we render clips for social (FB/IG paused). Hub needs none of the video tooling; vendor pinned, no CDN.

Repo shortlist saved to shared memory: motion-design-repos.md.

HANDOFF (start a new chat: "read the handoff in ~/Notes/projects/claude-hub.md and continue")

Status 2026-10-17: steps 1-9 DONE in the sandbox (~/claude-hub-sandbox, git, :8091, service claude-hub-sandbox, sync/coach off; live hub :8090 untouched). Owner reviews Today/Train/trend sheets on phone at http://192.168.5.15:8091/ (deep links: #train-swim, #race-5K, #tri-sprint, #tri-olympic). Tools: hub-shot [url] [png] [w] [h] (REDUCED=1 freezes animation), python3 test_sports.py in the sandbox dir. Built: sports.py (per-sport rows, tri legs via get_activity_splits, transition laps dropped, km-vs-m ride fix, race_trend, sprint/olympic estimates); app.py tables race_history + legs, daily prediction snapshot, est backfill; UI = HUD theme, Today/Train/Coach/Build tabs, tap-for-trend sheet (scrub, 30/60/90 d, estimated points dashed amber) on race cards, tri cards, Today metric cards, weekly distance/pace. Caveats: race history before 2026-10-17 is VO2max-scaled estimate (Garmin keeps none) and VO2max steps 42->35 on 07-25 (Garmin recalibration) so the early curve has a cliff; live hub does not record snapshots until it is replaced (or copy the sandbox race_history rows). Tri estimate was +7.6% vs the 09-13 practice (2:26:34); swim leg 526 m in 31 min looks slow/odd. Next: owner feedback on the look, then 10 tips, 11-13 sliders + motion, 14 verify, 15 replace live hub (update map card objects/containers/garmin-mcp.md).

Build steps (agreed layout 2026-10-17)

P0: 1 sandbox copy (own port, db copy, sync off) · 2 headless Chromium for 375 px shots · 3 owner's event+date. P1 data: 4 sport grouping (run / bike x4 types / swim x2) · 5 split multi_sport legs · 6 raise activity limit. P2 screen: 7 HUD look, tabs Today/Train/Coach · 8 Today (headline, 4 big numbers, one action line) · 9 Train (Run/Bike/Swim, weekly load, pace trend, 14-day status grid, count-up) — STOP here for owner to view on phone · 10 rule-based tips (7 h floor, swim/bike after 17:00, earned mornings, match training-sync). P3 sliders: 11 what-if card (run km, days, easy/hard, long run, added swim/bike) · 12 projection (PMC + Riegel, fitted to Garmin, error shown, "estimate", read-only) · 13 motion (Motion/Anime.js, pinned MIT). P4: 14 375 px screenshots + check vs Garmin race predictions · 15 owner approves on phone, then replace live hub, update map card + this note. Decisions made: personal hub exempt from brand palette/ee-theme, keeps phone layout + Home link.

Open

  • Homepage widgets still missing (dashboard ~/homepage, tiles in config/services.yaml, keys in ~/homepage/.env as HOMEPAGE_VAR_*_KEY, 0600): qBittorrent (needs the web UI password; only a hash is on disk), Immich (old key 401s; owner makes a new one in Account Settings → API Keys), Jellyfin (owner makes one in Dashboard → API Keys). Once given, add the key to .env and a widget: block to the tile, then docker compose up -d --force-recreate in ~/homepage.
  • Tautulli: its API answers but get_activity returns empty data, so Tautulli probably cannot reach Plex. Fix its Plex connection (Settings → Plex Media Server), then re-add the Homepage Tautulli widget (key is already in .env as HOMEPAGE_VAR_TAUTULLI_KEY).
  • Event and date for slider targets: owner said skip for now (2026-10-17).
  • Rule-1 check: intervals.icu (free, Garmin sync, per-sport fitness/fatigue/form, API key) vs hub. Decide keep-hub + borrow its model, or feed hub from its API.
  • Owner to review Train + trend sheets on the phone (:8091) before tips/sliders.

Log

  • 2026-10-17 plan written after reading app.py, index.html and the live /api/analysis.
  • 2026-10-17 decision: personal hub exempt from brand palette/ee-theme (keep 375 px layout + Home link). Headless Chrome via ~/.local/bin/hub-shot.
  • 2026-10-17 steps 1-9 built in sandbox; race predictions and sprint/olympic estimates tap open a 90-day trend; est points labelled. Sandbox commit 3dcab80.
  • 2026-10-17 owner feedback on phone: wants miles (UI converts km->mi, s/km->min/mi, km/h->mph; swim yards, /100yd; backend stays metric), measured vs estimated lines must look different (now solid thick cyan vs thin dashed amber), fix padding (grid gap, bottom room for Safari bar).
  • 2026-10-17 owner: removed Coach and Build from the UI (backend /api/coach, /api/build stay until the live hub is replaced; sandbox 403s them). Added a second, VO2-independent predictor: fastest standalone run >=3 km in the last 42 days scaled with Riegel 1.06 (walk pace >10:00/km skipped) -> purple line on race sheets + 'from runs' on cards. 5K 32:18 vs Garmin 31:41; marathon 5:09 vs 5:48 (training pace is conservative). Sandbox commit a960abb. Possible third methods: HR-adjusted, critical speed.
  • 2026-10-17 correction: Garmin DOES keep daily race-prediction history (Garmin().get_race_predictions(start,end,'daily'), since 2026-07-26; null before). The MCP tool takes no params so it hid this. Hub now reads it via 'docker exec -i garmin-mcp-server python -' (container login, read-only) once a day -> real 'garmin' rows, 'est' rows deleted. Owner had noticed cyan was a lone dot and amber looked the same. The 07-25 VO2 'cliff' was just before Garmin's data begins. Open: live hub needs the same function (copy race_history or run backfill.py) when replaced.
  • 2026-10-17 updated garmin-mcp to upstream 202913f (was 3610be6, 87 commits behind); healthy, real calls + history import verified; rollback image garmin-mcp-garmin-mcp:pre-update. New tools usable for tips: get_running_tolerance(_trend), get_acclimation, get_recovery_time_remaining, get_heart_rate_zones.
  • 2026-10-17 owner asked for 90/180/1y/All on every graph. Sheets now show 30d/90d/180d/1y/All (buttons beyond the data are dimmed with 'only N days of data so far'). Backend keeps ~400 days (BACKFILL_DAYS=400, build_analysis(days=400), weekly 60 wk, tri trend 52 wk, runs-based 365 d); get_activities pages of 100 follow has_more/next_start (account's first activity is 2026-07-20); get_training_load_trend capped at 90 d so it is walked in 88-day chunks; Garmin race-prediction history caps at 1 year per call (use 364) and only exists from 2026-07-26. Sandbox one-off: backfill_deep.py. Deep link #metric-: for screenshots. Live hub will backfill itself (400 d, per-cycle throttle) once replaced.
  • 2026-10-17 fact: Garmin data for this account starts 2026-07-19/20 (steps, sleep, HR, VO2max, load, activities); race predictions 07-26. Deep backfill to 400 days found nothing older, so 180d/1y/All show ~90 days until the history grows. Nothing more to fetch.
  • 2026-10-17 owner: "keep growing this data… it will grow to years". Nothing prunes hub.db. build_analysis() now serves all history (days=None); rows older than 400 days are averaged to one row per ISO week (sports.downsample_old) so the payload stays small (5 yrs of daily rows -> ~600; /api/analysis ≈229 KB, 38 ms today); weekly sport charts, tri trends and runs-based predictions widen with the data (sports.span_weeks, cap 520 wk). hub.db is now in history-backup's sqlite3 .backup snapshot (claude-hub-hub.db.bak, 02:50 restic) and off-site (05:15 R2), raw file excluded; verified the .backup copy passes integrity_check. Sandbox commit 7f49a0d. Live hub still on old 120-day code until replaced (step 15). Caveat: weekly rows' load is one Monday's value, not a weekly mean of a different quantity.
  • 2026-10-17 steps 10-14 built in sandbox, one commit each (361caa3 tips, 150ef4f what-if tab, 5fa9621 load projection chart, 78cd6f0 motion, 672df63 verification). Tips (tips.py) mirror training-sync rules (7.0 h sleep floor, sport gaps, run ramp, acute:chronic). What-if (whatif.py): relative model, 7-day acute / 4-week chronic from hub load (minutes x intensity), scaled so chronic matches Garmin today; fit about 33% mean abs error over 28 days, shown in the UI. Pace slider only shifts race times by pace x distance, it is not a forecast. Verdict: ratio >1.5 bad, >1.3 warn, run ramp >30% warn, <0.8 info. Motion: vendored MIT motion 14.3.0, animate only, esbuild IIFE 65 KB at static/vendor/ (LICENSE + README, served by /vendor/, no CDN); card rise, chart wipe, number tween, all skipped under prefers-reduced-motion. Step 14: all 15 deep-link screens at 375 px have no horizontal scroll and no console errors; fixed a wiLoadChart path starting with L; hub race predictions equal Garmin's (31:41 / 1:07:16 / 2:34:28 / 5:48:36) and hub acute 224 vs Garmin 225. Next: step 15 go-live only after the owner approves on the phone (http://192.168.5.15:8091/): replace live hub :8090, copy race_history + legs data, update map card garmin-mcp.md.
  • 2026-10-17 sandbox steps 16–20 (commits 6fcfa93…2c3386f): multi-sport events from legs + event-calibrated tri estimate (backtest −4%/+15%); Garmin extras pipeline (running tolerance, recovery, endurance/hill score, fitness age, thresholds/FTP, events, strength sets) in garmin_extra.py; Train tab: goal card, fitness tiles, HR zones, PRs, GYM view. Go-live (step 15) still waits for Angel's phone OK. Apple Health: no web path to HealthKit; free route = iOS Shortcut POSTing to the hub endpoint; paid fallback Health Auto Export.
  • 2026-10-17 Apple Health (sandbox commit a8ea945): /api/apple-health now also takes the flat Shortcut shape {"steps":8123,"weight_lb":210.4,"date":"YYYY-MM-DD"} and a list of those for a one-off backfill (old weight etc.); /apple page = recipe (daily Shortcut + Backfill Shortcut) + live status; Apple rows tap to an all-history trend (A.apple.series). The Shortcut must point at the LIVE hub (:8090), which has no flat endpoint until step 15 go-live; the sandbox DB is reset of test rows. No auth token is set on the hub (LAN only).
  • 2026-10-17 owner prefers uploading the whole Apple Health export weekly over a Shortcut. Built in sandbox: POST /api/apple-export (raw export.zip/.xml, streamed to disk, ≤8 GB, background parse in apple_export.py, status at GET /api/apple-export and A.apple.export), upload button on /apple. Per-day metrics: steps (busiest source, not summed across iPhone+Watch), weight (kg→lb), body fat, resting HR, HRV, SpO2, VO2 max, active energy, exercise min, workouts. 400k records parse in ~2.5 s. Checked healthkit-to-sqlite first (loads everything into its own DB; hub only needs ~10 daily numbers). Shortcut kept as optional daily top-up. Live hub still lacks it until step 15.
  • 2026-10-17 owner uploaded the real Apple export to the SANDBOX: 2,051,287 records parsed in 27 s → 1,286 days, 501 workouts, weight 2023-04→2026-10 (245→260 peak→221.5 lb), body fat, resting HR, HRV, SpO2, VO2, energy. Apple Watch metrics (HRV/SpO2/exercise) end 2026-07-19 = when the Garmin watch took over; iPhone steps/weight continue. Found + fixed: a 30.6 lb weight mis-entry (SANE ranges per metric in apple_export.py). Open question for owner: Apple weight 221.5 lb (10-14) vs Garmin 95.25 kg (210 lb) — which is current? That sandbox DB holds real Apple data; migrate it at go-live (step 15) so he does not have to re-upload.
  • 2026-10-17 GO-LIVE (step 15) done. Live hub :8090 now runs the sandbox code (sandbox commit 5d8d1bb): app.py, sports.py, tips.py, whatif.py, garmin_extra.py, apple_export.py, static/ copied from ~/claude-hub-sandbox; deploy method = copy from the sandbox git, systemctl --user restart claude-hub (unit unchanged, no token). hub.db merged (race_history, legs, Apple data incl. the real export, events, strength). Verified on live: sync ran after restart (error None, 401 days), extras/events/strength/tri/apple all populated, 15 deep links no overflow at 375 px, no console errors. Rollback = ~/claude-hub.pre-golive-20261017-2157 (old dir incl. old hub.db; stop unit, swap dirs, start). Upload the weekly Apple export at http://192.168.5.15:8090/apple (data already migrated, no re-upload needed). Sandbox :8091 stays for testing. Open: owner to say which weight is current (Apple 221.5 lb vs Garmin 95.25 kg, feeds bike W/kg).
  • 2026-10-18 owner: weight follows Apple Health (221.5 lb, 2026-10-14) over Garmin (95.25 kg). Bike W/kg tile now uses the latest Apple weight, Garmin as fallback only if no Apple weight: 264 W / 100.5 kg = 2.63 W/kg (was 2.77). Sandbox commit after 5d8d1bb, copied to live static/index.html (no restart needed).
  • 2026-10-18 owner: "Homer" = the Homer services dashboard (container homer, ~/torrentvpn/homer/assets), not the company Home screen; wanted the hub's neon look there. Added assets/custom.css (hub tokens: #06090d/#0c1219, cyan #22d3ee + orange #ff8a2b, mono headings, glow on hover) and listed it under stylesheet: in config.yml (backup config.yml.bak.20261018-neon). Homer is static, so no restart. The look is called "neon on dark" / sci-fi HUD (cyberpunk). Rollback: delete the custom.css line from config.yml.
  • 2026-10-18 Homer: added groups Shop (ERP, company Home, Vaultwarden, Listmonk, website preview) and Sandboxes (ERP sandbox, assistant sandbox, hub test copy) plus Tdarr; backup config.yml.bak.20261018-services. site.lan and sandbox.lan answer 502 while their service is stopped (expected). Not added (localhost-only, need an NPM .lan host first): Grafana :3000, Metabase :3002, Kanboard :8400, store :8500/:8510.
  • 2026-10-18 Homer shows AI usage: the top banner (hub /api/homer-message, Homer polls every 5 min) now reads ai-budget status (cached 60 s, ai_usage() in app.py, test in test_sports.py) and shows Claude 5h / week, Codex week, API $ (% of budget) with the Garmin line under it via
    . Banner colour: green <80% and on pace, amber >=80% or >=10 pts ahead of pace, red >=95% (custom.css styles is-success/is-danger). Homer only supports ONE banner and static tiles (the image has no generic JSON card), so a richer per-pool view would need a hub page linked from a tile. Deployed live (sandbox commits after 5d8d1bb).
  • 2026-10-18: Homer banner now shows Claude API credits as "$57 of $100, resets 8 Nov" on its own line (cap is $100 per ai-budget source; budgets.json total_cap 90 is the per-job ledger cap, not the display). Steps dropped from the banner; Garmin MCP tile removed from Homer config (dead end). Deployed live (claude-hub restarted).
  • 2026-10-18: Homer banner also drops resting heart rate (Angel); banner is AI usage plus load/sleep/readiness only when present.
  • 2026-10-18: Homer banner is AI usage only (Angel removed steps, resting HR, load); Garmin line shows only if ai-budget is unavailable.
  • 2026-10-18: Homepage (gethomepage.dev) trial running at http://192.168.5.15:8092 next to Homer (own project ~/homepage; config converted from Homer, neon custom.css, AI usage tiles from new hub endpoint /api/ai-usage; Glances widget works). Homer's Immich apikey returns 401, so no Immich widget until a fresh key; Pi-hole is v6 so its Homer apikey does not apply. Decision pending: switch homer.lan to it or keep Homer.
  • 2026-10-18: Homepage VPN Status tile = siteMonitor on homer.lan/assets/vpn-ok.json (ping) + customapi widget on vpn-status.json (tunnel, egress IP, city); still fed by vpn-check writing into homer/assets, so Homer's assets dir must stay.
  • 2026-10-18: Homepage has a neon home-in-ring icon (~/homepage/icons/icon.svg, rendered PNGs mounted over /app/public apple-touch + android-chrome); iPhone: Safari → Share → Add to Home Screen at http://192.168.5.15:8092 on home Wi-Fi.
  • 2026-10-18: Hub /api/ai-usage now also returns reset countdowns (claude_5h_in, claude_week_in, codex_week_in, api_in: "4h 39m", "6d 17h", "refreshing" once passed), computed from ai-budget status; test in test_sports.py (fixed clock). Homepage AI group has a second card "AI resets" with four countdown-only tiles (Option B). Known gap: a reset that crosses New Year reads "refreshing" until the year rolls.
  • 2026-10-18: HANDOFF (chat ~24 h old, start a new one): hub live = ~/claude-hub (deploy by copying from ~/claude-hub-sandbox + systemctl --user restart claude-hub); Homepage trial = ~/homepage on :8092. Open: keep Homepage and retire Homer? (then homepage.lan NPM host + *arr/qBittorrent widgets); fresh Immich API key; sandbox hub :8091 still running; confirm next history backup includes claude-hub-hub.db.bak.
  • 2026-10-18: Homer RETIRED (Angel: "Retire homer"). Homepage is the dashboard at homepage.lan (homer.lan kept as alias); NPM proxy host 1 repointed to 192.168.5.15:8092; Homer container removed, compose block commented out. VPN tile now reads hub /api/vpn-status + /api/vpn-ok (serve vpn-check output from homer/assets, which stays). Hub's /api/homer-message and the Homer-banner code are now dead; remove when convenient. Still open: *arr/qBittorrent widgets (need API keys), fresh Immich key.
  • 2026-10-18: Homepage widgets added for Radarr, Sonarr, Sonarr Anime, Prowlarr, Bazarr (keys copied from each app's config into ~/homepage/.env as HOMEPAGE_VAR_*_KEY, 0600; widgets reach apps via the .lan names). Tautulli widget dropped: its API answers but get_activity returns empty data (Plex link looks broken inside Tautulli). qBittorrent skipped (only a password hash, no plaintext); Immich still needs a fresh key made in its UI (the old one 401s). Jellyfin widget needs a key made in its dashboard.

  • 2026-10-22 read-only race-prediction audit (owner explicitly asked to investigate before implementation): checked live :8090 /api/analysis, read-only hub.db, sports.py, MCP prediction source, and live get_activity_splits/get_activity_weather for Oct 21. No application code/config/service/workout changes. Current Garmin / from-runs: 5K 31:14 / 32:18; 10K 1:06:47 / 1:07:21; HM 2:32:48 / 2:28:37; marathon 5:44:13 / 5:09:51. Reproduced that all four from-runs estimates are driven by the Sep 15 4.081 km tempo (26:03) using Riegel 1.06, window 42 d; HR, weather, elevation, long-run preparation and workout segments are unused. Stored standalone runs: 25 (all have avg HR), longest 8.437 km / 5.24 mi. Oct 21 whole-session pace 11:43/mi vs tempo laps 10:12/mi; laps expose warmup/interval/cooldown, HR and ascent/descent. Live weather works: 70 F, dew point 39 F, humidity 33%, wind 10 mph (station KJNX). DB stores summaries, no weather or per-second records. Current thresholds snapshot: LT HR 179, speed 2.667 m/s dated Oct 12; default max HR 205 needs verification before an HR-based model treats it as truth. Existing primary event: Nov 14 10K, 08:00, goal 1:02:08, location North Carolina. MCP get_race_predictions only wraps the general four-distance endpoint; hub does not fetch event-specific weather/course predictions. Recommendation: first check availability of Garmin event-specific prediction (supported watch/course required), use lap/FIT best efforts and calibrated pace-HR on steady sections, account for endurance preparation and event conditions, show uncertainty; critical speed is a later cross-check if adequate hard efforts exist. Keep HRV/sleep as context until individual predictive value is demonstrated. Compare methods chronologically using only pre-event data against actual standalone race/time-trial results; tri legs are a separate target. No accuracy improvement claimed from matching Garmin or from agreement between estimates. Research: Garmin event/weather/course prediction, Runalyze pace-HR calibration, marathon endurance, Riegel validation limits, critical speed study, weather study.

  • 2026-10-22 Race day preview implemented and verified; checkpoints 94f50a0 → a3978fa → c7db98f → 8cf0bd3 → 0b71689 → cc19751 → bc87d4c → c91d870. Owner phone review before live promotion. See Current section.

  • 2026-10-22 Owner asked to add training assessment to dashboard. Added dynamic Training direction in sandbox Train plus Race day shortcut; checkpoint 4d8ca1c, 45 tests plus existing checks and mobile browser validation. Live hub unchanged.

  • 2026-10-22 Owner: “Continue with recommendations.” Promoted verified race/training features to live, added actual race setup/GPX pacing and explicit result onboarding. Runtime 3d1606f; documentation 7f24444; 58 tests, isolated/browser/live checks passed. Data fingerprints preserved; code-only rollback verified. Current section owns deployment state.

  • 2026-10-25: Homepage: Bambu H2S tile in Infrastructure (after Home Assistant), homeassistant widget with the existing HA key: Status, Progress, Left, Nozzle/bed. Proxy returned live values. Backup services.yaml.bak-20261025-h2s.
  • 2026-10-25: Homepage HA tile 'TVs on' read 13 (counted idle Echos, Spotify, Alexa groups, Cast). Now counts only media_player with device_class tv in on/playing/paused → 2. Backup services.yaml.bak-20261025-tvcount.