Commit Graph
27 Commits
Author SHA1 Message Date
Marc-André Bombeck 7981c34cc7 Drop the two visit exports nothing consumes yet
The timezone resolver and the dependent-keys bundle shipped ahead of
their consumers, and the dead-code gate rightly went red on the trunk.
The surfaces that need them arrive with the next visit stage and bring
them back together with their callers.
2026-08-08 13:48:14 +02:00
Marc-André Bombeck 4a1bca0761 Publish the visit contract and name visits at the point of consent
Query keys land in the central factory before any surface reads them,
because the lint rule that bans a bare array means the first component
listing visits cannot compile without a slot. There is deliberately no
server-side cache invalidator beside them: nothing cached reads a visit
yet, and a sweep over caches nobody reads would look like coverage
without being any. The module says so at the place a reader would go
looking.

The OpenAPI table publishes seven paths with everything resolved — the
practitioner as an object rather than an id, each link with its label
and date, the appointment's next due instant computed. What it does not
publish is the reminder origin enum, which still reads with two members:
the third value exists in storage and reaches no response, because every
preventive-care read excludes it. Keeping that contract closed is why
the exclusion is worth a guard.

Classifying visits under the existing health-background domain widens
what that domain grants. Somebody who shared it so a relative could see
an allergy list now also shares every appointment and the address book.
That is stated where it is agreed to rather than left in a code comment:
the consent screen's sentence names visits and practices, in all six
locales, and the activity feed gains a verb line for each of the eight
new delegated actions so an owner reads what was done rather than
'made a change'.
2026-08-08 08:11:54 +02:00
Marc-André Bombeck 110511ff31 feat: create managed profiles from shared access
- add a managed-profile card to Settings -> Shared access, between the
  invitation card and the standing-state cards
- collect exactly the four fields the create route accepts, with the date
  of birth genuinely optional and never derived from a year
- refresh the account payload on success, so the new record reaches the
  switcher and the banner without a reload
- assert the section is reachable for an owner and refused for a Guardian
  inside a profile and for an adult manager
2026-08-07 01:53:26 +02:00
Marc-André Bombeck 9eab70f032 feat(02-12): add scoped profile summary
- expose bounded read-only profile facts for shared records\n- keep profile navigation separate from Settings mutations
2026-08-06 04:59:25 +02:00
Marc-André Bombeck 009de7cb5f feat: add record keyed settings
- Separate managed record settings from actor identity\n- Limit integration reads to the active managed record
2026-08-06 02:26:44 +02:00
Marc-André Bombeck 80175f3bfc feat(sharing): key the query cache by the record it holds
Every cached read in this app is about one health record, and nothing in the
key factory said which one — there was only ever the signed-in account's. Once
a browser can act on somebody else's, `["dashboard","snapshot"]` names two
different answers and the cache cannot tell them apart. That is this repo's
known cache-poison class, on health data, across two people.

One line at the root rather than a dimension threaded through twenty-three key
modules: a `queryKeyHashFn` that folds the record id into the cache identity of
every entry. Key arrays are untouched, which is the part that matters — prefix
invalidation and the import-time dependent-key bundles both match on the array
and keep working. Prefixing the arrays would have broken every bundle silently.

The hash is defence in depth. Switching is a full page reload, so the
in-memory cache is empty on the other side of it; the layers that survive a
reload are the IndexedDB snapshot and the service worker's data cache, and
those are handled by refusing to write either while a switch is on, and by
skipping the restore. Nothing of the owner's record ever reaches the disk of a
delegate's device, so a stale scope marker can cost an empty restore and never
another person's data.

The persister's per-account binding read the account payload's id, which under
a switch is the delegate in both states — it could not tell the two apart. It
now asks the record scope, which can.
2026-08-02 14:26:10 +02:00
Marc-André Bombeck 15b8df11bc chore(release): v1.32.19 — clear the Today view the instant a write lands
A dose logged from another screen marked the Today hero and the dashboard
stale without re-reading them, so returning to the home page kept showing the
old state until the 120 s poll or a hard reload. The daily reads run with
refetchOnMount off, so a bundle invalidation with the default active
refetch-type never touched them while they were unmounted.

Route every intake-affecting medication write through one blessed
invalidateMedicationReads (bundle invalidation plus a forced inactive refetch
of the digest and snapshot): Take all due, the intake edit dialog, the history
bulk/row delete, the dose-history ledger and its add dialog, the lifecycle and
danger-zone actions, and the intake import. Give the Vorsorge reminder
mutations the same daily-rail refresh, bust the biomarker assessment and the
labs-changes read after a lab write, and refetch the daily reads after a
briefing regenerate so the Today lead updates without a reload.

Restore the dashboard-layout concurrency belt: the settings layout query now
always loads its own tokened copy, so a home-page visit can no longer strand a
save without its base token and silently drop the v1.32.16 conflict guard.

Add a structural guard that fails CI when a new site invalidates a daily-reads
bundle without the paired inactive refetch, so this class cannot recur a
fourth time, plus behavioural coverage for the batch take and the Vorsorge
refresh.

Refs #581
2026-07-24 13:56:22 +02:00
Marc-André Bombeck 844b24c7ba fix(daily-loop): invalidate the Today digest on measurement/mood/water writes
Manual measurement, mood, and water writes never carried queryKeys.dailyDigest()
in their invalidation bundles — only medication intake did (v1.29.1). The digest
query also runs with refetchOnMount/WindowFocus off, so a write from the values
list, the mood list, or the hydration quick-add sheet left the Today hero's
score and rail items stale until a hard reload.

Adds dailyDigest() to measurementDependentKeys and moodDependentKeys, and adds
a shared refetchInactiveDailyReads() helper (mirrors the existing medication
seam) that forces the dashboard snapshot and digest to refetch even while
unmounted. Every measurement/mood/water write call site now runs it alongside
its existing *DependentKeys bundle.
2026-07-18 00:37:24 +02:00
Marc-André BombeckandClaude Opus 4.8 65f90160c7 fix(daily): refresh the Today digest after a dose, drop the hero ring cluster
The Today digest went stale after logging a dose: nothing invalidated the
`["daily","digest"]` query, and it reads meds from the same server snapshot
the intake routes already hard-evict — so the "dose due" rail item lingered
until a hard reload. Add the digest key to the medication-dependent bundle and
force the inactive digest (unmounted while the user is on the medication card)
to refetch alongside the dashboard snapshot; the server snapshot eviction seam
is reused, so the refetch returns post-write dose state immediately.

Remove the score-ring cluster from the Today hero — it read as uneven and
wasted tile space. The health-score ring is the hero's only ring now; the
score-ring selection contract stays server-side for the native client. Tighten
the hero's vertical rhythm so the worth-a-look rail sits close under the lead
instead of floating below a half-empty column.

Wire the remaining sleep transports into the morning-refresh trigger: Google
Health, Fitbit, Oura, and Polar now enqueue the debounced morning refresh on a
last-night segment landing, matching the Withings / WHOOP / Apple seams — those
sources had no trigger at all. Make the trigger observable: each early-return
gate now annotates a distinct `daily.morning_refresh.skipped` reason (the
not-last-night gate also carries the samples' resolved local dates, today's key
and the tz) so a real landing shows exactly which gate fired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018V8V9qapgHi7fhgtMJ8SkM
2026-07-17 11:00:22 +02:00
Marc-André Bombeck 168c1c85d7 feat(dashboard): promote a Today hero above the tile strip
Surface the day's read at the top of the dashboard: a Today hero that
renders the daily digest — the health score with an honest
provisional/final and sleep-pending face, the briefing lead with a
read-the-full-briefing affordance, and the worth-a-look rail of priority
cards. The dense tile grid and chart rows stay exactly where they are,
one scroll below, so muscle memory survives and no new nav destination
is added.

The hero reads one cached digest cell (GET /api/daily/digest) through the
centralised query-key factory and unwraps the envelope; it recomputes
nothing and never warms a provider on mount. It is gated on the insights
module and self-gates its empty state: a genuinely empty account renders
nothing rather than an alarming card, while an account with a score but
nothing notable gets a calm all-clear line in the rail's place. A shimmer
skeleton holds the footprint while the digest loads.

Priority-card actions wire to their existing destinations by construction
(log dose, reconnect a sync, view check-ups). New copy lands in all six
locales.
2026-07-16 19:31:13 +02:00
Marc-André Bombeck d4be568625 chore(release): v1.28.42 — snappier lists, correct mood refresh, safer first sign-up
Performance + correctness fixes from the campaign's verified perf-correctness
audit (the two server-query findings — comprehensive-generate + boot-discovery
distinct — are deferred to a careful separate perf release):

- format-locale built a fresh Intl.DateTimeFormat/NumberFormat per call
  (main-thread stall across every history list). Routed through the existing
  process-wide formatter-cache keyed on locale + options (timeZone + hour-cycle
  in the key); output byte-identical.
- measurement-list rendered every row twice (desktop table + mobile cards, one
  CSS-hidden), unvirtualized. Now renders one layout via the SSR-safe
  useIsMobile(md) hook; row markup + every data-slot unchanged.
- moodDependentKeys omitted dashboardSnapshot(), so a mood write left the
  dashboard tile/score stale ~120s (server cache was already correct). Added it.
- register first-user admin was a count-then-create race (two concurrent
  first sign-ups could both become ADMIN). Now an advisory lock + in-transaction
  re-count makes first-admin atomic; P2002 maps to 409 instead of 500.

Regression test per fix. No migration (0243 still free).
2026-07-16 03:18:55 +02:00
Marc-André Bombeck 5445fc67ea chore(release): v1.28.34 — nutrient intake lands as quiet context (server side)
Supplement-style intake written to Apple Health by nutrition apps —
vitamins, minerals, water, caffeine — can now sync as daily totals into a
dedicated NutrientIntakeDay store (migration 0241): composite (user, day,
nutrient) identity, re-post replaces, no soft delete, no rollup coupling.
A code-side catalog carries the 26 nutrient codes with canonical units,
plausibility caps, and EFSA dietary reference values (direction-aware;
chromium cites NIH ODS where EFSA declined a DRV).

Deliberately not a nutrition feature: no diary, no calories, no dashboard
surface. POST /api/nutrients/batch is gated behind the new opt-in
nutrients module (default off — with the module off the server refuses
ingest entirely), rate-limited, idempotency-wrapped, with per-entry
unit-mismatch and out-of-range skips. GET /api/nutrients serves a
catalog-ordered summary; the only visible surface is a read-only card
under Settings → Sources. Coach/correlation access is a later slice; the
iOS reading side is coordinated in the companion-app tracker.
2026-07-14 13:10:47 +02:00
Marc-André Bombeck b3e6ba03d7 feat(measurements): add custom metrics (define, log, chart)
Add a user-defined custom-metric store alongside the built-in measurements
on the Measurements surface. A user defines an arbitrary metric (name, unit,
optional target window, decimals, description), logs values, and charts them.

The store is a separate generic catalog + values pair, isolated from the
closed measurement-type system: not synced, not in FHIR, not in insights —
log and chart only. Names and units are plaintext, mirroring the biomarker
catalog. The inline list lives beneath the measurement list (no new nav
entry); tapping a metric opens a detail page with the trend chart, target
band, stat strip, and an editable value history.

Migration 0224 adds custom_metrics and custom_metric_entries.
2026-06-30 08:54:39 +02:00
Marc-André Bombeck a27a1ee71d Merge feat/v125-inbound-documents into release/v1.25.0
Inbound documents module (migration 0218).

# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	prisma/schema.prisma
#	src/lib/modules/__tests__/gate.test.ts
#	src/lib/query-keys/index.ts
2026-06-28 17:05:50 +02:00
Marc-André Bombeck 088cad6c64 feat(environment): add opt-in module, settings surface, API, and nightly fetch
Add the opt-in Environment module (default off), its Settings surface (home
location search, travel overrides, backfill, with Open-Meteo attribution), the
gated API routes (overview, home, geocode, travel CRUD, backfill), the nightly
pg-boss fetch job with a lookback window plus on-demand backfill, the query
keys, six-locale strings, and the regenerated OpenAPI. The module performs an
outbound weather fetch tied to a coarse location, so it ships dark and the user
turns it on deliberately; off means no job, no row, no egress.
2026-06-28 16:58:52 +02:00
Marc-André Bombeck 422778f679 feat(documents): module gate, nav, and review-then-confirm UI
Add the opt-in inboundDocuments module (off by default, like the MCP
endpoint): ingesting a clinical document egresses it to the OCR/vision
provider, so the surface ships dark and the user turns it on from the
Modules hub. The born-gated /documents page carries the prominent
non-diagnostic disclaimer (we read, we don't interpret; nothing is saved
until you approve it; only your clinician can interpret these results),
uploads a document, and walks each extracted fact: source provenance and
confidence are shown, low-confidence facts are blocked from approval until
edited, and the confirm step commits the approved facts.
2026-06-28 16:57:16 +02:00
Marc-André Bombeck ce2bddcc51 Merge branch 'feat/v125-records' into release/v1.25.0
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	scripts/rotate-encryption-key.ts
#	src/lib/crypto/encrypted-columns.ts
#	src/lib/openapi/routes/index.ts
#	src/lib/query-keys/index.ts
2026-06-28 16:10:23 +02:00
Marc-André Bombeck 5052b2cc08 feat(records): CRUD API + OpenAPI for allergies and family history
Add the `/api/allergies` and `/api/family-history` endpoints (list / create /
read / update / soft-delete), each wrapped in `apiHandler` with Zod
`safeParse` + `returnAllZodIssues`. `userId` is always narrowed from the
session/Bearer — never a body field — and the Prisma `data` is built
field-by-field (no mass assignment). Creates wrap in `withIdempotency`. The
free-text reaction/note are encrypted before write and decrypted fail-soft on
read via the server-authoritative DTOs.

Register the query-key factory entries and the OpenAPI route tables (new
`Records` tag); regenerate `docs/api/openapi.yaml`.
2026-06-28 16:03:05 +02:00
Marc-André Bombeck 7575275d7e feat(mental-health): opt-in PHQ-9 / GAD-7 screeners with item-9 crisis signposting
Add the validated mental-health self-assessment surface, beside mood tracking
(never replacing it) and off by default:

- MentalHealthAssessment stores per-item answers in a single AES-256-GCM Bytes
  blob (registered for key rotation); only the server-authoritative total,
  severity band, and item-9 flag are denormalised. The total projects to a
  PHQ9_SCORE / GAD7_SCORE measurement row so the trend rides existing infra
  without ever exposing item content.
- Item-9 safety: any non-zero PHQ-9 self-harm answer surfaces a calm,
  locale-aware crisis-resource set (DE: TelefonSeelsorge 0800 111 0 111 / 112;
  international fallback) and stamps crisisShownAt. No clinical interpretation,
  no AI ingestion of item content, no third-party alerting.
- API + OpenAPI: POST/GET /api/mental-health/assessments; the response excludes
  raw item answers by construction. Required Pfizer/PHQ attribution lives in the
  instrument module, rendered verbatim.

The screener scores are kept off the Coach snapshot and MCP surfaces, and out of
exports by default — item content never reaches the AI pipeline.
2026-06-28 15:47:39 +02:00
Marc-André Bombeck 2d5c4cfe46 fix(dashboard): surface freshly-added and synced measurements without a reload
A measurement added in-app left the dashboard hero, score ring and tile
strip showing yesterday's value until a hard reload. The snapshot query
carries refetch-on-mount/focus off and a 120 s poll, but the measurement
mutation bundle never invalidated the snapshot key, so an in-app create /
update / delete updated every other surface and left the Startseite stale.
Add the snapshot key to the measurement-dependent bundle, mirroring the
medication fix; the manual routes already hard-evict the server snapshot
bucket, so the triggered refetch returns post-write data at once.

Background syncs (Withings, Apple Health, iOS batch) fire no client
mutation event, so the keys were never invalidated and the dashboard only
caught up on the 120 s poll or a reload. Refetch the snapshot on window
focus so returning to the tab after a sync surfaces the new readings; the
60 s staleTime keeps rapid focus toggles free and the warm server cache
keeps the focus refetch cheap.
2026-06-20 12:00:07 +02:00
Marc-André Bombeck b079431a8d feat(illness): add the condition-journal MVP behind a born-gated module
Introduce a retrospective illness/condition journal: episodes with a
lifecycle (acute / chronic-ongoing / recurring / flare) and a daily
symptom timeline forked structurally from the cycle log-day surface. The
journal is a record, not a medical device — no prediction or diagnosis UI.

- Register `illness` as a born-gated (opt-in / default-off) module. The
  gate resolver flips the disabled-allowlist default to off for born-gated
  keys, so the surface stays dark until the account opts in from the
  Modules hub. `isIllnessEnabled` + `requireIllnessEnabled` delegate to the
  module foundation (one source of truth) and re-stamp an illness errorCode.
- Episode CRUD under /api/illness: list / create / get / edit / resolve /
  delete, plus a per-episode-day day-log upsert on (episodeId, date). Every
  route wraps apiHandler, Zod-validates with returnAllZodIssues, narrows
  userId from auth, builds data field-by-field, and gates the module.
  Free-text notes encrypt at rest through the shared Bytes codec.
- Log-day sheet (cycle fork): ~8 Jackson/WURSS symptom chips 0-3 + a
  functional-impact group + fever + an encrypted note, one row per day.
- Episode history page + a born-gated nav entry, neutral palette, status
  via a neutral Badge, retrospective-only copy. i18n across all six locales.
- Cover the new routes in the module-route-gate inventory; unit tests for
  the day-log upsert, the gate enforcement, and the born-gated resolver.

The /api/auth/me module map now projects the `illness` flag (openapi.yaml
regenerated).
2026-06-16 21:09:35 +02:00
Marc-André Bombeck 4f039626be merge(labs): structured lab results with reference ranges, doctor-report and FHIR
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/pl.json
#	src/lib/openapi/registry.ts
#	src/lib/openapi/routes/index.ts
#	src/lib/query-keys/index.ts
2026-06-14 23:43:47 +02:00
Marc-André Bombeck 271cc0dcfa feat(labs): add structured lab-result store with /api/labs CRUD
Record discrete biomarker readings (HbA1c, LDL, ferritin, TSH, …) with
their value, unit, reference range and sample date. The store pairs with
the annual blood-panel reminder, which until now had nowhere to record
the result it reminds the user to obtain.

The list/create/update/delete endpoints narrow userId from the session,
build the write data field-by-field, soft-delete via deletedAt, and
encrypt the optional note at rest. A neutral three-state reference-range
classifier (in-range / below / above / unknown) is computed
server-side so every surface renders the same calm verdict.

findLatestLabResultForAnalytes exposes the read point the blood-panel
reminder consults to mark itself satisfied, without coupling the two.
2026-06-14 23:39:16 +02:00
Marc-André Bombeck c6045482c3 feat(reminders): preventive-care surface — wann, was, wo
Add the Vorsorge page and section: an upcoming-reminders list sorted by
server-computed next-due, with a create form (measurement-linked or
free-text, cadence, notify hour, location) and a per-card Erledigt and
delete action. Cards stay neutral-coloured regardless of due state — the
status reads through a discreet badge only, never an alarming tint. Copy
ships in all six locales.
2026-06-14 23:33:37 +02:00
Marc-André Bombeck d1ba01548f Surface sleep-debt and chronotype on the sleep page
Compute cumulative sleep debt and the MCTQ chronotype server-side off the
canonical night reconstruction the Sleep Score reads, and render them on
the sleep page: a sleep-debt headline with a calm partial state under the
night threshold, and a chronotype card whose social-jetlag detail and
corrected mid-sleep sit behind an advanced disclosure. Both hold a learning
state on thin data and never assert a total or a band.

Serve the same values to the dashboard summary and a new GET /api/sleep/rhythm
read so the native client renders identical figures. Free vs work nights
default to weekend in the user's timezone until day-type tracking exists.
2026-06-14 19:03:11 +02:00
Marc-André Bombeck 19a24a5160 fix(dashboard): a dose taken on the dashboard updates the dashboard
The hero band, dose tally, verdict and checklist all read one snapshot
query that neither refetches on mount nor on focus — and no mutation
ever invalidated its key, so a dose taken right there stayed visibly
due for up to the next two-minute poll. The snapshot key joins the
medication-dependent invalidation bundle; the intake routes already
hard-evict the server-side bucket, so the triggered refetch returns
post-write data immediately. (#316)
2026-06-12 21:47:47 +02:00
Marc-André Bombeck 35e8f076a2 refactor(openapi,queries): feature-located registries behind thin indexes
The five-thousand-line route registry splits into thirteen domain
modules and the query-key factory into twelve feature files — each
behind an index that preserves exact registration order, proven by a
byte-identical generated contract. The factory lint rule learns the new
home directory.
2026-06-11 00:10:00 +02:00