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.
Default documentDate at store time. A store-only upload no longer lands
with a NULL filing date: when the client omits one, default it to the
upload day so the column is always set on a stored row. The library list
sorts, groups, and range-filters on the same effective date the UI shows,
so undated uploads no longer mis-sort under nulls-last, break date-group
headers, or fall through the range filter. The date stays user-editable
and may be cleared back to NULL via PATCH.
Block re-extraction once any fact is APPROVED. A partially-confirmed
document stays EXTRACTED, which the CONFIRMED guard does not catch;
re-extracting would delete the approved facts (severing the committed
record's provenance) and re-stage them as PENDING, letting a second
approval duplicate the committed lab / condition / medication. The
extract route now refuses with 409 (documents.inbound.alreadyPartlyConfirmed)
when approved facts exist; the user must finish reviewing or discard first.
RFC 5987-encode the original-download filename. The Content-Disposition
now carries both an ASCII filename= fallback and a filename*=UTF-8'' value
so a non-ASCII name survives intact; the stored set is inline-safe by
construction, so the dead attachment branch is removed.
Add a light per-user rate-limit backstop to the original-read, PATCH, and
DELETE routes (generous; self-DoS only — the routes are owner-scoped).
Lean the list counts: tally facts for the whole page in one grouped count
instead of materialising fact-id rows, and exclude REJECTED facts from
factCount. Extend the documentDate index with deleted_at to cover the
list scan. Document the deliberate plaintext-title PHI tradeoff at the
column and the store site.
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.
Login only wiped the in-memory query cache; the IndexedDB query snapshot
and the service-worker data/page caches from the previous account stayed
on disk. Logout and the 401/expiry redirect already drop all three. Run
the same offline-cache wipe on a successful login too, before the new
user's dashboard prefetch warms, so a fresh sign-in on a shared browser
inherits nothing from the prior session. The per-account restore guard
still stands; this removes the stale data at the source.
Settings shell: move focus to the section heading when the active section
changes within the mounted shell, so keyboard and screen-reader users are
placed in the new content instead of being left on the nav link they
activated. The first mount is skipped to avoid an unexpected focus move on
a deep link. Raise the inactive sub-tab text from muted-foreground to
foreground/70 so it clears the WCAG AA contrast floor over the tinted
sub-tab strip.
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.
Invert the inbound-documents surface from an OCR inbox into a document
library: a file can be stored without any AI provider, and extraction
becomes an opt-in action on top.
- Migration 0222: add `title` + `document_date` columns, a
`(user_id, document_date)` index, the richer kind taxonomy
(lab/imaging/prescription/insurance/vaccination), and a STORED status
made the column default. The status enum is recreated so the new value
can be set as the default within the migration transaction; kind values
use ADD VALUE. Nullable columns, no backfill.
- POST /api/documents/inbound is now store-only: bounded multipart read,
magic-byte MIME sniff, AES-256-GCM at rest, persisted as STORED with no
provider/consent/budget call and no egress.
- POST /api/documents/inbound/[id]/extract carries the relocated
extraction gauntlet (resolve provider, consent, rate-limit, budget, run,
reconcile, stage facts) against an already-stored row; absent a provider
it 422s the enhancement only and leaves the stored document intact.
- PATCH /api/documents/inbound/[id] edits title/kind/documentDate
field-by-field, owner-scoped by session userId.
- GET /api/documents/inbound gains search, kind filter, documentDate
range, sort, and keyset pagination via the parameter-bound query builder.
- Extend the validation schemas + DTO, regenerate the OpenAPI contract,
and add route tests for the store/extract/edit/list paths.
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.
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.