Skip to content

Context API: Phase 0

FastAPI service on UDev that serves as the Forge fleet's curated context layer. Implements the Decorator Crab master plan (see memory/handoffs/decorator-crab-master-plan-2026-04-21.md).

Status: Phase 0 live. Eight Sleep + Garmin wellness signals ingested from HA every 15 min.

Endpoints

Method Path Auth Purpose
GET / , Service info
GET /healthz , Row counts + source registry summary
GET /sources Bearer Full source registry
POST /ingest/<source> Bearer Normalized fact ingest
GET /context?about=wellness\|home\|all&window=24h Bearer Curated JSON slice

Bind

127.0.0.1:7358: LAN-only, Tailscale-reachable. Never exposed via Cloudflare Tunnel.

Data model (SQLite at /home/justinwieb/forge/data/context.db)

Table Purpose
events_raw Append-only raw payloads, 90-day retention (manual cleanup cron TBD)
facts_wellness One row per (ts, source, user_side, metric). Unique key prevents dupes.
facts_home HA state snapshots
sources Per-source last-success / last-error audit
consents Per-source scope + revocation; tripwire row guards rule #1

Operations

# setup (idempotent)
bash infra/context-api/setup.sh

# start / stop / status
sudo systemctl [start|stop|status] forge-context-api
journalctl -u forge-context-api -f

# trigger a manual HA → context-api poll
infra/context-api/.venv/bin/python infra/context-api/scripts/ha_poller.py

# query from bash
TOKEN=$(grep CONTEXT_API_TOKEN .env | cut -d"'" -f2)
curl -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:7358/context?about=wellness&window=7d" | jq .

What the HA poller maps

HA entity Context metric Source
sensor.eight_sleep_<side>_sleep_score score eight_sleep
sensor.eight_sleep_<side>_sleep_duration_min duration_min eight_sleep
sensor.eight_sleep_<side>_hrv hrv eight_sleep
sensor.eight_sleep_<side>_resting_heart_rate rhr eight_sleep
sensor.eight_sleep_<side>_respiratory_rate resp_rate eight_sleep
sensor.eight_sleep_<side>_bed_temp bed_temp eight_sleep
sensor.eight_sleep_<side>_room_temp room_temp eight_sleep
sensor.eight_sleep_<side>_presence_start/end presence_start/end eight_sleep
sensor.eight_sleep_<side>_bed_presence bed_presence eight_sleep
sensor.garmin_steps steps garmin
sensor.garmin_distance_km distance_km garmin
sensor.garmin_calories_{total,active} kcal_{total,active} garmin
sensor.garmin_resting_heart_rate rhr garmin
sensor.garmin_body_battery body_battery garmin
sensor.garmin_stress_avg stress_avg garmin
sensor.garmin_intensity_minutes intensity_min garmin
sensor.garmin_sleep_{duration_min,score} sleep_{duration_min,score} garmin
sensor.garmin_training_readiness training_readiness garmin

Phase 0 exit criteria (not yet met)

  • Schema created + HA ingest functional
  • 5 consecutive days of clean HA + wellness ingestion
  • Weather (NWS) normalizer added
  • Frigate normalizer added
  • Personal ICS normalizer added
  • Daily rollup cron writes memory/daily/YYYY-MM-DD_rollup.md

Rule #1 tripwire

db.init() refuses to start the service if any active consent row has email = '[email protected]'. This hard-fails the service rather than silently ingesting from personal Gmail.