Commit Graph
526 Commits
Author SHA1 Message Date
Marc-André Bombeck 304f0c7057 refactor(settings): route every card header through SettingsCardHeader
Hand-rolled section headers had drifted into a dozen variants — some
without an icon, some with a text-sm description instead of the shared
text-xs, the data-reset title once even tinted red. Route the remaining
ad-hoc headers through the SettingsCardHeader primitive so icon sizing,
title weight and description size live in one place.

Give the integrations connections group (the data sources) its own
labelled header for parity with the delivery-channels group below it.

A coverage test now globs every settings *-section.tsx / *-card.tsx and
fails on any hand-rolled <h2 text-lg> outside the primitive, with a short
allowlist for the collapsible-toggle security cards and the layout
section pending its hub rebuild.
2026-06-30 12:22:08 +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 bdac5b2c0d feat(admin): resolve the sign-in carrier online + give it its own column
The network operator (carrier) used to resolve only from the optional
offline GeoLite2-ASN database. On a host without it the carrier stayed
empty even though the location resolved fine online.

Mine the operator from the same online provider response the location
lookup already fetches — ip-api's top-level `isp`/`org`/`as`, ipwho.is's
nested `connection.{isp,org,asn}` — so the carrier is populated out of the
box. The offline ASN database stays authoritative when configured.

A single `lookupIpGeo` now returns location + ASN + carrier in one pass;
the audit write and the backfill both go through it. The backfill also
revisits rows that already had a location but no carrier, so the column
fills in across existing history, not just for new sign-ins.

In the admin sign-in overview the carrier moves out of a chip stacked
under the login method into its own column, next to the location.
2026-06-29 23:51:40 +02:00
Marc-André Bombeck 11774f731e refactor(settings): fold tracking modules into Appearance, dedupe nav
Collapse the duplicated settings navigation so each surface lives in
one place:

- Appearance ("Darstellung") stops being a link hub and renders the
  dashboard, insights, and every tracking module's settings inline as
  stacked, anchored sections. Each module section keeps its module
  gate, so a disabled module's section does not render. The standalone
  Medikamente / Stimmung / Labor / Krankheit / Vorsorge nav entries are
  gone; their routes 301-redirect to /settings/layout#<anchor>.
- Move the delivery channels out of Notifications into Integrations,
  where the other connected services live. Notifications keeps only the
  reminder types. /settings/channels now redirects to
  /settings/integrations#channels; the per-channel anchors still resolve.
- Fold the clinician share-link surface into the health-record section
  as a labelled Sharing group. /settings/sharing redirects to
  /settings/gesundheitsakte#sharing.
- Drop the duplicated active-sessions and security-activity cards from
  Account; they remain under Data & Privacy.
- Reserve the scrollbar gutter on the settings content column so a
  section with a vertical scrollbar and one without render at the same
  width.

Move-only: every card renders through its existing component unchanged.
2026-06-29 22:16:02 +02:00
Marc-André Bombeck 2500dbeb1a fix(mental-wellbeing): match the standard chart, widen the page, drop the redundant chrome
- The screener trend now reads like every other trend: a monotone line over
  the primary colour with the dashboard's soft gradient area fill and matching
  margins. The surface-only severity-band backgrounds are gone (the band still
  shows in the tooltip), so it no longer looks different from the dashboard.
- The landing fills the content width like the other tracking surfaces instead
  of a narrow max-w-3xl column.
- The voluntary-self-test disclaimer line and the visible page heading + its
  tooltip are removed; the title stays as an sr-only heading for the outline.
- The instrument card's last-test line adopts the medication-card grammar:
  label left, the relative day (today / yesterday / date) right-aligned, no
  severity badge.
2026-06-29 20:00:36 +02:00
Marc-André Bombeck 7e901e674a fix(ui): own the time picker so it follows the 24-hour preference
The native datetime-local / time picker renders its clock and AM/PM
toggle by the browser UI language, not the page lang or the app's
time-format preference — so an English-language browser showed AM/PM
even with 24-hour selected. Replace the native time control with an
app-controlled TimeField (text entry + a dependency-free popover of
hour/minute, plus AM/PM in 12-hour mode) whose hour cycle always tracks
the preference. DateTimeField now composes the native calendar (date)
with TimeField (time); the committed value stays a local
yyyy-MM-ddTHH:mm string, and min/max clamp the recombined value.

Also thread the time-format preference into the doctor-report PDF so its
generated-at timestamp honours it instead of the locale default.
2026-06-29 19:03:47 +02:00
Marc-André Bombeck 7eca388f56 Merge v1.25.3: channels into notifications + Darstellung appearance hub 2026-06-29 15:49:39 +02:00
Marc-André Bombeck 537b31f4ac feat(settings): fold delivery channels into Notifications, rename the Dashboard hub to Appearance
Two surgical, additive, reversible changes to the flat settings navigation.

Notifications becomes one surface with two sequenced, labelled groups:
Reminders ("what you receive") first, then Delivery channels ("where it
lands") = the existing channels panel, unchanged. The standalone Channels
sidebar entry and slug are dropped; /settings/channels 301-redirects to
/settings/notifications#channels (mirroring the reminders redirect). The
per-channel anchors keep resolving.

The mis-titled "Dashboard" area (slug kept as layout, so no route churn) is
renamed Appearance / Darstellung and widened from two links to six: dashboard
arrangement, insights overview and pills, plus deep-links to the view/sort/
order cards of medications, labs, the illness journal, and checkups. Each
module keeps its view cards on its own page; the hub only indexes them. The
checkups view card gains a vorsorge-view anchor so the deep-link lands.

i18n updated across all six locales; settings and notifications tests track
the new structure.
2026-06-29 15:47:39 +02:00
Marc-André Bombeck 78fec1ceff Merge v1.25.3: v1253-mental-wellbeing 2026-06-29 15:30:33 +02:00
Marc-André Bombeck d3057e4a92 Merge v1.25.3: v1253-briefing-timeout 2026-06-29 15:30:33 +02:00
Marc-André Bombeck d4272b13c4 feat(mental-health): one-question check-in + card layout + trend history
Rework the PHQ-9 / GAD-7 screener surface to the card + detail-page grammar
the rest of the app uses, and replace the all-questions-at-once form with a
one-question-at-a-time check-in. Presentational + client-flow only — the API,
encryption at rest, the opt-in module gate, and the item-9 crisis-safety
behaviour are untouched, and the surface stays off the AI Coach by construction.

- Split the single 387-line component into a focused folder: an orchestrator +
  landing, instrument card, check-in wizard, result block, history card + its
  lazy chart, a shared crisis card, shared types, and a pure step-grammar module.
- Check-in reuses the medication wizard's stepper + Back/Next footer grammar,
  rendered inline on the page: one question per step, auto-advance on select,
  backward navigation to change an answer, never skip ahead to an unanswered
  question, and a review step that folds in the optional functional-difficulty
  follow-up before submit. Progress is announced and the steps are keyboard
  navigable.
- Header carries the instrument title + one standardized explanation line; the
  "voluntary self-test, not a diagnosis" disclaimer renders only on the landing
  and behind the info tooltip, never while taking the test.
- Landing follows the Vorsorge / medication card layout: neutral instrument
  cards with a last-result line + Start, and a history card.
- History gains a lazy Recharts trend chart with muted severity-band reference
  areas (never an alarm colour) and a PHQ-9 / GAD-7 toggle, plus a dated list
  with band badges. A flagged row keeps a discreet "support shown" marker that
  re-surfaces the static crisis-resource card without decrypting item content.
- Add the new mentalHealth.* keys across all six locales; drop the orphaned
  stem key the redesign no longer renders. New tests cover the step grammar
  (advance / back / reachability), the disclaimer staying hidden during the
  test, the item-9 crisis surfacing, and the history rendering.
2026-06-29 15:09:28 +02:00
Marc-André Bombeck 4ad4707be9 fix(insights): derive briefing warm budget from the response timeout
The comprehensive briefing warm/cron path wrapped generation in a fixed
130 s outer abort, but the per-user response-timeout setting accepts
10-600 s and the AI settings tell users to raise it for slow local
models. Any account whose response timeout was >= ~130 s had every
comprehensive warm clipped by the warm bound, not the provider, so the
generation never landed: no cached briefing row was written and the
dashboard fell to the hard empty state.

Size the warm outer abort from the resolved effective timeout
(`aiResponseTimeoutSeconds * 1000`, falling back to the comprehensive
surface budget) plus a fixed headroom for feature extraction, prompt
assembly, and the cache write. A raised setting now extends the warm and
nightly budgets instead of being silently capped. An absolute ceiling
(the 600 s write-time maximum plus the headroom) keeps a wedged call from
pinning the worker while staying above the largest value a user can set.
Both the nightly loop and the forced on-visit warm read the per-user
setting and use the derived budget.

Make the empty state more useful: record the upstream HTTP status on a
failure marker and classify it (timeout / auth / rate-limit / provider /
format), so the "couldn't generate" state points at the right lever —
raise the response timeout for a timeout, re-check the provider for an
auth rejection — instead of one generic line. Keep-last-good behaviour is
unchanged.

Align the response-timeout field copy with the actual default budget
(~120 s, not ~180 s) across the placeholder, the OpenAPI description, and
the inline note.
2026-06-29 15:08:04 +02:00
Marc-André Bombeck c6cbd5442f fix(labs): polish the biomarker-detail header and description
Resolve the catalog slug from both the current-locale display name and the
English canonical name. A marker stores the catalog name as it read at
creation time, so one minted under the English default and then viewed in
another locale never matched a map built only from the current locale's
names — its rich per-biomarker description silently fell back to the generic
line. The English bundle is always in the client chunk, so adding its names
makes the lookup independent of the locale the marker was created in.

Tidy the header control cluster:

- Drop the standalone "adjust target range" control and its sheet; the
  reference bounds are already editable inside the full marker editor behind
  the pencil. Remove the now-dead handler, mutation, and `targetRange.*`
  strings across all locales.
- Give the show-all-values, edit, and delete controls one shared ghost-icon
  treatment instead of the show-all control's separate muted variant.
- Add hover tooltips to edit and delete, matching the other icon controls.
- Order the controls left → right: delete, edit, show all values, add value.
2026-06-29 14:55:05 +02:00
Marc-André Bombeck f564cdeef7 revert(settings): restore the flat settings navigation
Revert the nine-group consolidation and the card relocations back to the
flat sidebar: sessions, trusted devices, and login activity return to
Privacy, and account deletion returns inline under Advanced. Drop the
/settings/layout redirect and the now-unused group and sign-in cross-link
i18n keys across all six locales.

Keep the collapse-by-default sessions and activity cards, the medical-history
cross-link from "About me", and the chronic-conditions consolidation.
2026-06-29 07:12:10 +02:00
Marc-André Bombeck 679f8a05c9 fix(documents): surface confirm failures and harden the library edges
Close the gaps the final verification pass flagged on the documents surface:

- Confirm no longer claims success when the route rejects a fact into its
  `failed[]` array (HTTP 200). A unit-mismatch fact left silently PENDING with
  a green toast now raises a warning naming how many couldn't be saved and why.
- Unify the German documents copy on "Angaben": the discard-confirm body, the
  provider-unsupported note, and the module description still said "Fakten".
- After upload, sort by date-added rather than document date so a back-dated
  upload always lands on the first page where its detail panel can open.
- Give the list its own load-error state with a retry instead of rendering the
  "no documents yet" empty state on a failed fetch.
- Classify the local-OCR-disabled extract error to a clear message.
- Drop the orphaned settings.sections.notifications.inboxLink key (no call site).
2026-06-29 01:03:44 +02:00
Marc-André Bombeck 2ec4828c1f Merge v1.25.1: deep data/cross-cut (cadence-aware alert compliance, lab-unit guard, signals in briefing, biomarker fallback, cleanups) 2026-06-29 00:38:45 +02:00
Marc-André Bombeck 2a66e81031 Merge v1.25.1: settings deep-round IA (sign-in consolidation, sub-tab relabels, account-delete relocation, a11y id, ES labels) 2026-06-29 00:38:38 +02:00
Marc-André Bombeck 2321ce29db fix(insights): route alert-card compliance through the cadence-aware engine + deep data cleanups
The insights alert cards computed medication compliance with the naive
`schedules.length × days` denominator, so a fully-adherent weekly
injectable read ~14% and fired a false low-compliance alert (and a paused
medication mis-fired the same way). Route the alert path through the
canonical `calculateCompliance` engine like every other compliance
surface — pause eras, schedule revisions and dose windows honoured.

Document confirm: committing a numeric observation against an existing
biomarker silently replaced the stated unit with the catalog's, with no
conversion — a stated "6.1 mmol/L" against a mg/dL marker committed
"6.1 mg/dL". Reject on a unit mismatch (fail closed) so the user
reconciles on the review screen, the same posture as the unit-required
guard.

Wire grip strength, waist (with waist-to-height) and pain into the
briefing feature set and the grounding allow-set so the clinical-depth
signals reach the daily narrative; the mental-health screeners stay
excluded by construction.

Biomarker detail: fall back to a generic description for catalog-less
markers, and keep the heading + description with a retry on the error
state. Collapse the medication External-API and Lifecycle groups by
default. Re-enable the insights scroll-to-top regression and assert the
shipped main-content reset.

Align the EN haemoglobin/haematocrit display names to British spelling.

Drop the redundant `(userId, deletedAt)` index on inbound documents — a
strict prefix of the new three-column index. Trim the dead
MedicalDisclaimer variants to the one live `dataPosture`. Move the
crisis-resources card off the retired amber tint to a calm
destructive-subtle border. Add a registry invariant asserting every
biomarker signal key resolves to a real catalog slug.
2026-06-29 00:37:39 +02:00
Marc-André Bombeck aa432244ce fix(settings): consolidate sign-in management and tidy the settings IA
Follow-ups on the v1.25.1 settings group consolidation.

- Sign-in security is no longer split across two top-level groups. Active
  sessions, trusted devices, and the login-activity feed move into Account
  > Security alongside the second-factor and passkey controls, so every
  "who can sign in as me" control sits in one place. The Data & Privacy
  pane keeps the data / retention / encryption story and cross-links to
  the security home. Sessions and activity are no longer duplicated on the
  Profile tab.
- First-child sub-tabs no longer repeat their group name: the section
  titles become "Profile & preferences", "Connections", and
  "Privacy & retention" across all six locales.
- Account deletion moves out of Advanced into the Account group as a
  standalone card; research mode and the data reset stay in Advanced.
- Disambiguate the Spanish labels for the health record vs medical
  history (they both read "Historial médico"); medical history becomes
  "Antecedentes médicos".
- The section heading carries its id on a single breakpoint instance to
  clear a duplicate-id-aria violation; the focus hook keeps targeting
  both instances and picks the visible one.
- The in-page sub-tab strip scrolls its active tab into view on a narrow
  viewport, matching the group strip.
- Redirect /settings/layout to the Display landing now that nothing links
  to the old personalisation hub.
- Note in code that delivery channels deliberately stay under
  Integrations (a channel is a delivery provider).
2026-06-29 00:34:24 +02:00
Marc-André Bombeck e7683c9f54 fix(documents): reconcile library quality-of-life passes
- Reword the upload toasts to the store-only reality ("stored", not "read"),
  so the success message no longer claims AI ran and the panel it opens no
  longer contradicts it.
- Classify upload failures (too large / wrong type / rate-limited / invalid
  metadata) into specific messages; add a client-side 12 MB pre-check and show
  the accepted formats and size ceiling beside the file field.
- Clear filters, search, and sort on a successful upload so the new document is
  in the list the detail panel opens against, and add a "Clear filters" action
  on the no-results state.
- List approved facts read-only after review so the link between a document and
  the records it contributed stays visible instead of "Nothing left to review".
- Format date-group headers through the app's own date formatter, so they honour
  the user's date-format preference instead of a textual medium style.
- Stack the toolbar until md and let it wrap; wrap the fact badge row on narrow
  screens.
- Tie the disabled Approve to a plain-language reason, label the confidence
  percentage, and render it through the locale percent formatter.
- Treat a fully-confirmed re-extract (422) as the calm already-reviewed note,
  matching the partly-confirmed case.
- German: unify on "Angaben" for the extracted items and drop the anglicism.
2026-06-29 00:33:30 +02:00
Marc-André Bombeck f4919e9634 Merge v1.25.1: expand the biomarker reference descriptions across six locales 2026-06-28 23:59:45 +02:00
Marc-André Bombeck 92b963382e feat(labs): expand the biomarker reference descriptions
Replace the terse one-line catalog descriptions with richer three-to-four
sentence explainers for all 30 biomarkers shown on the lab detail page:
what each marker measures, its physiological role, and what it relates to
clinically. The text stays static, factual, and non-diagnostic, with no
reference ranges baked into the prose. Updated across all six locales
(de/en/es/fr/it/pl) at exact key parity.
2026-06-28 23:59:06 +02:00
Marc-André Bombeck 884323d787 fix(documents): accessibility and review-flow polish for the library UI
Tighten the documents library review surface after a QA pass:

- Surface the provider-unsupported and already-confirmed notes as polite
  live regions (role=status / aria-live) so keyboard and screen-reader
  users hear them when the suppressed-toast paths fire.
- Render a document's detail panel directly after the row that opened it,
  and tie the row's aria-controls to the panel id, fixing reading and
  tab order.
- Gate Discard behind a confirm dialog and toast on failure.
- Disable Upload until a file is chosen, with an inline hint.
- Namespace per-fact editor field ids so simultaneously-open editors no
  longer share DOM ids (label-for associations stay correct).
- Hide Extract once any fact is approved, mirroring the server guard, and
  treat the already-confirmed response as a calm inline note.
- Add a document-date range filter, a kind fallback for the detail title,
  and a screen-reader hint on the new-tab original link.
- Calmer, neutral needs-review badge and more legible row badges.

Adds the new strings across all six locales.
2026-06-28 23:47:23 +02:00
Marc-André Bombeck c6a7de8f2a feat(documents): build the Documents library UI
Evolve the inbound-documents screen into a real document library on top of
the store-first backend.

- Upload stays store-only and always works: the form now files a document
  with an optional title, category, and document date in one step. Extraction
  is a separate, explicit action.
- List becomes an organized library: documents grouped by date, with a title
  (filename fallback), category and status badges, and pending/fact counts. A
  toolbar adds title/filename search, a category filter, and a sort control
  (document date / date added / title, asc/desc). Keyset pagination loads more
  via the returned cursor through useInfiniteQuery.
- Detail panel: view or download the original, edit metadata (rename,
  recategorise, set the document date), optionally run extraction, then the
  existing review-then-confirm flow. When no scan provider is configured,
  extraction surfaces a calm inline note instead of a hard error; the stored
  document is untouched. Discard stays.
- Add a parameterised list key to the query-key factory so each filter/sort
  combination keeps its own cache bucket while staying under the documents
  prefix for invalidation.
- Rename the surface to "Documents" across all six locales (title, subtitle,
  module label/description) and add the new toolbar, edit, extract, and
  date-group strings. Route, model, and module key stay internal.

Add component and unit coverage for the grouped list, the detail/review
surface, the metadata edit form, the provider-unsupported note, and the
search-param/grouping/error-classification helpers.
2026-06-28 23:26:01 +02:00
Marc-André Bombeck 174bee298e Merge v1.25.1: consolidate Settings into nine groups with in-page sub-tabs 2026-06-28 23:01:14 +02:00
Marc-André Bombeck 18c737d620 feat(settings): consolidate the settings nav into nine groups
The settings left rail rendered one entry per section — 26 visible — which
read as a long flat list on desktop and a 26-chip scroll strip on mobile.
Group the sections into nine top-level entries and surface each group's
children as in-page sub-tabs, keeping the rail one level deep and the second
level inside the page.

Every section route stays live at its own URL. The rail is now driven by a
SETTINGS_GROUPS model; a reverse index maps each child slug onto its group so
deep links and the per-page settings links keep resolving and highlight the
right group, with the active child marked on the sub-tab strip. This
generalises the existing hidden-child highlight mechanism rather than moving
routes, so bookmarks and the contract surface are untouched.

Groups: Account (profile, sign-in), Tracking (areas, the per-domain pages,
targets), Display (dashboard, insights), Integrations (connections, channels,
source priority), Notifications, AI & Coach, API & Access, Data & Privacy
(export, health record, sharing, privacy, advanced), About. Per-domain
settings keep their view and data together inside one sub-tab — only the nav
groups, never a single concept.

Rename the Display group title away from "Dashboard" so it no longer collides
with its own Dashboard child. Add the nine group labels and the sub-section
nav label across all six locales. Drop the now-unused hub back-link.
2026-06-28 23:00:35 +02:00
Marc-André Bombeck 4af13a24e4 Merge v1.25.1: fold the inbound-document disclaimer into the page subtitle
# Conflicts:
#	src/components/documents/inbound-documents-view.tsx
2026-06-28 21:48:48 +02:00
Marc-André Bombeck d7fd3ea9b4 Merge v1.25.1: drop the repeated inline disclaimers from medication, insights, and labs 2026-06-28 21:48:21 +02:00
Marc-André Bombeck adb7892b97 fix(ui): drop the repeated inline disclaimers from medication, insights, and labs
The layered self-description standard already carries the canonical
intended-purpose line, the Coach standing note, the emergency note and
the data-posture note. The medication, insights and labs surfaces each
repeated a further context-specific disclaimer footer that added noise
without adding meaning.

Remove the three rendered footers and retire the now-unused
`insights` / `labs` / `medications` disclaimer variants from the shared
component (type, key map and doc). Drop the orphaned selfDescription
keys from all six locale bundles so call-site and reverse i18n coverage
stay in parity.
2026-06-28 21:39:40 +02:00
Marc-André Bombeck c10eae2c4e fix(documents): fold the inbound-document disclaimer into the page subtitle
Remove the standalone amber disclaimer card from the inbound-documents
review screen. The non-diagnostic essence now reads as the plain muted
subtitle line under the page title: upload a clinical document and review
the extracted details. Drop the now-unused disclaimer.title /
disclaimer.body keys across all six locales and the AlertTriangle /
CardDescription imports left orphaned by the removal.
2026-06-28 21:35:30 +02:00
Marc-André Bombeck 6b7e6f3332 Merge v1.25.1: wellbeing check-in as a standalone module page 2026-06-28 21:30:53 +02:00
Marc-André Bombeck 0109fbbc4c Merge v1.25.1: biomarker detail mirrors the Insights metric sub-page 2026-06-28 21:30:48 +02:00
Marc-André Bombeck 883b6bb5e6 Merge v1.25.1: inbound original-document view/download + age-stratified grip/waist bands 2026-06-28 21:28:23 +02:00
Marc-André Bombeck 37c977e3a4 feat(mental-health): make the wellbeing check-in a standalone module page
Move the PHQ-9 / GAD-7 screener off `/insights/mental-wellbeing` to a
top-level `/mental-wellbeing` route so it no longer borrows the Insights
tab strip and layout shell. The page now uses the standard module-page
frame at the wider content width the other module pages use.

Replace the standing screening disclaimer block and the opt-in/private
line in the header with an info icon after the page heading whose
hover/focus tooltip carries the screening-not-a-diagnosis note. Drop the
now-orphaned `disclaimer` / `optInNote` strings across all six locales
and add the `infoTooltip` string in their place.

The questionnaire, scoring, severity result, PHQ-9 item-9 crisis-resource
handling, focus-on-submit, and 44px touch targets are unchanged. Repoint
the module nav entry and the registry comments at the new route.
2026-06-28 20:48:39 +02:00
Marc-André Bombeck ad43eaeff0 Mirror the metric sub-page layout on the biomarker detail
Bring the lab biomarker detail in line with the Insights metric pages:

- Lead with the static catalog description directly beneath the heading,
  the explainer-caption position the metric pages use.
- Drop the early big-value + reference-badge block that did not match the
  assessment-card styling.
- Split numeric and qualitative markers: numeric keeps the stat strip and
  chart, qualitative shows only the latest result text (no strip, no chart,
  no range badge). The assessment card now renders for any marker with
  readings and self-suppresses for qualitative-only markers.
- Move the full reading history off the detail page onto a dedicated
  /labs/[biomarkerId]/values route, reached by a top-right list control,
  matching the metric pages' show-all-values affordance.
2026-06-28 20:37:01 +02:00
Marc-André Bombeck 08314c7c09 feat(documents): view and download the original inbound document
The inbound-document store keeps the uploaded doctor letter encrypted at
rest (InboundDocument.contentEncrypted, AES-256-GCM), but there was no way
to read the original back — the decrypt helper had been dropped as unused.

- Restore decryptDocumentFromBytes in the document store, fail-closed on a
  bad / missing key id.
- Add GET /api/documents/inbound/[id]/original: owner-scoped (userId-narrowed
  query), module-gated, returns the decrypted bytes with the stored MIME
  type. A PDF / image is served inline so the browser can render it; other
  types download as an attachment. The filename is sanitised before it
  reaches the Content-Disposition header. The bytes are PHI — never logged,
  never served cross-user, and a decrypt error fails closed (500) rather than
  returning ciphertext. Cache-Control is private, no-store.
- Surface a 'View original' action in the document review screen.
- Document the route in the OpenAPI registry and regenerate the spec.
- Cover the route with a test: owner-scoped success, attachment fallback,
  404 for another user's document, and fail-closed on decrypt error.

i18n: viewOriginal across all six locales.
2026-06-28 20:33:29 +02:00
Marc-André Bombeck d3967bcdd0 Surface pre-existing conditions in the medical-history section
The chronic-conditions and Coach-focus self-context read as foundational
medical history, so the Anamnese section now surfaces them as a read-only
shared view next to allergies and family history, with a link to their single
editing home in personal context. Personal context links back to the medical
history. The card is coach-gated and reads through the existing
/api/coach/about-me query key, so nothing is moved or duplicated and there is
no schema change.
2026-06-28 20:29:21 +02:00
Marc-André Bombeck 3d304acf1e merge: QA fix polish into release/v1.25.0 2026-06-28 18:36:56 +02:00
Marc-André Bombeck 8e4c107a18 merge: QA fix mental-health into release/v1.25.0 2026-06-28 18:36:26 +02:00
Marc-André Bombeck a69697b50f fix(insights): wire grip / pain / waist / waist-to-height across web and MCP
Grip strength, pain (0–10 NRS), waist circumference, and waist-to-height
existed in the measurement enum, signal registry, and FHIR export but had
no web entry, no detail page, single non-sex-aware bands, and a split-brain
MCP exposure. This closes those gaps.

Web capture: the four signals join the manual measurement form, so a
web-only self-hoster can log them (the iOS client and the measurements API
already could).

Detail pages: flip detailPage on for the four signals and mount each on the
shared HealthKit metric template (chart, stat strip, Coach-read strip,
assessment card) via routed sub-pages and tab-strip pills, the same detail
spine every other metric carries.

Sex-aware bands: grip strength (EWGSOP2 men 27 / women 16 kg) and waist
(WHO men 94 / women 80 cm) now classify by the profile sex through the
age/sex norm resolver the assessment already threads; the resolver also
returns a sex-specific, age-independent band when no date of birth is set.
Fix the waist-to-height boundary so the NICE >= 0.5 rule is correct (the
last in-range value is 0.49).

MCP single source of truth: decouple the registry mcp facet from the Coach
snapshot — a signal can be MCP-readable without being on the Coach surface.
Mark the four physical signals mcp:true, derive the MCP rich-reads clinical
set from the registry flag instead of a duplicate allowlist, and relax the
invariant to coachSnapshot implies mcp (not the reverse). PHQ-9 / GAD-7 and
the environmental signals stay mcp:false and unreachable. compare_metric and
detect_changepoints now advertise clinical-signal support to match
get_metric_baseline.
2026-06-28 18:33:53 +02:00
Marc-André Bombeck dd6b0867a4 i18n: translate illness journal strings and add v1.25 UI strings
The illness journal copy (load error, delete confirmation, edit/menu
labels, parent links, and the vital-sign labels) shipped in English in
every locale; translate it into de/es/fr/it/pl. Also add the new strings
the v1.25 surfaces introduce — the biomarker 'load more readings' action,
the environment city-search zero-result message, and an accessible label
for the document upload file input — across all six locales.
2026-06-28 18:29:26 +02:00
Marc-André Bombeck 15e740bc2b i18n(mental-health): ship validated PHQ-9/GAD-7 translations and localize crisis copy
The PHQ-9/GAD-7 item wordings rendered in English for es/fr/it/pl users
(de was already validated). Replace them with the officially validated,
public-domain Pfizer/PRIME-MD localized screener wordings (distributed via
the MAPI Research Trust) rather than ad-hoc translations — re-wording a
validated instrument invalidates the score. Localize the crisis-resource
copy too: translate the descriptive parentheticals while keeping the org
proper-nouns (TelefonSeelsorge, Nummer gegen Kummer, krisenchat,
Find a Helpline) and the US brand lines (988, Crisis Text Line) verbatim.

Add the new mental-wellbeing module label/description and the nav label
across all six locales.
2026-06-28 18:25:16 +02:00
Marc-André Bombeck 4f39058629 Merge branch 'feat/v125-env-location-conservative' into release/v1.25.0 2026-06-28 17:47:35 +02:00
Marc-André Bombeck 3bd52a322b feat(self-description): add the non-diagnostic / intended-purpose standard
Introduce one canonical "what this is / is not, not a medical device"
statement plus a layered, calm disclaimer set, and apply it on the
surfaces that earn a context note.

- New shared `MedicalDisclaimer` component renders the copy as plain
  React text (no markdown renderer, per the XSS posture). Variants:
  intendedPurpose, coach, insights, labs, medications, emergency,
  dataPosture.
- New `selfDescription` i18n namespace across all six locales: the
  intended-purpose line, the per-surface notes, the emergency note, and
  an explicit data-posture statement (self-hosted, data stays on your
  instance, encrypted at rest, BYOK / local model, no third-party
  analytics).
- Apply once per surface, layered not plastered: a calm standing line on
  the Coach new-chat hero, one framing line in the Insights layout shell
  (covers the briefing and trends), a labs note (ranges vary, only a
  clinician interprets), a neutral medications note, and the data-posture
  line where the provider key is configured. Mental-wellbeing keeps its
  existing crisis copy; records keep their existing note.
- Extend the Coach system prompt (EN + DE) with a self-description ground
  rule so the Coach answers "what are you / where does my data go"
  honestly and stays non-diagnostic.

App-authored copy was already observational; the few diagnose/treat
tokens are negations, idioms, or framed as the user's own self-reported
statement, so no softening was required.
2026-06-28 17:43:31 +02:00
Marc-André Bombeck bacd591dee feat(environment): frame overrides as location periods and surface effective-from
Settings → Environment now reads the override list as "location periods": a place
plus a date range that covers a trip OR corrects a stretch of earlier history,
overriding the home for the days it spans. Show the home's effective-from date
and prefill the backfill start with it, so the default span matches what the
server will actually store. Strings updated across all six locales.
2026-06-28 17:36:21 +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 a1b4f0d00e Merge feat/v125-environmental into release/v1.25.0
Environmental context module (migration 0217).
2026-06-28 17:02:36 +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 33bf952d9d feat(documents): OpenAPI contract and translations for inbound documents
Document the /api/documents/inbound/* surface in the OpenAPI registry and
regenerate the spec. Add the inbound-documents copy across all six locales,
including the non-diagnostic disclaimer and the module label/description.
2026-06-28 16:57:24 +02:00
Marc-André Bombeck e216a9d678 feat(coach): warm, localized proactive nudge with opt-in AI enrichment
Rework the proactive Coach nudge so it reads like one human checking in on
another instead of a terse English alert.

Copy: the title is a name-led greeting that rotates day to day ("Guten
Morgen, …" / "Hey …"; the 05:15 frame makes a morning opener safe) and the
body is one calm observation plus a gentle invite — never an imperative.
Drop the verbatim quoting of the user's decrypted self-context sentence:
the focus is referenced only by presence, abstractly, and is no longer
decrypted at all. Remove the 80-char mid-word clamp; each channel composes
to a length budget instead.

Cadence: suppress the nudge when the user already talked to the Coach in
the last ~24 h. With the existing rolling frequency cap (never two in a
window) and the no-trigger-no-nudge rule, the surface stays anti-nag.

AI enrichment (opt-in via the coach prefs blob, default off): when a
provider is healthy the body is composed through the model under hard
guards — a per-user budget gate, a tight per-call timeout, a per-tick
ceiling so the sequential tick can't stall across the user base, and the
outbound safety screen on the output. Any error, timeout, budget refusal
or unsafe reply falls silently back to the deterministic template, which is
always the default and the fail-closed fallback.

Localized greetings, per-trigger bodies and the focus note across all six
locales; the prefs opt-in is surfaced in the notification settings.
2026-06-28 16:50:49 +02:00