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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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).
- 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.
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.