Commit Graph
115 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.8 b5e106776f chore(release): v1.13.3 — health score rates blood pressure again, steps in Insights
- Health score reads the all-time blood-pressure-in-range rate (not the trailing
  30 days), so an account with BP history but no recent readings keeps its BP
  pillar instead of showing "no rating"; the 30-day rate still drives the tile
  headline. (Regression from the v1.8.7 BP-headline window change.)
- ACTIVITY_STEPS now has its own Insights sub-page + nav entry (it was tracked +
  on the dashboard but absent from the Insights metric registry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:53:15 +02:00
Marc-André Bombeck 016f866965 fix(insights): health-score BP pillar from all-time window + surface ACTIVITY_STEPS in Insights 2026-06-05 22:47:46 +02:00
Marc-André BombeckandClaude Opus 4.8 25bed4262c chore(release): v1.13.2 — honest weekly-med compliance, correct sleep average, calmer wellness scores
- Rolling-cadence (weekly GLP-1 injection) compliance now reconstructs the full
  per-cycle history across the window instead of counting only the next-due dose,
  so a faithful injection history reads an honest rate, not a 0%/100% flip; adds a
  per-medication currentCycle state (on_track/due/missed/none) for a non-scary card.
- Sleep average reads from the de-duplicated per-night reconstruction (bare ASLEEP +
  granular stages no longer double-count → no impossible ~20 h); night reconstruction
  hardened against an empty-segment throw.
- Wellness-score tiles redesigned: gentle card-toned surface (was a saturated slab),
  a larger thinner custom SVG ring with a soft per-score gradient, renamed
  "Your health scores", fully legible in light + dark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:27:27 +02:00
Marc-André Bombeck a55816bf58 fix(medications): retrospective rolling-cadence compliance + currentCycle descriptor 2026-06-05 22:09:06 +02:00
Marc-André BombeckandClaude Opus 4.8 d3d58a0c30 chore(release): v1.13.1 — wellness rings fill to the actual score
The score-ring's RadialBar swept the full circle for any value because the
angular domain was only pinned on the PolarRadiusAxis (which governs radius,
not sweep). Recharts then auto-scaled the angular domain to the single datum,
so every ring read as 100%. Add a PolarAngleAxis with a fixed [0,100] number
domain so the bar sweeps value-percent of the track. Affects the overview
wellness tiles and the score-anatomy detail view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:18:52 +02:00
Marc-André BombeckandClaude Opus 4.8 f1329b24e1 chore(release): v1.13.0 — per-user custom mood tags + catalogue hide
Let a user shape their own mood-tag set on top of the global catalogue:
create custom tags (label encrypted at rest, icon from a fixed allow-list),
and hide built-in tags per user. Custom tags live in `mood_tags` with a
non-null `user_id` and a `custom:<uuid>` key; `GET /api/mood/tags` returns
the effective per-user set (catalogue − hidden + own customs) with new
`custom` + `label` fields, and `?include=hidden` surfaces hidden catalogue
tags for the management screen. The write path resolves a `custom:` key only
against the caller's own rows — a foreign custom key is dropped, never linked.
Delete soft-deactivates by default (history intact); `?purge=true` removes
the row and its links. BINARY-only for v1.

New: POST/PATCH/DELETE /api/mood/tags/custom[/:key], PUT /api/mood/tags/:key/hidden,
migration 0127 (user_id + label_encrypted columns, mood_tag_hidden table,
`custom` category). Integration setup snapshots + restores the seeded catalogue
because the new mood_tags→users FK makes TRUNCATE users CASCADE wipe it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:50:05 +02:00
Marc-André BombeckandClaude Opus 4.8 f25b6aa2dc chore(release): v1.12.12 — deterministic fallback so the period retrospective is never empty
When no AI provider is configured (or before the first AI summary is written),
the period-in-review card stayed empty. Compose a concise, factual, strictly
non-causal summary from the same structured digest the AI prompt is built from
— ranked metric changes vs the prior period, vitals that moved outside their
typical range, and an honest count of statistical associations — and persist it
with a `deterministic` provider marker so a later AI warm replaces it in place.
The read route now warms regardless of provider; the read-never-blocks posture
is unchanged. Fills the no-key demo's retrospective too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:10:08 +02:00
Marc-André BombeckandClaude Opus 4.8 038be6ac57 chore(release): v1.12.11 — unify settings notice colours and callout style
Route every Settings callout, badge and status note through the semantic
colour tokens (success / warning / info / primary) instead of raw palette
classes, so the same meaning reads identically across cards and stays
legible in light mode. Add a single SettingsInfoTile primitive — icon +
tone + text on one fixed surface — and adopt it for the threshold-override
caution note, replacing a bespoke box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:42:18 +02:00
Marc-André Bombeck a15766a418 chore(release): v1.12.10 — a timeless taken-log no longer back-fills a far slot 2026-06-05 16:03:58 +02:00
Marc-André Bombeck 17be1084b1 chore(release): v1.12.9 — tighter mood catalogue, clearer cards, fixed retrospective 2026-06-05 15:40:04 +02:00
Marc-André Bombeck 186798a02f chore(release): v1.12.8 — fuller WHOOP data, charts that follow your selection 2026-06-05 14:58:08 +02:00
Marc-André BombeckandClaude Opus 4.8 de732ba732 docs(api): regenerate OpenAPI for new MeasurementType values
Pick up AVERAGE_HEART_RATE / MAX_HEART_RATE / SLEEP_DISTURBANCE_COUNT in the
MeasurementType enum the iOS client consumes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:46:14 +02:00
Marc-André Bombeck 7ce8868079 chore(release): v1.12.7 — Insights that react, read clean, and stay legible 2026-06-05 14:15:58 +02:00
Marc-André Bombeck 52f4417504 chore(release): v1.12.6 — one consistent Insights surface 2026-06-05 13:01:31 +02:00
Marc-André Bombeck 72146af6f9 chore(release): v1.12.5 — correct sleep summary and readiness contributors 2026-06-05 11:35:26 +02:00
Marc-André Bombeck f062a6016a chore(release): v1.12.4 — a calmer, more consistent Insights 2026-06-05 10:48:07 +02:00
Marc-André Bombeck bd22d68e03 chore(release): v1.12.3 — mark the dose you're looking at 2026-06-05 09:08:24 +02:00
Marc-André Bombeck 5d8a9799ac chore(release): v1.12.2 — WHOOP connect from the app, consistent assessments and medications 2026-06-05 08:21:00 +02:00
Marc-André Bombeck f531494c59 chore(release): v1.12.1 — security, data-integrity, and insight-quality hardening 2026-06-05 03:58:30 +02:00
Marc-André Bombeck 678ee80299 chore(release): v1.12.0 — Google Health sync, rated mood factors, Coach and insights overhaul 2026-06-05 01:19:38 +02:00
Marc-André Bombeck 86b2774567 feat(fitbit): add the Fitbit connection data model and FITBIT measurement source
Lay the data-model foundation for the Fitbit/Pixel integration over the
Google Health API, mirroring the WHOOP provider. No OAuth, sync, or UI yet.

- Prisma: FitbitConnection (1:1 per user, encrypted access/refresh tokens)
  and FitbitOAuthState models, two encrypted BYO-credential columns on User,
  and FITBIT appended to the MeasurementSource enum. Migration
  0117_v1120_fitbit_integration is additive only — one enum value plus two
  tables and two nullable columns, no new MeasurementType (every launch
  metric maps onto an existing type).
- Wire the two new encrypted columns and the FitbitConnection token columns
  into scripts/rotate-encryption-key.ts.
- FITBIT touchpoints: the read measurementSourceEnum (not the writable set),
  the DEFAULT_SOURCE_PRIORITY and workout-source ladders, the sources-section
  label map, the IntegrationKey union plus its display name, and a
  fitbit()/fitbitStatus() query-key pair. The source-rank SQL and read-tier
  collapse pick FITBIT up from the ladders with no code change.
- Add the FITBIT source label to all six locale bundles and regenerate the
  OpenAPI contract so the source enum carries FITBIT for the iOS decoder.
2026-06-04 21:02:10 +02:00
Marc-André Bombeck 9b8a7c98a0 chore(release): v1.11.5 — mood relations insights, sleep depth view, clear-the-decks polish 2026-06-04 19:43:27 +02:00
Marc-André Bombeck e4a85c58af feat(sleep): add the read-only sleep-night hypnogram endpoint
GET /api/sleep/night?date=YYYY-MM-DD returns one reconstructed night for
the phase-progression view: the canonical source's stage segments (each
with an absolute start/end span), the per-stage breakdown, the asleep/
in-bed/awake totals, the mid-sleep awakenings count, and same-wake-day
naps surfaced separately. Session-clustered, wake-day-keyed, one
canonical source per night so two sources never overlay. date omitted
returns the most recent night. A view over existing per-stage rows — no
schema, no new measurement type. OpenAPI regenerated.
2026-06-04 18:56:20 +02:00
Marc-André Bombeck cb4fd14a8a chore(release): v1.11.4 — sleep totals, trend captions, and a round of fixes 2026-06-04 17:27:12 +02:00
Marc-André Bombeck 3bc6124cf1 chore(release): v1.11.3 — WHOOP body data, quality-of-life polish, fuller translations 2026-06-04 15:58:51 +02:00
Marc-André BombeckandClaude Opus 4.8 074e3bbdf1 chore(release): v1.11.2 — Coach-memory controls, SSRF hardening, more pinnable metrics
Coach-memory management UI (review/forget durable facts), connect-time
DNS-rebinding pins on user/operator-overridable outbound hosts, eight
additional pinnable dashboard metrics, plus a rollup write-race fix and
fast-path source-priority amortisation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:31:25 +02:00
Marc-André BombeckandClaude Opus 4.8 cb8568a2fd chore(release): v1.11.1 — source-aware vitals + Coach long-term memory
Closes the three v1.11.0 follow-ups: cross-source vital de-duplication
(measurement rollups resolve overlapping sources to the priority-ladder
canonical reading across charts, summaries, insights, and the live
fallback), the Coach's rolling encrypted conversation summary, and
durable encrypted personal facts with a list/forget surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:56:21 +02:00
Marc-André BombeckandClaude Opus 4.8 8e2e697370 feat(coach): facts management routes (list + forget)
Add the GDPR/trust surface for the durable Coach facts: GET lists the
caller's active facts decrypted, DELETE /[id] soft-deletes one, and a
bulk DELETE clears all — every verb apiHandler + requireAuth +
requireAssistantSurface("coach") + ownership-scoped, with an unknown or
cross-user id collapsing to an idempotent no-op that never leaks
existence. Registered in the OpenAPI contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:29:18 +02:00
Marc-André Bombeck df5f1d170b chore(release): v1.11.0 — WHOOP provider hub, longitudinal coach, clinician-grade FHIR record 2026-06-04 04:16:57 +02:00
Marc-André Bombeck e1cdbf3e06 chore(api): regenerate OpenAPI for the capabilities FHIR/share descriptors 2026-06-04 02:34:43 +02:00
Marc-André Bombeck 4afe1dc6d8 chore(api): regenerate OpenAPI for the trajectory derived metric 2026-06-04 01:40:48 +02:00
Marc-André Bombeck ceabf8a54d chore(api): regenerate OpenAPI for the FHIR and share-link routes 2026-06-04 01:05:15 +02:00
Marc-André Bombeck 86dea3e176 chore(api): regenerate OpenAPI for the WHOOP measurement types 2026-06-04 00:50:26 +02:00
Marc-André Bombeck b56f712d4e chore(release): v1.10.4 — strain honesty label + six-minute-walk reference caveat 2026-06-03 21:33:30 +02:00
Marc-André Bombeck 730c40dfef chore(release): v1.10.3 — personalised strain, a daily signal card, deeper derived metrics 2026-06-03 20:10:17 +02:00
Marc-André Bombeck 46be440ee8 chore(api): regenerate OpenAPI for the derived series field 2026-06-03 19:48:57 +02:00
Marc-André Bombeck e2c7d08bad chore(api): regenerate OpenAPI for the additive derived-metric ids 2026-06-03 19:31:29 +02:00
Marc-André Bombeck 44028b2a6e chore(release): v1.10.2 — honest AI connection test, consistent insights, retention re-enabled 2026-06-03 09:18:55 +02:00
Marc-André Bombeck 0ec5f7552f chore(api): regenerate OpenAPI for the capabilities endpoint 2026-06-03 08:59:31 +02:00
Marc-André Bombeck b7b7e327ce chore(release): v1.10.1 — consistent medication cards 2026-06-03 07:17:01 +02:00
Marc-André Bombeck 7bd66615b6 fix(jobs): disable the dense intra-day retention drain (P2002 fold collision) 2026-06-03 03:39:01 +02:00
Marc-André Bombeck 3080828fb7 fix(jobs): defer the dense intra-day retention boot drain past startup to avoid pool exhaustion 2026-06-03 02:22:35 +02:00
Marc-André Bombeck d43cbf3a3b chore(release): v1.10.0 — derived wellness metrics, device-event awareness, deeper ingestion 2026-06-03 00:08:22 +02:00
Marc-André Bombeck b92d5cec82 feat(insights): batch the derived-metric reads behind one route
The Insights cold mount fired 14+ independent GET /api/insights/derived
requests — Readiness, Sleep and the vitals baselines are compute-on-visit,
and all of them share one Prisma pool. That is the pool-starvation class
that surfaces as the app hanging then recovering.

Add GET /api/insights/derived/batch?metrics=a,b,c[:type] that resolves the
requested metrics server-side under a bounded p-limit(4) with the profile
loaded once via the shared loadBaselineProfile, returning a map of the same
flat Derived<T> envelope keyed by the request token. Add the useDerivedBatch
hook + the insightsDerivedBatch query-key factory entry; the single-metric
route + hook stay for the per-score detail pages. The single route now also
adopts loadBaselineProfile instead of re-inlining the profile read. Honors
the existing 8s ceiling, retry:0, replace-with-fallback, and warms nothing
on visit. Register the new route in the OpenAPI contract.
2026-06-03 00:04:56 +02:00
Marc-André BombeckandClaude Opus 4.8 f170198a83 fix(measurements): validate single-POST source against the writable allowlist
WRITABLE_MEASUREMENT_SOURCES was a dead export whose docblock claimed it
gated the single-entry POST, but the schema only refined out COMPUTED —
WITHINGS and IMPORT stayed client-writable, so a client could attribute a row
to a source the server owns (the Withings webhook / CSV importer) and pollute
the per-source canonical picker. Wire createMeasurementSchema.source to a Zod
enum built from the now-correct {MANUAL, APPLE_HEALTH} allowlist; the enum
subsumes the old COMPUTED refine. Regenerate the OpenAPI contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 23:40:24 +02:00
Marc-André Bombeck a8daef7376 feat(insights): read the persisted wellness scores through the derived contract
Register RECOVERY_SCORE / STRESS_SCORE / STRAIN_SCORE as derived metrics that
read the nightly-persisted COMPUTED Measurement rows — a passthrough, never a
recompute. The dispatcher routes the three ids to a thin reader that returns
the latest score, a trailing-window trend, and a direction-aware band (stress
and strain invert, so a high stress reads red). This lets every surface — the
dashboard, the Coach snapshot, the doctor report — consume the scores through
the one Derived<T> route + hook rather than each growing its own read path.
Regenerate the OpenAPI enum, which picks up the three ids from the registry.
2026-06-02 23:13:26 +02:00
Marc-André Bombeck 33b75361f3 feat(measurements): add a COMPUTED source and first-class score types
Add a server-owned COMPUTED measurement source for rows a server engine
derives rather than a device or a person reports, plus three first-class
0-100 score types — RECOVERY_SCORE, STRESS_SCORE, STRAIN_SCORE (unit
"score", plausibility range 0..100).

COMPUTED is read-only from a client's perspective: it joins the read /
decode enum so the native client can decode the rows it queries and
charts, but every client-facing write surface rejects it exactly like
WITHINGS and IMPORT — the batch ingest allowlist stays {APPLE_HEALTH,
MANUAL} and the single-entry POST 422s a COMPUTED source.

All three score types thread through every type-exhaustiveness site: the
unit + plausibility-range maps, the PR-direction resolver (scores carry
no record direction, like the device events), the category overlay (a
new "scores" cluster), the list-view label / icon / colour maps with
locale strings across all six bundles, and the insight chart-token
allowlist + orphan-enum stripper. They are not clinical vitals, so they
stay out of the doctor-report PDF.

Only the recovery score is computed today; the stress and strain types
are defined now so the engines that compute them later need no schema
change. Migration 0106 is purely additive (ADD VALUE IF NOT EXISTS).
2026-06-02 22:31:21 +02:00
Marc-André Bombeck e893a43005 feat(fhir): wire the German BfArM ATC locale toggle into the export route
The builder already accepts a germanAtc option that appends the BfArM ATC
URI after the WHO entry. Thread it from the health-record export route: an
explicit germanAtc flag on the selection payload wins, otherwise it derives
from a German-region locale. The WHO coding stays coding[0], byte-identical
and locale-independent, so a WHO-only consumer is unaffected.

Add the optional germanAtc field to the strict export selection schema and
regenerate the OpenAPI contract.
2026-06-02 22:22:50 +02:00
Marc-André Bombeck a45325af52 feat(healthkit): thread the device-event verdict through batch ingest
Accept the optional HKCategoryValue codepoint on each batch entry and
persist the resolved rhythm_classification on the stored row. The wire
field is additive and ignored for non-event identifiers, so every
existing caller is byte-for-byte unchanged. Regenerate the OpenAPI
contract.
2026-06-02 22:07:45 +02:00
Marc-André Bombeck 635e64ace7 feat(healthkit): ingest seven additive HealthKit signals end-to-end
Wire seven previously-deferred HealthKit quantity identifiers through
the full pipeline — enum, mapping, plausibility range, category, list-UI
metadata, FHIR coding, and the generic metric-status assessment:

  CARDIO_RECOVERY          heartRateRecoveryOneMinute   bpm
  WRIST_TEMPERATURE        appleSleepingWristTemperature celsius
  FALL_COUNT               numberOfTimesFallen           count
  SIX_MINUTE_WALK_DISTANCE sixMinuteWalkTestDistance     m
  STAIR_ASCENT_SPEED       stairAscentSpeed              m/s
  STAIR_DESCENT_SPEED      stairDescentSpeed             m/s
  BREATHING_DISTURBANCES   appleSleepingBreathingDisturbances count

Each value rides raw on the wire (identity conversion). Fall count is
cumulative (daily SUM); the stair speeds drain to a daily mean; the rest
store as latest per-workout / per-night spot readings. Wrist temperature
and breathing disturbances are flagged privacy-sensitive. The category
sleep-apnea and walking-steadiness EVENT identifiers stay deferred — they
are events, not quantities.

Each new metric gains an Insights sub-page on the shared scaffold with the
generic assessment card, localised across all six locales. Cardio fitness
(VO2 max) — ingested since v1.4.23 but assessment-less — gains its own
/insights/cardio-fitness page so it picks up the assessment instead of
riding as a bare chart-row on the pulse page.

Normal-range anchors are cited where defensible (six-minute walk 400-700 m,
ATS guideline + population reference); the rest defer to the user's own
baseline rather than fabricate a band.

Migration 0104 appends the enum values additively (IF NOT EXISTS, forward-only).
2026-06-02 22:02:50 +02:00