Commit Graph
163 Commits
Author SHA1 Message Date
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é BombeckandClaude Opus 4.8 9a07a27001 fix(insights): default to current full-size AI models for the headline feature
The OpenAI BYOK + admin-shared fallback defaulted to a small model and
Anthropic to a 2024 Sonnet, so a user on the operator's shared key got the
weakest model for the Insights assessments. Bump the OpenAI default to gpt-4o
and the Anthropic default to claude-sonnet-4-6; both stay overridable by user
config. Sync the provider docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:58:28 +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 2a8be9fc4a docs(integrations): pin exact WHOOP scopes + webhook URL, add Google Health setup 2026-06-05 12:43:09 +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 540abebf09 feat(ops): alarm on a TLS leaf SPKI change for pinned native clients
The native client SPKI-pins the server's TLS leaf certificate. When the
leaf auto-renews (GTS re-issues it roughly every 90 days) the leaf keypair
— and so the SPKI pin — changes, and a client shipped only the old pin
refuses to connect on the next renewal: a silent outage. Nothing
server-side signalled that a client pins the served leaf.

Add a pg-boss monitor that probes the served leaf every 6 hours, computes
its iOS SPKI pin (base64(sha256(DER subjectPublicKeyInfo)) via a raw TLS
socket + X509Certificate), and compares it against the operator's
known-good set in TLS_LEAF_SPKI_PINS (comma-separated for the dual-pin
renewal window). On a served pin outside the set it fails loud: a
tls.pin.leaf_changed wide-event annotation, a system.tls.pin_changed audit
row, and a high-priority SYSTEM_ALERT fanned out to admins through the
existing dispatcher idiom. Transient TLS/network failures annotate
tls.pin.probe_failed and never alarm.

The baseline is an env var, not a persisted last-seen row — a self-learning
baseline would silently adopt the first rotation and suppress the alarm the
pinned client needs; an unset baseline logs the served pin and warns rather
than auto-adopting. The runbook documents extraction, the re-pin + dual-pin
procedure (≥11 days before expiry, ship to TestFlight), and the assumption
that the probed host is the host the client connects to.

Wires the queue into the worker (allQueues + cron + worker), adds the
optional env to the manifest, compose whitelist, and .env example, and
covers the SPKI computation + change detection with a fixture-cert unit
test plus a queue-registration guard.

(cherry picked from commit cdb88cd77b88aec71ad2f21caf2b15c185dfc01e)
2026-06-05 08:10:13 +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é BombeckandClaude Opus 4.8 65c7012590 docs: document the v1.11.0 features
Add WHOOP as a synced provider (per-user BYO-keys OAuth2 + HMAC-signed
webhooks), the longitudinal coach (narrative + per-metric trajectory),
and the read-only FHIR R4 REST API plus the scoped, time-limited
clinician share-link record across README, the self-hosting and ops
runbooks, and a new WHOOP integration page. Note the two optional
WHOOP instance env vars, that migrations 0110-0114 are additive, and
the two known limitations (dual-source standard-vital dedup deferred;
coach durable-memory deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:25:22 +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é Bombeck ecf93317dd docs: document the wellness-summary report section and correct the computed-scores note 2026-06-02 23:43:22 +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