Commit Graph
186 Commits
Author SHA1 Message Date
Marc-André Bombeck fd9603a578 chore(release): v1.30.33 — the session cookie becomes a secret
Also closes the device list's exposure of the same value, refuses a
cross-device refresh rotation, replaces a vacuous expiry test with one
that fails when the check is removed, and asserts the Nightscout host
guard at save time rather than relying on the probe's placement.
2026-07-19 12:09:17 +02:00
Marc-André Bombeck 1c2cfffdb8 fix(consent): a withdrawn AI consent stays withdrawn, and the web can withdraw
The settings mount fires an idempotent consent heal so an account that
predates the gate does not fall into the no-key state. It looked only for
an active receipt, so a deliberate withdrawal — which sets revokedAt —
read the same as never having consented, and the next visit to the AI
settings granted again, writing an audit row for an act the user never
performed. GDPR Art. 7 (3) puts withdrawal on the same footing as the
grant; one undone by a page load is not a withdrawal.

The mint now distinguishes the two callers. The mount heal grants only to
an account with no consent history at all and leaves a revocation
standing; switching document auto-read on is the consent act itself and
may supersede an earlier revocation. The revocation is re-checked inside
the transaction so a withdrawal landing mid-mint is not overwritten.

The revoke endpoint has existed since v1.4.40 but only the native client
called it: on the web, consent could be given and never taken back. The
AI settings now carry the standing decision and the control that changes
it, in all six languages.
2026-07-19 11:52:34 +02:00
Marc-André Bombeck db86b4f76d chore(release): v1.30.31 — surface a due dose on Today; ground the briefing against what the model was actually given 2026-07-19 09:23:45 +02:00
Marc-André Bombeck f61483d19b fix(sw): repair the service-worker version fallback after the release merge
The changelog merge left conflict markers inside the CACHE_VERSION
assignment. The version generator only rewrites the version string, so
it could not repair the broken structure, and every service-worker test
failed while both branches were green on their own.
2026-07-19 05:37:23 +02:00
Marc-André Bombeck 5c2bee79d8 Merge branch 'main' into release/v1.30.28
# Conflicts:
#	CHANGELOG.md
#	docs/api/openapi.yaml
#	package.json
#	public/sw.js
2026-07-19 05:32:41 +02:00
Marc-André Bombeck a3be2d7c92 chore(release): v1.30.28 — meter the briefing path, the last provider entry a sync can reach 2026-07-19 05:26:16 +02:00
Marc-André Bombeck a5631f3d12 chore(release): v1.30.27 — merge the resting read path, fix the rollup window and the batch duplicate 2026-07-19 05:20:29 +02:00
Marc-André Bombeck e9424ef0be chore(release): v1.30.26 — fence document-sourced text and fix the sanitiser that defeated itself 2026-07-19 04:58:17 +02:00
Marc-André Bombeck 81e66cbc5c chore(release): v1.30.25 — meter the status tier, bound the locale fan-out, restore snapshot-once 2026-07-19 04:27:54 +02:00
Marc-André Bombeck 3d776a236a chore(release): v1.30.24 — one locale-aware safety screen on every model-output boundary 2026-07-19 04:05:37 +02:00
Marc-André Bombeck 4b60852f9f chore(release): v1.30.23 — module switches hold on the connector wire 2026-07-19 03:27:30 +02:00
Marc-André Bombeck a6600f0215 chore(release): v1.30.22 — background work stops piling up on itself 2026-07-19 03:00:36 +02:00
Marc-André Bombeck 81e564a724 chore(release): v1.30.21 — dose reminders land on the right hour across a clock change 2026-07-19 02:44:42 +02:00
Marc-André Bombeck 3ffaf1906c fix(admin): refuse a restore whose payload names a different owner
The restore target came from the encrypted payload's own userId field
while the audit entry recorded the backup row's userId. A file declaring
a different account would therefore have been written into that account,
with the trail naming the one the operator selected.

Both values are in hand at that point, so compare them and refuse on
mismatch, recording both ids. Adds the first test coverage for this
route; the mismatch case fails without the guard.

Also correct the CLAUDE.md claim that every API route wraps apiHandler -
the three MCP OAuth routes deliberately use withBackgroundEvent so they
can return RFC-format error bodies, and carry the same protections by
hand.

chore(release): v1.30.20
2026-07-19 02:16:54 +02:00
Marc-André Bombeck 6b08ffd7c5 chore(release): v1.30.19 — module switches hold on the aggregate surfaces; consent required before assistant egress 2026-07-19 01:58:47 +02:00
Marc-André Bombeck e31c66e959 fix(security): scrub wide events at the builder, encrypt the replay cache
Three leaks of the same class - data leaving the process unprotected.

addMeta and addExternalCall wrote their values verbatim while setError
and setHttp scrubbed, so the builder's redaction contract held for two
entry points out of four. The finished event is serialised whole to
stdout and the log store, which made meta a trusted sink by accident: a
Telegram request timeout put the bot token there, and the AI provider
chain fed upstream error bodies through it. Both now scrub, at any depth.

Scrubbing cannot help with the second half. A gateway that echoes the
request it rejected echoes the prompt, and on that path the prompt
carries the health snapshot; no pattern separates that from any other
text. Upstream bodies are therefore classified rather than quoted - the
diagnostic question is which rejection occurred and whether it repeats,
which a classification answers without the content.

The idempotency replay cache stored response bodies unencrypted. The
create paths echo their own decrypted payload, so cycle notes, mood text
and allergy reactions sat in cleartext for 24 hours in a column that
lands in every backup; the existing guard missed it because it matches
secret shapes and health data is not secret-shaped. Bodies are encrypted
now, and when encryption is unavailable the response is not cached at
all rather than written in the clear.

chore(release): v1.30.18
2026-07-19 01:28:20 +02:00
Marc-André Bombeck 586c4318ca chore(release): v1.30.17 — enforce Bearer token scope 2026-07-19 01:13:11 +02:00
Marc-André Bombeck 20ff0c1868 fix(measurements): stop the derived resting mint from suppressing itself
The dense-intraday fold tombstones the raw heart-rate readings a proxy
account's resting figure is derived from, and mints one derived
RESTING_HEART_RATE row per folded day to carry the figure forward. The
mint is skipped for accounts that already hold native resting rows,
because the read resolver ignores the proxy for them.

The probe behind that skip matched every source, including the COMPUTED
rows the mint itself writes. From the second run onward it therefore
reported native resting data for every proxy account, skipped the mint,
and let the fold delete that day's raw readings anyway - losing the
resting figure permanently, once per run.

Scope the probe to non-computed sources. Adds a regression test that
reproduces the second-run state and fails on the previous code.

chore(release): v1.30.16
2026-07-19 00:13:54 +02:00
Marc-André Bombeck d0a74e37da fix(ai): keep the output-language directive terminal on composed prompts
getBaseSystemPrompt appended the directive, then each metric module
appended its own section after it, so the directive was no longer the
last instruction the model reads - the one property that makes it
authoritative. The base now exposes its body, and every composing module
wraps the finished prompt so the directive ends it.

Also drops a duplicate import that survived the merge of the two prompt
branches.
2026-07-18 23:54:09 +02:00
Marc-André Bombeck 5b464b26de chore(release): v1.30.14 — server-prefetch the workouts list; correct the aggregated heart-rate bucket docs
Move the workouts list read into a shared cached module so the RSC
wrapper and GET /api/workouts share one deduplicated projection pass,
and dehydrate it under the client cell's exact query key so the page
paints its rows instead of a loading skeleton.

Correct the aggregated heart-rate externalId documentation: the example
and the min/max field notes still described the original hourly bucket,
while the accepted contract has been 10-minute buckets since v1.30.7.
2026-07-18 22:42:23 +02:00
Marc-André Bombeck 7802903d93 chore(release): v1.30.13 — server-prefetch the insights, medications, and coach pages 2026-07-18 20:38:35 +02:00
Marc-André Bombeck 9e2c445ad7 chore(release): v1.30.12 — index the dashboard medication reads 2026-07-18 18:04:41 +02:00
Marc-André Bombeck c2420d7f02 chore(release): v1.30.11 — native OIDC SSO sign-in 2026-07-18 17:03:35 +02:00
Marc-André Bombeck 57e855eeb3 chore(release): v1.30.10 — localized correlation insights + richer workout detail 2026-07-18 16:13:53 +02:00
Marc-André Bombeck f3d92520d1 chore(release): v1.30.9 — faster dashboard first load 2026-07-18 14:43:57 +02:00
Marc-André Bombeck d4f78811d9 chore(release): v1.30.8 — harden aggregated heart-rate bucket validation 2026-07-18 11:59:38 +02:00
Marc-André Bombeck 0b37c15a52 chore(release): v1.30.7 — 10-minute aggregated heart-rate upload contract 2026-07-18 11:21:17 +02:00
Marc-André Bombeck cf3f217e01 chore(release): v1.30.6 — accessibility pass 2026-07-18 10:29:43 +02:00
Marc-André Bombeck 0f5b18d946 chore(release): v1.30.5 — reachable ECG surface and a sync-status fix 2026-07-18 09:26:58 +02:00
Marc-André Bombeck c93d02c089 chore(release): v1.30.4 — read-only assistant connector reaches the full record 2026-07-18 08:07:35 +02:00
Marc-André Bombeck aaa05241b3 chore(release): v1.30.3 — quality re-check follow-ups 2026-07-18 06:49:16 +02:00
Marc-André Bombeck de8368cf01 chore(release): v1.30.2 — coach history, navigation, correctness, and performance 2026-07-18 05:08:06 +02:00
Marc-André Bombeck ccfda544f8 chore(release): v1.30.1 — mobile, feedback, and consistency polish 2026-07-18 03:55:34 +02:00
Marc-André Bombeck efdcafb08d chore(release): v1.30.0 — quality pass, document summaries, efficacy chart
Correctness (multi-source dedup, ordering, Strava sport, PR/correlations), medical-safety fixes, faster Today view, onboarding, app-wide year-on-dates, a metric catalog, a fuller backup, accessibility, and internal refactors — plus document auto-summaries and the medication efficacy chart.
2026-07-18 02:12:52 +02:00
Marc-André Bombeck 85a05efb4d chore(release): v1.29.5 — OIDC redirect base + same-origin redirect hardening 2026-07-17 17:30:54 +02:00
Marc-André Bombeck 05f80ad378 fix(workouts): order the list newest-first; show the year on past-year dates
The canonical picker re-sorts its output to startedAt ASC for its rollup callers, so the list route sliced the oldest page and recent workouts never surfaced on a long history — re-establish newest-first after the pick. And a workout from a previous year now shows its year so it is not read as this year's.
2026-07-17 16:53:06 +02:00
Marc-André Bombeck c527cd5aa4 chore(release): v1.29.3 — hydration and micronutrients surface
Fluid intake and device-recorded vitamins/minerals get a home: a dashboard water tile with quick-add, and a Nutrients page under Insights showing hydration over the last month, caffeine, and the micronutrients with data — each against its reference intake as context. Manual and device-synced water coexist via a source-aware day-total key.
2026-07-17 16:13:41 +02:00
Marc-André Bombeck 2b5fb617ba chore(release): v1.29.2 — WHOOP workout sport, intraday history, Today dismiss, vitals info-tip
WHOOP workouts map to canonical sports (bike rides show as cycling) with a history backfill and a richer cross-source merge; the intraday heart-rate view gains a day navigator with 90-day dense retention and an hourly fallback for older days; milestone/ECG/tension cards can be dismissed from Today; vitals context moves into an (i) popover and the VO2max heading no longer clips; Oura cycle phases feed the cycle tracker where available.
2026-07-17 14:35:06 +02:00
Marc-André Bombeck 99d80f8512 chore(release): v1.29.1 — polish and fixes from the daily view
Today refreshes right after a dose; a calmer Today hero; the Coach names the attached document; a fresh install starts with tracking on; the vitals explainer line is removed; the illness journal gains an editable end and a settled course line; the morning refresh now triggers from every sleep source and records each skip.
2026-07-17 11:12:30 +02:00
Marc-André Bombeck 2d9237fbad chore(release): v1.29.0 — a daily health companion
The capstone of the daily-value milestone. On top of the Today view, the daily
loop (coach check-in, morning freshness, daily note) and the documents/coach
work already shipped, this completes the picture:
- ECG woven into the insights narrative (device-attributed only, never the
  app's own reading; a new recording surfaces gently in Today).
- Intraday heart rate as the shape of the day, with a careful elevated-at-rest
  tension note (never during activity, never a verdict).
- Calm milestones — states reached, not streaks to keep.
- The cohesion layer: the chosen health rings return to the Today hero, each
  surface carries its metric's calm colour identity, and a quiet reached-moment
  for a milestone — one design throughout.

No new data collected; every daily surface reads the overnight digest. No
migration in this release.
2026-07-17 09:10:56 +02:00
Marc-André Bombeck b87f7b50d1 chore(release): v1.28.56 — documents, closer to hand
Bundles the documents/coach slices onto the daily-system release:
- Ask the Coach about a document opens in a side panel (expandable to full
  page), not a full-page jump.
- Attach one or more documents to a Coach chat from the composer (pick from the
  vault or upload); the conversation runs fenced — grounded in those documents
  only, no health data or tools — and can never reach the tool loop (a sticky
  document-scoped flag plus dual client/server enforcement; migration 0245).
- A read document that looks like lab work has its values detected and proposed
  for your labs, added only on your one-tap confirm.
- A lab scan is also filed in your vault (encrypted) and linked to the readings.

Adversarially security-reviewed (multi-document attach): all clear.
2026-07-17 00:10:16 +02:00
Marc-André Bombeck e272453327 chore(release): v1.28.55 — the Today view comes to life
Bundles three daily-loop slices onto the Today view:
- Coach check-in: a due plan review surfaces as a calm Today card with
  keep/adjust/let-go, wired to the existing plan lifecycle; silence never
  changes a plan.
- Morning freshness: the sleep-dependent parts of the digest and score refresh
  the moment last night's sleep arrives (a debounced, timezone-correct per-user
  job across the Withings, WHOOP and Apple transports), with an honest
  provisional state until then. Adds migration 0244 (a per-user refresh marker).
- Daily push: an opt-in, once-a-day morning notification carrying the day's
  read, off by default, riding the existing dispatch cascade — a calm note,
  never an alert.

All read the overnight digest; nothing generates on open.
2026-07-16 23:33:33 +02:00
Marc-André Bombeck cd3dfb81bc chore(release): v1.28.54 — one hero on the dashboard
Retires the legacy opt-in dashboard hero now that the Today view leads the
dashboard, so the two never stack. Removes the hero mount, its skeleton, the
carousel/spotlight components only it rendered, and the heroVisible layout
field (a stored layout that still carries it is ignored on read, never
re-persisted, so nothing to migrate). The greeting is now always shown. The
mobile-density guard measures the Today hero.

The score-ring selection stays in the layout contract for the native client;
its web picker is a follow-up decision (resurface in the Today view or leave it
native-only).
2026-07-16 21:41:00 +02:00
Marc-André Bombeck 25d720e6e7 chore(release): v1.28.53 — a Today overview at the top of the dashboard
Bundles the daily-digest spine (a single server-built digest read from the
overnight insight cache, never a fresh generation) and the Today hero that
surfaces it: the health score with an honest provisional/finalised face, the
day's lead read, and a short worth-a-look rail of one-tap items. The tiles and
charts stay below, unchanged. The Today hero takes the promoted slot when
Insights is on, so it never stacks with the legacy opt-in hero.

This is the shared foundation the later daily surfaces (morning refresh, an
optional daily note) build on — one digest, one card model, one design language.
2026-07-16 19:37:14 +02:00
Marc-André Bombeck 7c45c876a0 feat(dashboard): tiles for HRV, blood oxygen, breathing rate, temperature, and body composition
Several readings were synced and charted but had no place on the dashboard
strip: heart-rate variability, blood oxygen, breathing rate, wrist temperature,
and — for body-composition scale users — muscle mass, body water, and bone mass.
Each now has a tile.

The tiles are pure client additions: the dashboard snapshot already ships a
summary for every measurement type, so no backend, query, or migration changed.
Each tile is data-availability gated (renders only when the account has that
reading) and is toggleable and reorderable under Settings like the rest.

hrv and respiratoryRate graduate from iOS-only to web-writable widget ids, and
muscleMass is a new writable id (catalogue 36 to 37, within the 40-id PUT cap);
wristTemperature moves from pin-only to a web-rendered tile. All additive — the
widget enum only widens, which the iOS contract treats as non-breaking.
2026-07-16 15:21:03 +02:00
Marc-André Bombeck 5b24ca0fa1 feat(coach): document chats live in the Coach view and history
Asking the Coach about a document opened a separate, half-finished chat panel
that never appeared in Coach history. The bespoke drawer is gone; a document
conversation now opens the real Coach view, appears in the Coach list with a
document badge and a "Chatting about: <title>" note, and is read back through
the same history rail as any other thread.

The prompt-injection fence is unchanged and stays server-side. A document turn
is still sent through the hardened fenced endpoint (no tools, no health
snapshot, grounded in the document text only); the coach tool route continues
to fetch with documentId null, so a document-scoped conversation can never be
continued through the tool loop even from a hostile client. Only the surface
moved into the Coach; the boundary did not.

The relaxed conversation-list and detail filters drop only the documentId
predicate — userId scoping is unchanged, so a document thread is still visible
only to its owner. CoachConversationDTO gains documentId + documentTitle
(server-derived, title/filename only); no schema change (documentId exists
since 0230). OpenAPI regenerated.
2026-07-16 11:01:59 +02:00
Marc-André Bombeck 49ed0f41df feat(insights): view ECG recordings
ECG recordings synced from a compatible watch were stored encrypted but
shown nowhere. They now have a surface in Insights: a list of recordings,
each opening the full single-lead waveform on a familiar ECG grid, with the
date, duration, average heart rate, and the result the device recorded.

The read routes narrow ownership in the query and decrypt the waveform
through the existing fail-closed codec; the overview is min/max-decimated so
the R-wave peaks survive the reduction, with a raw path for a calibrated
view. The result shown is the recording device's own classification,
attributed to the device — HealthLog does not read or interpret the trace
and produces no verdict of its own. A permanent note states this, and a
non-normal result carries a line to discuss the recording with a clinician.
2026-07-16 09:55:35 +02:00
Marc-André Bombeck 0f4e0be1a6 chore(release): v1.28.49 — Apple Health re-import survives a natural-key collision
The Apple Health ZIP import upserted each spot reading on the
(userId, type, source, externalId) index only. When two readings shared the
natural key (userId, type, measuredAt, source, sleepStage) with different
externalIds — a re-import of overlapping data, or two same-instant Apple
samples from different source apps — the externalId upsert missed and the
create collided on the second unique index (migration 0055), raising P2002
and aborting the entire import (issue #486, after the v1.28.36 dedup fix let
the upload run).

flushSpotBatch now wraps the per-row upsert: on P2002 it probes the occupying
row by the natural key (tombstone-inclusive) and adopts it in place — re-keys
to the incoming externalId, takes the new value/unit, resurrects if
soft-deleted — mirroring the v1.28.39 provider natural-key rescues (closest to
withings/sync-sleep). If no twin resolves it, the one reading is skipped and
the run continues; the catch sits in the per-row loop outside any transaction,
so a collision can never abort the batch or the import. Adopted rows count as
updated, skipped ones surface honestly. Workout + stats: paths verified unable
to hit this (no second unique index / deterministic externalId). Repro +
real-Postgres integration tests added.
2026-07-16 08:48:41 +02:00
Marc-André Bombeck a9a318d4a4 chore(release): v1.28.48 — security follow-ups
From the campaign's security re-verification pass (posture otherwise held
across the 10-release week — no Critical/High):

- MEDIUM: the OIDC auto-provision branch re-introduced the first-admin race
  the password-register route fixed in v1.28.42 — count-then-create with no
  lock, so two concurrent OIDC first-logins (or an OIDC first-login racing a
  password first-registration) in the empty-DB window could both mint ADMIN.
  Now wraps count+role+create in a $transaction taking the SAME advisory lock
  key ('register:first-admin') and re-counts inside, so the two paths
  serialise; every provisioned field preserved.
- LOW: the nutrients batch catch echoed the raw exception message into the
  client-facing reason (violating its closed reason-set). Now returns the
  fixed 'upsert_failed' member and logs the detail server-side only.

No migration (advisory lock is runtime).
2026-07-16 08:03:12 +02:00
Marc-André Bombeck 4ec6ae661e chore(release): v1.28.47 — job queue engine update (pg-boss 12.26)
Bumps pg-boss 12.20 → 12.26 (schema-drift detection, self-healing background
index builds, correctness fixes). Some of those fixes surface errors earlier
versions silently swallowed. Verified safe: the app attaches an error listener
to the boss instance (reminder-worker.ts) and uses none of the direct calls the
change affects (insert/fetch/deleteQueue) — only send/work/start/stop/schedule/
getJobById, and the one getJobById is already wrapped in try/catch. The full
integration suite (101 files, 497 tests) exercising the queue against real
Postgres passes unchanged under 12.26. No config change.
2026-07-16 07:26:07 +02:00