3236 Commits
Author SHA1 Message Date
Marc-André Bombeck f207f1a7d9 Merge pull request #392 from MBombeck/release/v1.25.10
release: v1.25.10 — mobile add-form fix
v1.25.10
2026-06-30 10:41:17 +02:00
Marc-André Bombeck 9bae4dd2f6 fix(ui): stack the date-time field on narrow viewports
A native date input keeps its intrinsic width on mobile WebKit and won't
shrink, so the `date + fixed-width time` row overran a phone-width sheet —
the time field and its clock icon spilled past the screen edge and every
add form (measurements, mood, medication, …) scrolled sideways. Stack the
two halves on narrow viewports and only place them side-by-side from the
`sm` breakpoint up; each half then takes the full sheet width and nothing
overflows.
2026-06-30 10:33:16 +02:00
Marc-André Bombeck 9bba6204b4 Merge pull request #391 from MBombeck/release/v1.25.9
release: v1.25.9 — custom metrics
v1.25.9
2026-06-30 09:57:35 +02:00
Marc-André Bombeck 3d75db917e fix(settings): keep the section nav hydration-stable
The settings section navigation filtered its entries by the resolved
`user.modules` map directly in render. That map resolves at different
points on the server-rendered pass and the hydrating client pass, so the
two renders emitted a different number of nav entries — a hydration
mismatch that made React discard and regenerate the tree, leaving every
control on the page non-interactive (a disclosure that wouldn't open, an
export button that did nothing).

Emit the full fail-open list on both the server pass and the first client
paint (gated behind a `useSyncExternalStore` hydration probe), then apply
the module filter once, after hydration. Server and first client render
now always match, so the nav — and the rest of the settings page — stays
interactive.
2026-06-30 09:49:09 +02:00
Marc-André Bombeck c306d42347 chore(release): v1.25.9 — custom metrics
Renumber the custom-metrics migration to 0223 (the next free slot on the
trunk) and cut the release: version bump, regenerated OpenAPI contract,
service-worker stamp, and CHANGELOG entry.
2026-06-30 08:59:44 +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 150b827d01 Merge pull request #390 from MBombeck/release/v1.25.8
release: v1.25.8 — carrier in the sign-in overview
v1.25.8
2026-06-30 00:08:24 +02:00
Marc-André Bombeck 9b097b0553 test(audit): type the geo fixture so strict tsc accepts a non-null location 2026-06-30 00:00:24 +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 c14d760654 Merge pull request #389 from MBombeck/release/v1.25.7
Release v1.25.7 — settings, tidied
v1.25.7
2026-06-29 22:51:22 +02:00
Marc-André Bombeck b6886c50d2 chore(release): v1.25.7 2026-06-29 22:42:50 +02:00
Marc-André Bombeck cce0dd9414 refactor(settings): drop the no-op content-column scrollbar gutter
The authenticated shell's <main> scroll container already reserves the
scrollbar gutter; the settings content column is not a scroll container, so a
gutter rule on it has no effect. Remove it (and its misleading comment) rather
than imply a fix that does nothing.
2026-06-29 22:21:06 +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 8cd28ffd3f Merge pull request #388 from MBombeck/release/v1.25.6
Release v1.25.6 — optional HTTP geolocation provider
v1.25.6
2026-06-29 20:46:42 +02:00
Marc-André Bombeck e51c6dc2ad feat(geo): allow an opt-in plain-HTTP location provider
A self-hoster can set IP_GEO_ALLOW_INSECURE=true alongside an http:// URL to
use a free HTTP-only provider — the free ip-api.com endpoint is HTTP-only (its
HTTPS form needs a paid key) but its geolocation is often more accurate and it
carries the ISP. The default stays HTTPS-only with ipwho.is; plain HTTP is
refused unless the operator explicitly opts in, since the looked-up IP then
travels unencrypted over the server's own egress. Whitelisted in compose and
documented in the env example.
2026-06-29 20:38:04 +02:00
Marc-André Bombeck fbb88b81bc Merge pull request #387 from MBombeck/release/v1.25.5
Release v1.25.5 — mental-wellbeing polish, location lookup
v1.25.5
2026-06-29 20:30:43 +02:00
Marc-André Bombeck e91a28fade test(e2e): skip the download-event assertions under mobile emulation
Playwright's mobile project (Pixel 5 / isMobile) treats a Blob + anchor
click as a navigation and never emits a "download" event, so the two
settings-export download tests time out only on chromium-mobile while
passing on chromium-desktop. Skip them on mobile — the download wiring is
engine behaviour, not viewport-dependent, and stays covered on desktop.
2026-06-29 20:22:28 +02:00
Marc-André Bombeck c275683e42 chore(release): v1.25.5 2026-06-29 20:04:25 +02:00
Marc-André Bombeck abb4c71d0c feat(geo): make ipwho.is the default IP-location provider again
ipwho.is is free, needs no key, and resolves a city/country for the login
overview out of the box. It is the default online lookup again; the offline
GeoLite2 MMDB tier stays strictly optional (consulted only on an online miss,
skipped when the databases are absent). Operators whose egress ipwho.is
rejects can point IP_GEO_LOOKUP_URL at ip-api.com — the parser accepts both
response shapes, so the swap needs no code change.
2026-06-29 20:00:53 +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 23f803f982 Merge pull request #386 from MBombeck/release/v1.25.4
Release v1.25.4 — time picker honors the 24-hour preference
v1.25.4
2026-06-29 19:24:47 +02:00
Marc-André Bombeck 2c701e121e fix(measurements): keep blood-pressure field labels on one line
In the three-column blood-pressure entry grid the "Systolic (mmHg)" /
"Diastolic (mmHg)" / "Pulse (bpm)" labels wrapped onto a second line in
narrow columns. Render the localised label as the metric name (which
truncates if it must) plus the unit as a compact muted affix that never
wraps, so each field reads on a single line while keeping its unit.
2026-06-29 19:09:17 +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 426b21497a Merge pull request #384 from MBombeck/release/v1.25.3
release: v1.25.3 — Mental wellbeing, labs depth, time-format + tile fixes
v1.25.3
2026-06-29 16:11:10 +02:00
Marc-André Bombeck 00ea61ee9f chore(release): v1.25.3 2026-06-29 15:53:41 +02:00
Marc-André Bombeck d0eced41f9 Merge v1.25.3: honor the 24h time-format preference app-wide 2026-06-29 15:49:52 +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 0d45d0ab53 fix(format): honour the time-format preference on every clock renderer
The H24 / H12 preference only reached makeFormatters().time. Surfaces that
build their own Intl.DateTimeFormat — the sleep hypnogram axis, the workout
list and detail headers — omitted the hour cycle, so an en-locale (or AUTO)
user saw AM/PM even with H24 selected. The security activity feed and session
list rendered raw toLocaleString(), bypassing both the locale and the
preference entirely.

Export hourCycleOptions from format-locale as the single source for the
H12 / H24 / AUTO Intl options and spread it into the self-built formatters,
preserving each surface's locale and timezone. Route the security cards
through useFormatters().dateTime. Drop the date-time field's local copy of
the helper.

Guards: the helper contract, the en-US hour/minute call-site pattern (no
AM/PM under H24, AM/PM under H12), and formatUpdatedLabel fed an H24
formatter for the briefing freshness label.
2026-06-29 15:47:48 +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 a333776725 Merge v1.25.3: v1253-disable-documents 2026-06-29 15:30:32 +02:00
Marc-André Bombeck 9ede7575cf Merge v1.25.3: v1253-labs-detail 2026-06-29 15:30:32 +02:00
Marc-André Bombeck 0e21210263 Merge v1.25.3: v1253-insights-ui 2026-06-29 15:30:32 +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 7e83af5701 fix(modules): switch the inbound-documents module off in code
The inbound-documents surface is withdrawn from this release and returns
later with more depth. Park its module key (`inboundDocuments`) in a new
`CODE_DISABLED_MODULE_KEYS` list so it is hard-off for every account ahead
of both the operator-availability and the per-user opt-in layer.

The gate short-circuits a code-disabled key to disabled, so the resolved
module map reports it off everywhere: the nav entry stays born-gated and
hidden, the `/documents` page redirects, and every `/api/documents/inbound/*`
route returns the 403 module-disabled envelope. The Modules hub and the
operator-availability panel drop the row, and the per-user PATCH surface
refuses to write the key (a clean 422 instead of an inert preference).

The whole implementation stays intact — routes, the inbound-documents view,
the registry entry with its opt-in marker, the validation schemas, and the
migration are all preserved. Re-enabling the module is a one-line removal
from the list once the rebuild lands.

Regenerate the OpenAPI contract: the module-preferences PATCH body no
longer advertises the parked key.
2026-06-29 14:58:30 +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 38629d41ed fix(insights): repair the score-anatomy layout and align Coach affordances
The readiness/recovery score-anatomy detail card rendered broken: the
contributor factors were obscured and the method text overlapped the
title and ring on every breakpoint. The provenance explainer no longer
ships a collapsible info glyph; it renders its full method body inline,
but the anatomy view still wrapped it in an absolute top-right box, which
pulled the whole paragraph out of flow and painted it over the centred
title, ring and contributor rows. Render the method caption in normal
flow at the foot of the card so the factors read again and nothing
overlaps.

Move the weekly/monthly review Coach hand-off to the icon-only affordance
pinned top-right, matching the assessment cards instead of a trailing
text button.

Widen the Coach conversation-history page to the standard content width
so the conversation list no longer sits in an artificially narrow column.
2026-06-29 14:53:53 +02:00
Marc-André Bombeck 4266203d3a Merge pull request #383 from MBombeck/release/v1.25.2
release: v1.25.2 — Flat settings navigation
v1.25.2
2026-06-29 07:31:55 +02:00
Marc-André Bombeck b84cdba6e0 chore(release): v1.25.2 2026-06-29 07:16:21 +02:00
Marc-André Bombeck ad05b074b3 Merge v1.25.2: restore the flat settings navigation 2026-06-29 07:12:48 +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 7bd26ff2bd Merge pull request #382 from MBombeck/release/v1.25.1
release: v1.25.1 — Documents library, tidier settings
v1.25.1
2026-06-29 01:16:39 +02:00
Marc-André Bombeck 7c2e11369c chore(release): v1.25.1 2026-06-29 01:07:42 +02:00
Marc-André Bombeck a58fc2e0e2 Merge v1.25.1: final cleanup (surface confirm failures, German term unification, upload reveal, list error state) 2026-06-29 01:04:18 +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 86d1d5b726 Merge v1.25.1: documents deep-round QoL (store-framed copy, error classification, recoverability, date format, overflow) 2026-06-29 00:38:25 +02:00