Commit Graph
1996 Commits
Author SHA1 Message Date
Marc-André Bombeck d33be187e6 Copy polish: one pause dash, du-imperative errors, bare-question confirms, tokened micro-text
The German bundle's 109 spaced en-dash pauses join the em-dash norm (the
two date-range placeholders stay ranges); the three straight-quote
stragglers and three mixed-quote strings take German quotes; the eleven
Bitte-Infinitiv validation errors speak du-imperative like the rest of
the app; the two wirklich confirms become bare questions; the
notification-status description trades its semicolon telegram style for
two sentences in every locale; the Withings setup action shortens to the
sibling single word. Twelve arbitrary text-[10px]/[11px] sites move onto
the text-2xs/text-xs tokens; chart tooltips and the vendor calendar stay
as they are.
2026-08-14 10:08:49 +02:00
Marc-André Bombeck 9d64eb6aa2 Show what a backup contains before the operator types RESTORE
The restore dialog asked for its typed confirmation on nothing but a
filename and a date; the counts only surfaced in the audit row after the
rows were already replaced. A read-only summary endpoint now decrypts and
schema-validates the stored file through the same helpers the restore
itself runs and answers the summarizeBackup counts, and the dialog shows
them — per section, with an other-records line so a future summary key
can never silently vanish from the preview. A failed preview states its
own absence and never blocks the restore.
2026-08-14 09:51:40 +02:00
Marc-André Bombeck 90bad17cc8 Show the answers behind a screening score
Refs A3-8. The per-item PHQ-9 / GAD-7 / WHO-5 / SCI answers have been
stored encrypted since the assessments shipped, and nothing ever read
them back — the clinical detail (item 9 included) was invisible. A new
by-id read decrypts them for whoever holds the mind read grant (the same
grant that already serves the mood diary's prose and the item-9 flag),
degrades to an honest items-unavailable state on any decrypt or shape
problem, and the history rows gain an expandable breakdown rendering the
official instrument texts the check-in flow already carries — nothing
retranslated, no alarm styling on item 9 beyond what the house already
does.

Rides along: the guard inventories learn the branch's new routes (frozen
counts moved with reasons), the reminder-engine test harnesses carry the
ledger delegate, and the custom-metric restore toast keys sit in their
right namespace in all six locales.
2026-08-14 09:46:48 +02:00
Marc-André Bombeck 651d37b494 Let the dashboard hero show what matters most to its owner
The hero card composition was fixed: score panel first, the worth-a-look
rail beneath. A reminder-driven person asked for the reverse. The stored
dashboard layout gains an optional hero field ('score' default,
'reminders' promotes the rail into the hero slot), validated as a closed
enum, preserve-when-absent on write, omitted from the blob at the default
so untouched accounts serialize byte-identically. The choice sits in
Settings beside the other hero controls; the promoted rail degrades to
the calm all-clear line when nothing is due.
2026-08-14 09:46:35 +02:00
Marc-André Bombeck a3561b4600 Tell a first-run what Apple Health is actually doing
Refs #778. Two honesty gaps from the first-run report: the web onboarding
presented Apple Health like a web connector although the connection only
completes in the iPhone app, and the initial backfill gave no sign of
progress, so a working first sync looked broken.

The onboarding source step now says plainly that Apple Health is set up in
the iPhone app after onboarding, without dead-ending the flow. The
integration card gains a server-authoritative progress summary — rows
accepted, oldest reading reached, and an honest flowing/waiting line
derived from the last accepted batch. Nothing the server does not know is
shown: no percentage, no ETA, no invented throttle state; a quiet stretch
reads as waiting for the phone, not as an error.
2026-08-14 09:46:24 +02:00
Marc-André Bombeck 7bf8dbd8af Give custom-metric values the same second chance as every other entry
Refs A3-11. Deleting a logged custom-metric value was the one hard delete
left on an entry surface — everywhere else a deletion tombstones and the
toast offers Undo. The DELETE verb now stamps deletedAt, every read
(list, latest-value, counts, revive check) filters the tombstone, and the
new entries/restore endpoint clears it scoped to owner and parent metric —
a foreign, live or mismatched id is a silent no-op, never an existence
leak. The history table's delete toast carries the Undo action.
2026-08-14 09:34:43 +02:00
Marc-André Bombeck 689cacc039 Keep the measurements and mood filters in the URL
The documents view already round-trips its filters through the query
string; the measurements and mood lists kept theirs in memory, so any
navigation dropped them. The documents pattern is extracted into a shared
hook (use-url-filter-sync) and both lists adopt it: type/source/day-bounds
and the measurement value range survive reload, back and sharing, defaults
stay out of the URL, invalid facets drop leniently. The insights readings
subpage keeps its locked type in memory — that URL belongs to the insights
route. /measurements no longer strips ?type=, so the deep links the
Vorsorge cards mint now persist as filter state.
2026-08-14 09:24:08 +02:00
Marc-André Bombeck 5619783648 Let a checkup be skipped or pushed back, and remember every outcome
Refs #223. A due Vorsorge item could only be done, deleted or ignored;
Telegram's postpone moved the date with no trace. Both decisions are
first-class now, and both are honest:

- POST /{id}/skip (no body): the interval restarts from the skip instant
  through the recurrence engine, lastSkippedAt is stamped, skipCount
  increments, any snooze clears. lastSatisfiedAt is NEVER touched — done
  and skipped stay distinguishable forever, enforced in the primitive and
  pinned by tests.
- POST /{id}/snooze ({until: YYYY-MM-DD}): the server resolves the day to
  the notifyHour in the profile timezone and moves snoozedUntil and
  nextDueAt to the same instant, so every due-state consumer follows
  without knowing snooze exists. At least tomorrow, at most five years.
- GET /{id}/history: the new append-only completion ledger
  (MeasurementReminderEvent), one row per satisfy from every engine path
  and per skip, onTime derived at write time against the due instant that
  was current when the event landed.

Two anchor fixes close the backdated-event gaps: the auto-resolve floor is
now the later of last satisfy and last skip (an old reading synced late no
longer resolves the cycle the user just skipped), and a backdated real
result re-anchors the next due at the later of its own instant and the
skip decision.

Screenings stay skippable and snoozable — the satisfy-side 409 guards
claimed fulfilment; a skip claims the opposite. Appointment reminders 404
on all three routes, in the lookup, like every by-id sibling. /checkups
gains the postpone sheet (quick chips, date field, the skip zone with its
own confirm) in both view branches, with muted meta lines and no card
colour change. The MCP preventive-care tool carries the resolved fields;
the OpenAPI contract publishes all three paths and the widened DTO.
2026-08-14 09:23:58 +02:00
Marc-André Bombeck cd12b3886e Say what the stage tooltip's total actually is
Refs #611. The nap separation itself shipped in v1.34.4; what remained of
the report was the tooltip footer labelling a single night's total with the
orphaned 'Average per night' key (the headline card that owned it was
dropped in v1.22.0, so hovering one column claimed an average that was that
night's sum). The footer now names the main session's sleep, and on a day
with an inferred nap adds the whole-day total beside it — the Main sleep /
Naps / Total triple the report asked for. Percentages keep dividing by the
night alone.
2026-08-14 09:23:39 +02:00
Marc-André Bombeck d2ff7cb769 Settle the suite-wide fallout of the wave
- The record-settings contract, slug-list, and section-frame tests
  follow the slugs that left the registry (layout children resolve
  their headings from the layout namespace, like production).
- The orphaned settings.sections.<module>.title/description keys leave
  all six locales; the reverse-coverage guard flagged them the moment
  their nav entries stopped existing.
- The operator-cap ledger read fails open on a read error — the
  reservation layer still meters against the same ledger, and a
  transient blip must not take the fallback chain down.
- The reaper queue joins the subsystem surface; its healed count and
  the sweep's nag_dismissed join the job-fact vocabulary.
- The intake-event contract fixture carries the full row the resource
  now declares; the workouts empty-state test asserts the
  source-neutral cue; the persister poll gates on rendered content
  first.
2026-08-14 06:37:30 +02:00
Marc-André Bombeck 7611c82ae7 Poll on real conditions in the e2e suite and un-age the expiry fixtures
- account-sharing polls the offline persister's own IndexedDB store for
  the flushed snapshot instead of sleeping past the debounce.
- The record-session-fence cases poll for the observable halves of their
  setups (the switch round-trip landing, the exit switch being issued);
  the one remaining fixed window is a documented absence-assertion grace
  with nothing to poll on.
- The documents suite's two remaining sleeps are named for what they
  are: a focus-dance grace the outer document cannot observe, and the
  long-press gesture duration itself.
- The inventory-date fixtures derive their printed expiry from now
  instead of a fixed 2027 date that would silently flip the fixture to
  expired when the calendar catches up.
2026-08-14 06:17:34 +02:00
Marc-André Bombeck 4b009c3a45 Publish the resolved per-slot dose units and a slot-aware runway on the medication wire
A medication whose slots carry different doses (a whole tablet in the
morning, a half at noon — #219) was consumed correctly but reported
wrongly: the wire shipped only the raw nullable per-slot column (clients
had to derive the inheritance), stockDosesRemaining divided by the
medication-level units alone, and no runway figure existed outside the
low-stock push.

- Every schedule now carries resolvedUnitsPerDose, resolved server-side
  exactly like the intake consumption resolver.
- stockDosesRemaining divides the units pool by the schedule-weighted
  average units per dose.
- runwayDays (whole days of supply under the slot-aware burn rate) rides
  the list AND detail payloads; the detail GET also gains the stock
  aggregates the list already had.
- The low-stock engine runs the same slot-aware math, so the push and
  the wire cannot disagree; the web cards, table, and detail Übersicht
  prefer the server figure over their client estimate.
- OpenAPI regenerated with the new fields.
2026-08-14 05:41:15 +02:00
Marc-André Bombeck 542ca91be5 Finish the wiring polish: verbosity gate, upload flash, /about links, dead settings slugs
- The Coach verbosity picker is disabled while the concise tone forces
  brief, with a hint saying so, instead of a live control that silently
  does nothing.
- A fresh document upload flashes its new row like the duplicate path
  always did; the settle decision is a pure exported helper with a test.
- /about gains its two inbound links: the /privacy footer and a
  Settings→About credits row (the GeoLite2 EULA wants the attribution
  reachable from the running app).
- The eight settings slugs shadowed by permanent redirects (the seven
  layout children + sharing) leave the slug registry: no more
  prerendering unreachable pages, no dead component wirings, and the
  SectionPlaceholder fallback that could never render is gone. The
  redirects, section components, and layout-hub subpages are unchanged.
2026-08-14 05:31:33 +02:00
Marc-André Bombeck 3b1314d1b0 Enforce the reminder nag cap, compare the disclaimer version, drop the unread analytics columns
- The daily Coach sweep now charges each ignored day against
  surfaceCount and auto-dismisses at three — the cap the schema comment
  promised since v1.22 but nothing enforced.
- The welcome gate compares the stored disclaimer acknowledgment
  version against the current one, so revised disclaimer text
  re-prompts; /api/auth/me carries the version for the client.
- slope90 and anomalyCount leave the analytics wire: slope90 had no
  reader anywhere (computed on every request, in SQL twice), and the
  slim path could never compute anomalyCount (it always reported 0).
  anomalyCount stays as an in-process field on summarize() for the
  insights feature builder, which is its only real consumer.
2026-08-14 05:19:05 +02:00
Marc-André Bombeck 6b3a5a4182 Settle the four remaining layout deviations from the audit
- Vorsorge dashboard tile title joins the dashboard heading ladder
  (titleAs=h2 like its sibling tiles).
- A user's own note on the mobile measurement card renders in the
  foreground colour; user data is never muted.
- The compact Vorsorge list row expresses its density on the Card
  instead of overriding the content slot's padding with an off-scale
  step.
- The AI-consent card holds its space with a skeleton while the receipt
  loads instead of popping in and shifting the section below.
2026-08-14 05:07:14 +02:00
Marc-André Bombeck 487eb6970c Polish the copy the audit flagged across all six locales
- Singular variants for the bulk-delete toast and confirm dialog
  (measurements + mood); the call sites branch on a count of one.
- Consent confirm buttons shortened so they no longer overflow the
  small button on phones.
- Workouts empty-state CTA is source-neutral instead of assuming an
  iPhone with Apple Health.
- "Pills" is no longer a navigation term in a medication app: DE says
  Navigations-Chips, EN chips, PL chipy; ES/FR/IT subtitles align with
  their own established terms.
- Health-score insufficient hint drops the translationese and fixes the
  pronoun.
- Side-effect tag helper uses the established Stimmungs-Tags term.
- Success toasts that are single fragments lose the terminal period
  (37 keys, all locales).
- Integration pill parked state short enough to survive truncation.
- The iPhone-managed reminder badge carries a short label; the full
  sentence moved to a helper line below instead of clipping mid-word.
2026-08-14 05:02:49 +02:00
Marc-André Bombeck db6465c17d Account for every dropped Apple Health sample the result used to hide
Three silent gaps in the import accounting, one commit:

The reproductive samples of an export land in recordsRead but were
discarded without a counter when cycle tracking is off — the result now
names them (samplesSkippedModuleDisabled). A cycle flush that failed
partway zeroed its stats AFTER day-by-day writes had landed; the flush
is now fault-isolated per day and reports daysFailed plus the first
failure reason instead of claiming nothing happened. And the import
card hardcoded skipped: 0 while the worker has computed, persisted and
served per-class skip counters since v1.15 — it now renders them,
splitting deliberate exclusions (unsupported and deferred types, a
disabled cycle module) from genuine refusals (unparseable, out of
range, failed writes), with a per-type breakdown. Only refusals tip
the outcome tone.
2026-08-14 02:06:19 +02:00
Marc-André Bombeck a45dde2039 Retire the guardian toggle over a pref nothing reads
The evidence disclosure stopped honouring showEvidenceByDefault in
v1.4.27 and the owner surface retired the control with it — but the
managed-record coach form still offered the switch, silently governing
nothing. Remove the control, the managed-schema pick, the response
field, and the label in all six locales; the strict schema now refuses
the key outright.
2026-08-14 01:56:25 +02:00
Marc-André Bombeck 15fb5dbbdb Point the import-guide links at the docs site that actually has them
Both import cards linked /docs/integrations/data-import — a path the
app never serves, so every click 404'd. Link the external docs site
through the shared INTEGRATION_DOCS_BASE constant, opening in a new tab
like every other integrations runbook link.
2026-08-14 01:54:10 +02:00
Marc-André Bombeck c23e677b6e Gate the bottom-nav Meds slot on the medications module
The mobile bar pinned /medications unconditionally while the page
renders nothing when the medications module is off — a visible tab over
a blank screen. Run both fixed flanks through the one slot filter the
Insights slot already uses (sharing coverage + module map, fail-closed
until mounted), so a disabled module is hidden everywhere instead of
relocated.
2026-08-14 01:52:37 +02:00
Marc-André Bombeck 11aa3dc060 Stop describing a Research-Mode gate that no longer exists
The titration-curve docblock contrasted itself against an estimated-PK
chart 'behind the Research-Mode gate' — that switch was retired as inert
and the columns dropped with migration 0321. Describe the real contract:
both charts render ungated, the estimated one carrying the persistent
modelled-not-measured disclaimer.
2026-08-14 01:48:31 +02:00
Marc-André Bombeck 61114fab90 Accelerate the chart range switch, prefetch two list pages, calm two polls
placeholderData keeps the previous range painted while a health-chart
range switch fetches, paired with its truth condition: the container
dims and goes inert while isPlaceholderData is true, so the old range
cannot masquerade as the new one. The mood chart needs no pairing — its
range is a client-side slice over one static-key query and never drops
to a skeleton.

/checkups and /mood gain the dashboard-template RSC prefetch: the
primary list read moves into a shared lib read (route + wrapper consume
one query, so filter/order/wire shape cannot drift), the wrapper honours
the DASHBOARD_SSR_PREFETCH kill-switch, resolves identity through
getUnswitchedSession, JSON-round-trips the dehydrated state, and fails
soft to the bare client on every error path. Page-prefetch unit tests
pin the seeded keys and the fail-soft arms; the SSR hydration spec
gains an assertion per page.

The achievement-unlock poll drops to five minutes (in-app writes already
invalidate the cell; the poll only covers out-of-band ingest), and the
plan-proposal poll gains the advisor-style bounded attempt ceiling so an
open thread stops polling after its budget.
2026-08-13 19:06:56 +02:00
Marc-André Bombeck f2f40ae8b2 Pair every daily-read invalidation with its inactive refetch
Ten client mutation sites forgot a consumer: a coach-accepted checkup
stayed invisible for up to five minutes, a threshold or target edit left
the dashboard bands stale, a score-config save marked the daily reads
stale but never refetched them, a booked visit missed the Today rail, a
source-priority reorder kept every chart on the old canonical picks, an
adopted self-context note never reached the Settings read. Each site now
invalidates what its write changed and forces the unmounted snapshot +
digest reads to refetch through refetchInactiveDailyReads.

New illnessDependentKeys bundle carries the daily reads (Rest Mode
frames both), dailyDigest joins encounterDependentKeys, and the
chart-data prefix gains a factory accessor for whole-cache flushes.

The daily-reads guard learns the cycle / encounter / vaccination /
illness bundle families and a second matcher for direct snapshot/digest
invalidateQueries without a forced refetch, each with a frozen reasoned
allowlist. Both matchers were proven live by reverting a fix and
watching the guard go red (see the test doc comment).
2026-08-13 18:48:10 +02:00
Marc-André Bombeck 408c1f599a Resolve the settle verdict in render, not in an effect
Same guarded transition, expressed as the render-phase state
adjustment the hooks lint rule asks for: a resolved settle clears the
state in the same committed render that carries the outcome, so the
falling-edge toast still sees both flips at once and the block runs at
most once per settle.
2026-08-13 12:46:55 +02:00
Marc-André Bombeck 893db887e9 Record why a document's index attempt failed and say so
A content-index attempt that failed (raster failure, a scan with no
text layer, a provider error) recorded its reason only in the server's
observability stream. In the vault, hasContentIndex false looked
identical for a document nobody ever tried and one that failed three
times, and the detail sheet had nothing to say about either.

Apply the summaryState pattern to the index. Two additive columns on
InboundDocument (migration 0333) record when the last attempt finished
and why it produced no index, as a closed reason set: local-empty,
local-unsupported, decrypt-error, raster-failed, provider-error,
pdf-needs-anthropic, empty-transcription. The writer sits at the choke
point the auto job and the corpus backfill share, and in the manual
index route; a success clears the reason. The provider path's specific
failure now survives the local fallback, since "no text" is less
actionable than "the PDF would not render". rasterizePdf names its
failure class so a build that cannot rasterize at all is distinguished
from a PDF that would not render. New guard in both index paths: an
empty provider transcription never becomes a successful empty index.

The detail sheet renders the reason in plain words (muted, six
locales). The DTO and OpenAPI carry the two fields; the disaster
recovery backup and restore carry them verbatim like their summary
siblings.

Refs #776
2026-08-13 12:40:50 +02:00
Marc-André Bombeck 45dc93932a Settle a timed-out assessment refresh instead of calling it failed
The force regenerate aborts its request at 45 s while the server's
inline generation keeps running and usually writes the fresh briefing
moments later. The old handling fired an error toast at the abort and
re-read the old cache once; with a one hour staleTime and focus
refetch off, nothing ever converged, so the page claimed failure while
holding a finished assessment at arm's length.

A timeout now enters a settling state: the hook remembers the cachedAt
baseline it was showing and polls the read path on the existing
bounded cadence (25 s, up to 10 attempts) until it can prove an
outcome. Cache advanced past the baseline: the fresh payload lands and
the success toast fires. A failure marker newer than the baseline, or
the attempt cap: an honest error. While settling, the strip's button
stays busy and says the assessment is still being prepared; no toast
fires until the outcome is proven. Both poll reasons live in the
nextAdvisorPollInterval seam, the verdict in resolveAdvisorSettle, the
toast mapping in regenerateToastKind; all three are pure and pinned by
unit tests. The contract is documented at both ends (hook and generate
route). New copy in all six locales.

Refs #786
2026-08-13 12:24:46 +02:00
Marc-André Bombeck 86a3d74187 Report how many documents the reindex actually queues
The index-all trigger returned Boolean(jobId) while the client typed
the field as a number, so the toast read "Indexing true document(s)".
The route now answers with the count of live documents still lacking a
content index, computed from the same live-scoped counts the usage
gauge reads, and an honest 0 when no job was created. OpenAPI schema
updated to the integer shape; the hook's type carries a contract note
so the wire and the toast cannot drift apart again. Route test pins
the number, a component test pins that the toast renders the count and
never the word "true".

Refs #776
2026-08-13 12:13:55 +02:00
Marc-André Bombeck e991ef7c77 Refetch the inactive daily reads after a quick-add intake
The medication quick-add mounts from the bottom-nav capture picker on
every page, not only on the dashboard, so its bare dependent-key bundle
invalidation (default active refetch) left the unmounted Today digest
and dashboard snapshot stale until the next poll — back on the home
screen the dose still read as due for up to two minutes. Route the
submit through invalidateMedicationReads, the one blessed entry point
that pairs the bundle with the forced inactive refetch, and drop the
surface's allowlist entry in the refetch guard: its dashboard-only
rationale stopped being true when the capture picker started mounting
this form globally. The other capture-picker forms (measurement, mood)
already pair the refetch.
2026-08-13 09:14:55 +02:00
Marc-André Bombeck a1b1617d0f settings: move the About-me note and free-text allergies into the Anamnese
The free-text self-context note and the free-text allergy line are
personal medical context, so they join conditions, allergies and family
history in Settings -> Anamnese. The note is its own card (same AI-surface
gate as the inclusion card); the allergy line sits directly under the
structured AllergyManager in the same card, labelled as a free-text
supplement so the two inputs never read as duplicates. The account
section drops the old panel. Store, API and query keys are unchanged;
the scoped Clear (note only, allergies untouched) carries over with its
tests. Orphaned strings removed, new labels in all six languages.
Refs #159
2026-08-13 06:16:45 +02:00
Marc-André Bombeck 13b7d0379b coach: reopen an interrupted conversation with a visible retry
An aborted stream now leaves a trace on both ends. The unmount cleanup
records the interrupted conversation (sessionStorage, one-shot) before
the abort fires, and invalidates the cached thread + rail so the return
visit refetches the advanced server state. The /coach page consumes the
record on its next mount: a known id reopens that thread, a first-turn
abort resolves via most-recent; every explicit entry intent (?c=, ?doc=,
?workout=, ?scope=, ?ask=) wins over the resume. The persisted cancelled
marker renders as a muted interrupted note with an Ask-again action that
resubmits the question through the normal send path and its gates.
Localised in all six languages. Refs #781
2026-08-13 06:16:45 +02:00
Marc-André Bombeck d26bbca52a fix(import): translate machine-readable import failure reasons
A user whose import died to a worker restart saw the literal string
interrupted_by_restart in the import card — the reconcile writes it as
a machine-readable code, and the card echoed it raw. Classify the two
coded reasons (interrupted_by_restart, the insufficient_memory prefix
from the memory preflight) onto translated, actionable copy across all
six locales; the copy tells the user their data is safe and to upload
the export again, which the re-enabled card already supports. Honest
free-text reasons keep passing through verbatim. Refs #775.
2026-08-13 06:16:44 +02:00
Marc-André Bombeck e83b58c9da Let the AI-consent card grant again after a withdrawal
POST /api/consent/ai/web now accepts an optional { intent } body. Without
one (the AI-settings mount heal) a standing revocation still blocks the
mint; intent "affirmative" is the user's own consent act and may
supersede an earlier revocation, exactly as a first grant would. The
consent card offers the explicit grant control whenever consent does not
stand, and the audit row records which intent minted the receipt.

The document auto-read opt-in already declared "affirmative" on enable;
the settings-mount heal posts no body. Both stay as they are.
2026-08-13 02:27:05 +02:00
Marc-André Bombeck fd104dd0ac feat(documents): review and extract lab values on the document itself
The server chain existed end to end (extract stages PENDING facts, per-fact
edit clears the low-confidence gate, confirm is the only write into the
structured stores) but the document detail offered no control over any of
it: it counted observation facts and linked to Labs. A user whose automatic
staging run was skipped had to re-upload the same PDF through Labs > Scan a
report.

- The detail sheet gains a staged-facts block with honest states: pending
  facts open an inline review (approve/reject per fact; a low-confidence
  fact is checked and saved first via the existing per-fact edit); a
  lab-filed, already-read document with nothing staged offers "Extract lab
  values" over its stored text; otherwise nothing renders.
- The extract route gains { mode: "stored" }: the same text-structuring
  pass the automatic worker runs, over the document's own content index.
  No re-upload, no second read of the original; full gauntlet including
  the shared document-AI bucket with charge-at-dispatch honesty; 422
  notIndexed when no stored text exists.
- A manual "Read with AI" / "Read again" now continues into the same
  lab auto-staging the background worker performs; the response reports
  labFactsStaged and the toast names the values waiting for review.
- Unchecked values are rejected and the copy says so; an unverified
  low-confidence fact receives no decision and stays pending rather than
  being silently discarded. Confirmation remains the only write into
  Labs, conditions, and medications; approved facts carry the
  committedRecordId link back to the source document (existing model, no
  schema change).
- The "lab values from this document" link now counts APPROVED facts
  only — it claims values that actually exist in Labs.
- Copy in all six locales; OpenAPI regenerated for the new extract mode
  and the index response field.
2026-08-13 00:33:14 +02:00
Marc-André Bombeck 8e16df6894 feat(documents): make the document-AI rate limit an operator setting with honest accounting
The document-AI actions (read/index, extract, suggest, summarise) shared a
hard-coded 6/hour bucket that lied twice: the 429 said "try again in a few
minutes" while the window is an hour, and several failure paths burned a
slot before any provider was dispatched.

- DOCUMENT_AI_LIMIT_PER_HOUR configures the shared per-user ceiling
  (default 6, clamped 1-1000), wired end to end: compose environment
  whitelist, .env.production.example, env manifest, operator docs.
- INSIGHTS_RATE_LIMIT_PER_HOUR joins the compose whitelist and manifest;
  it was documented but never listed, so an operator value could not reach
  the container.
- The 429 mirrors the bucket's reset instant into meta.retryAt (plus the
  existing X-RateLimit-Reset header); the client derives "try again in
  about N minutes" from it in all six locales, with a plural-aware key.
- A slot is consumed only when a request actually reaches the provider (or
  the local index/echo work): malformed bodies, unreadable originals,
  refused consent, and exhausted daily budgets refund the charged slot via
  a window-scoped refundRateLimit primitive. Consistent across all four
  routes sharing the bucket.
2026-08-13 00:33:14 +02:00
Marc-André Bombeck 3254e44281 fix(documents): keep the read action visible for an unread document under auto-read
The AI section of the document detail sheet collapsed whenever auto-read
was on and nothing was pending review, without checking whether the
document actually has a content index. A document the automatic pass
never reached (uploaded before the preference, or the pass did not
complete) then had NO visible way to be read; turning auto-read off was
the only workaround.

Collapse only once the document is read (or when indexing is unavailable
on the transport, where no action could be offered anyway). An unread
document keeps the Read with AI action, introduced by a short honest
note in all six locales; the suggest and summarise extras stay hidden
because the auto-read promise still covers them. The three states stay
distinct: unread shows the note plus the action, an in-flight read shows
the reading label, a read document collapses the block.

Refs #777
2026-08-12 22:52:03 +02:00
Marc-André Bombeck cfda1d9a78 refactor(charts): route the mood chart header through TileHeader
The mood chart hand-rolled a CardTitle where every other insights tile
header goes through TileHeader. Swap it for the shared primitive, which
renders the same CardTitle at text-base, and drop the now-unused CardTitle
import. Like-for-like; the surrounding control rows are untouched.
2026-08-10 10:23:24 +02:00
Marc-André Bombeck 6130f6ee6f feat(records): save the whole lifestyle-context tile with one action
The lifestyle-context tile carried a Save button per fact kind. Collapse
them into one footer Save that writes only the dirty kinds, firing the
existing bitemporal call per kind (POST a first value, PATCH a revision)
and settling them together so a single conflict cannot take the healthy
writes down with it. A kind that 409s keeps its draft on screen and names
itself, instead of vanishing behind one green toast. The per-kind Remove
stays.
2026-08-10 10:23:24 +02:00
Marc-André Bombeck 83d31dc9c1 style(medications): prettier formatting for the per-slot override step 2026-08-10 07:58:48 +02:00
Marc-André Bombeck 33769de372 test(medications): cover per-slot units consumption and wizard round-trip
Integration: a medication with a morning slot at 1 unit and a noon slot
at 0.5 draws the right count for each taken dose against a real
Postgres. Unit: a per-slot dose + units set in the wizard survives
create and round-trips on edit-hydrate.
2026-08-10 07:58:48 +02:00
Marc-André Bombeck 80958ab307 feat(medications): show per-slot units on both med cards
Surface the per-schedule units-per-dose beside the per-slot dose the
card already prints, identically on the standard and the GLP-1 card,
so a split plan reads at a glance which time takes a half. Muted
addendum, no width movement.
2026-08-10 07:58:48 +02:00
Marc-André Bombeck 64093d2124 feat(medications): per-slot dose and units in the create wizard
Give each parallel schedule its own optional dose and units-per-dose
input on the times step, so a split plan (a whole tablet in the
morning, a half at noon) is ONE medication with three schedules
instead of two drugs that double-count. Leaving both blank inherits
the medication-level values, so a single-dose plan is unchanged. An
as-needed slot that sits alongside scheduled ones now rides the same
medication as a PRN schedule rather than flipping the whole-medication
as-needed flag. New keys land in all six locales.
2026-08-10 07:58:48 +02:00
Marc-André Bombeck 57938982ea feat(settings): edit emergency profile under Anamnese
Add a Notfalldaten card to the Anamnese settings section with a manager
for the three enum facts and the three free-text fields, writing through
the new emergency route. Add every string across the six shipped locales.
2026-08-10 05:32:59 +02:00
Marc-André Bombeck 310a9a4690 feat(doctor-report): put emergency data on page one
Add an EMERGENCY leaf to the identity group of the report catalogue,
default-on, no module gate. When the leaf is admitted and the profile
holds emergency data, the aggregator collects it and the PDF renders an
emergency sheet as page one (banner, blood type, severe allergies, active
medications, chronic conditions, implants, advance directive and organ
donor, contacts, notes), then breaks to send the rest of the report to
page two. The section is withheld when the leaf is not admitted or no
data is present. Guards updated for the new leaf counts.
2026-08-10 05:32:59 +02:00
Marc-André Bombeck 7bb4fe3310 refactor(settings): modernize the managed-record controls onto the primitives
The delegated-caregiver settings forms were the last hand-rolled control
block: native <input>/<select> with a copied border literal, a native
type=date for the birth date (the browser-locale trap closed for the
owner surface in v1.25.4), module checkboxes where the owner section
shows switches, and a save button mid-form with the error line dropped
below it.

Route them through Input, NativeSelect, DateField, and Switch, and give
every form a shared footer that places the error line above the action
row and the action row last (§12). The Switch rows keep a hidden mirror
input so the FormData submit contract is unchanged. Extend the section
tests to hold the DateField primitive and the action-last order in place.
2026-08-09 11:38:17 +02:00
Marc-André Bombeck a959db8679 refactor(ui): route settings shells, actions, and empty states through primitives
Bring the remaining settings and dashboard surfaces onto the shared
anatomy: the API and MCP revoke buttons become the destructive variant
instead of an outline hand-tinted red; the runtime-actions row places
its raw-data setting and status lines above the actions, not under them;
the account section paints its card and header while loading instead of a
lone spinner; the share revoke becomes a right-aligned row action rather
than a full-width alarm strip per link; the recent-workouts tile and the
MCP and notifications empty states compose Card and EmptyState instead of
hand-rolled bg-card shells.
2026-08-09 11:27:18 +02:00
Marc-André Bombeck 7f6eae8425 style(ui): pull off-scale spacing and micro-labels onto the scale
Sweep the spacing rests that sat off the 1/1.5/2/3/4/6/8/10 scale: the
health-score panels and their skeleton, the recommendation ask-Coach
gutter, the onboarding tour footer, the vaccination row, the danger-zone
alert margins, the recovery-codes action wrapper, and the chart-tooltip
tile. Move the last hand-written micro-sizes (coach counters, the
personal-record badge, the score-ring label, the dose-history and
cycle-calendar headers, the mood tag chips) onto the text-2xs token, and
give the documents upload buttons the mobile tap floor their peers set.
2026-08-09 11:27:05 +02:00
Marc-André Bombeck b6332a96d9 refactor(ui): route hand-rolled card headers through the shared header primitives
Move the last hand-rolled tile and section headers onto TileHeader and
SectionHeading so every card speaks one header language: the vorsorge
dashboard tile, the three illness cards, the metric-correlation section,
and the custom-metric list group. The mood-chart title drops to the
primitive semibold weight and its bucket label becomes a TagChip.

Align the in-card sub-headings of the integration cards to the one
sanctioned section-label tier (muted uppercase eyebrow) instead of a
second text-sm font-semibold dialect.

Keep the onboarding and mental-health wizard hero H1s at their
deliberate semibold weight, now marked inline as an intentional
exception so a future heading sweep leaves them alone.
2026-08-09 11:03:43 +02:00
Marc-André Bombeck 27826ac825 docs: correct stale UI comments on error handling and colour tokens
- mood-insights-sections: the docblock claimed the sections render nothing
  on a failed read; the rest region now surfaces a QueryErrorRow with retry
- band-tokens / coverage-meter: the raw dracula primitives now carry their
  own AA-tuned light overrides, so the no-override claim no longer holds;
  the band still rides the semantic token for concept cohesion
- medication-card-body: drop the pb-2.5 header claim the header no longer sets
- modules-section: the body follows the card gap rhythm, not an mt-4 margin
2026-08-09 10:18:33 +02:00
Marc-André Bombeck 089c74cf23 feat(links): searchable, grouped entity link picker for visits and shots
The visit form's document / lab / condition link blocks and the
vaccination form's document link were flat lists capped at 25 rows with
no search: 30 lab values arrived undifferentiated and anything past the
cap was unreachable. Replace them with one shared picker — an inline
summary of removable chips over an add button, and a sheet that searches,
groups and multi-selects. Lab results gather under their panel and sample
date with a select-all per group, documents gather by month, illnesses
stay flat. The picker raises only the fetch limit; the server link cap
stays authoritative.
2026-08-09 07:01:15 +02:00
Marc-André Bombeck 40e53b14d6 feat(practitioners): search and group the address book by specialty
Search now covers the specialty column alongside name and practice, so a
field name like "Zahnmedizin" finds the practice even when the name and
practice hold nothing of the sort. The same OR reaches the visit form's
practitioner picker, which now shows the specialty beside the practice so
a specialty hit is legible. The list groups its rows under their
specialty, sorted alphabetically with the unspecified rows gathered into
one group at the end; when nothing carries a specialty the headings stay
suppressed and the list reads flat as before.
2026-08-09 06:53:57 +02:00