Commit Graph
5 Commits
Author SHA1 Message Date
Marc-André Bombeck ae8b614dd0 feat(insights): choose the status regeneration gap by ingest source
A sync from a watch or an integration now waits the instance's sync gap
(AppSettings.aiSyncRegenGapHours, else AI_SYNC_REGEN_GAP_HOURS, else 6 h)
before it may regenerate a status card again; a hand entry keeps the hour.
Every caller of afterMeasurementMutation and invalidateStatusInsightsForTypes
names its source, so a new ingest path cannot skip the choice.

Refs #1217
2026-10-15 22:49:00 +02:00
Marc-André Bombeck 9fb5d7d7d0 Route measurement deletions into the rollup recompute path
A soft-deleted measurement never reached the rollup tier: the WHOOP
delete webhook tombstoned rows without recomputing the dirtied buckets,
and the freshness watermark probe filtered tombstones out, so the
deletion's updatedAt bump was invisible and the ghost aggregate read as
fresh forever.

Extract the post-write tail every measurement surface repeats into
afterMeasurementMutation (rollup recompute + status-insight
invalidation, both best-effort), call it from the WHOOP delete webhook
with the (type, measuredAt) pre-image collected before the updateMany,
and drop the deletedAt filter from the watermark probe so a tombstone
counts as staleness.
2026-08-14 01:22:41 +02:00
Marc-André Bombeck 62eb431d66 release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0

* test: stabilize water capture re-entry
2026-07-22 04:14:15 +02:00
Marc-André Bombeck 2cb84c17e9 Add per-resource cursors, fetch-by-id, and full backfill to WHOOP sync
Replace the single shared last-synced cursor with a per-resource cursor map
on the WHOOP connection. Recovery, sleep, workout, and cycle each advance only
their own cursor on a successful fetch+upsert, so a slow or 403-ing collection
never drags the incremental window forward for its siblings and its own
late-arriving records stay inside the overlap. Legacy connections fall back to
the shared last-synced instant, so no historical state is lost.

Add single-record fetch-by-id client routes and per-resource by-id syncs. The
update webhook now carries the resource id through the queue payload, so the
worker resolves and upserts that one record immediately rather than re-walking
the whole collection on the next overlap tick.

Anchor a full sync at a fixed deep-history point instead of returning no lower
bound, so a backfill covers the multi-year span an incremental tick would skip
and a stale cursor cannot silently narrow it.
2026-06-14 17:19:09 +02:00
Marc-André Bombeck dd1071158f feat(whoop): OAuth connect/callback + HMAC-signed webhook + CSP gating
Connect mints the state nonce into the WhoopOAuthState ledger and redirects
to the WHOOP authorize page; callback verifies the nonce (atomic
delete-first consume, four distinguished reason tags), exchanges the code,
fetches the WHOOP profile for the user id, persists the encrypted
WhoopConnection, and enqueues the self-converging history backfill.

The webhook handler rate-limits per source first, verifies the
WHOOP_WEBHOOK_SECRET path segment, then verifies the HMAC-SHA256 body
signature over the raw bytes (timestamp + body, timingSafeEqual, stale
timestamp reject) before any work. Valid *.updated enqueues the matching
per-resource sync; *.deleted soft-deletes (workouts hard-delete, no
soft-delete column). Unknown users return 200 to stop retries.

CSP gates api.prod.whoop.com to the WHOOP settings surface + /api/whoop/*;
the webhook path joins the public-path allowlist and the path-secret redact
registry. Env wiring adds WHOOP_WEBHOOK_SECRET + WHOOP_REDIRECT_URI to the
compose whitelist and .env.production.example.
2026-06-04 01:40:27 +02:00