The file-a-visit affordance painted on every checkup reminder, including
the self-measurement ones (weigh-in, blood pressure, a screening
check-in) where there is no appointment and 'you went' is never a
reading. Gate both the card and the dense-list branch on the same
free-text predicate the mark-done action already uses: a reminder with a
measurement type is satisfied in the app, a free-text reminder stands for
a practice visit. The server states the same boundary in resolve.ts.
The section carried a bespoke muted error row with its own retry button —
the exact drift QueryErrorRow was made to absorb. Swap it for the shared
primitive so the failure reads as foreground content. Test watched the
bespoke row first.
The tile already read the query's error flag but never branched on it, so
a failed reminder read fell through to "no upcoming checkup reminders" —
a missed preventive-care nudge hiding behind an honest-looking empty
state. Add a QueryErrorRow with retry before the empty check. Test
watched fail first against the fall-through.
A lab panel files itself against the visit it came from, and never has to. The
OCR review and the manual form both offer the suggestion through the one shared
field, anchored on the draw date rather than on now, so backdating a reading
moves the offer with it. The commit writes one link PER RESULT ROW, which is
why the join is m:n: a marker re-run on a different day belongs to its own
visit. Best-effort by design — the readings are the write that matters and must
not be lost to a link that could not be made.
The condition side gets a Visits about this card with an action that pre-links
the episode. From the episode side only, and that is a decision: an episode
spans weeks and a visit is a point, so matching the other way would be wrong
often enough to teach a person to ignore every suggestion in the product.
A due checkup gains a second action beside erledigt. Filing the visit as done
satisfies the checkup through the same forward-only, idempotent primitive the
checkbox uses, so doing both is one satisfaction and no error. The two actions
answer different questions — one says it happened, the other says where.
An appointment now appears where a person looks, and NEITHER surface relaxes
the reminder-table exclusion that keeps it out of the preventive-care lists.
Both read the visit table: a quiet line under the dashboard checkup block for
anything inside two weeks, and one rail item for today or tomorrow. A diff in
either that touches origin is the wrong fix.
A visit write now also refreshes the reminder reads. Closing a checkup moves
its next-due date on three surfaces that would otherwise keep showing it due
until their own poll.
OpenAPI regenerated in the same commit.
The two consoles used the same primitives and still read as two systems,
because everything without a primitive had drifted: the header-to-body
step existed in three competing techniques at once (`mt-4` on the first
body child, `space-y-4` on the card, `mb-4` on the header), the body
started either at the card edge or 28 px in, and both shells hand-rolled
the page title instead of calling `PageHeader`.
Move the decisions into the primitives so a call site cannot re-open them:
- `SettingsCard` is gap-based like `Card` (`flex flex-col gap-4`). The
header-to-body step is the container's, not a margin somebody remembers.
A dense card still overrides the gap on the card itself.
- The body starts at the card edge. `pl-7` is gone — off the spacing scale,
and it mis-aligned action rows against their own forms wherever two
neighbouring cards disagreed about it.
- The card header's description slot loses its `space-y-1`. That was the
licence call sites used to stack explainer paragraphs into a muted
`text-xs` slot; the slot holds one sentence.
- New `SettingsCardActions`: one action row, trailing edge, last thing in
the card. `align="start"` spells the one exception, a card whose only
actions are links out.
- New `StatTile` replaces four hand-rolled label/value boxes in Admin.
- `SectionHeading` moves from `insights/` to `ui/`. It is the heading above
a group of cards, which is what Settings needs; Settings had been using a
card header with no card, which reads as a card that failed to paint.
Both shells now render their heading through `PageHeader`, which gained
`headingAs="div"` for the second copy each shell paints (mobile above the
nav strip, desktop inside the grid) so only one is a real `<h1>`. The
page explainer is muted everywhere: the primitive said foreground, the
standards and every hand-rolled copy said muted, so pages disagreed with
each other. Medications, Vorsorge, the labs / custom-metric / illness
detail pages and the clinician view drop their hand-rolled title blocks
too, including the mid-word truncation on the medications subtitle.
Tests pin the parts that can be checked exactly: the card's gap, the
description slot's missing rhythm, the action row's two alignments, the
`<h1>`-free shells, and a sweep asserting `pl-7` stays gone.
`/` runs its reads server-side and hands them to TanStack through a
HydrationBoundary, so the first HTML already carries real tiles. React threw
that away on every cold load. The app shell hydrates first and its mount
answers `/api/auth/me`; the dashboard sits below `loading.tsx`, hydrates later,
and its first client render therefore read an account the server never had.
That disagreed with the streamed HTML, React reported #418 and re-rendered the
whole page. The prefetch paid for its queries and delivered none of its paint.
The account payload is now withheld from the hydration render wherever it is
only rendered. `useAccountOnceMounted()` answers null until the component has
mounted, which is what the server rendered, and hands the payload over on the
next render. The dashboard page, the caregiver card, the getting-started
checklist, the achievements tile and the unit labels read it that way. `useAuth`
itself still reads the cache, because the document vault redirects off
`user.modules` and a first render claiming "no account" bounces it to `/`.
Two tiles disagreed without any account involved. A query reports `isLoading`
true on the client's first render and false on the server, so both now gate on
whether the cell holds an answer.
Nothing in CI ran the shipped configuration: the suite's server turns the
prefetch off so route mocks keep governing the dashboard. A second server runs
it on, and one spec asserts that no hydration error is reported and that the
tile node the server streamed is still the node in the DOM once the page
settles.
Measured over nine cold loads with the CPU throttled 4x: seven bailouts before
and none after, median LCP 869 ms before and 145 ms after. FCP and the time to
the first tile in the DOM are unchanged, because the streamed paint was never
the problem. Keeping it was.
Nine surfaces offered a delegate an action the server declines, and the
switch had never been asked about any of them.
The Today rail's dismiss and the coach check-in's keep / let-go write
through routes that resolve the caller, so both refuse under a switch;
they now ask canManage and are absent at either grant level. A priority
action with no href and no handler is no longer rendered as an inert
button at all — the type carries that rule now.
The Vorsorge dashboard card offered the mark-done that /checkups already
withholds, and the checkups list branch inlined its own ungated copy of
the button the cards branch gates, so which view a browser last chose
decided whether the action appeared. Both bind canManage.
The chart overlay cog persists through a route that resolves the caller
and stores a preference belonging to the person rather than the record.
Gated once in the control so the three chart wrappers cannot drift.
Three query parameters opened sheets past a gated button: ?add=<TYPE> on
measurements, ?new=1 on medications, and ?edit=1 on a medication detail,
the last of which opens a wizard refused at both levels. A deep link is
the same affordance as the control that produces it.
The efficacy retarget dial rewrites a setting the owner chose. The
episode documents card offered a link and an upload the vault gates on
the same endpoint throughout. And the dose ledger built its own success
toast, missing both the Undo suppression and the "saved to" receipt its
two siblings already carry — the one a delegate met on every dose.
The Coach drawer is not mounted inside a shared record, so every button
calling askCoach() opened nothing. The launch provider now publishes no
value there and each entry point's existing null check does the rest;
the documents sheet, the one that rendered without checking, now checks.
Every page a switch can reach now renders the controls the grant covers and
none of the others. Readings, lab results and illness entries keep their add
path at WRITE and lose edit, delete and selection at both levels. Mood,
screeners, cycle days, checkup reminders and the document vault lose every
mutation control at both levels, because no grant admits those verbs and the
routes behind them refuse a switched session outright.
Absent, not disabled, throughout. The banner already says whose record this
is, so no chip repeats it beside each missing button; the one exception is
the measurements empty state, where somebody who may add will look for the
edit control beside the row they just entered, and one muted line says where
the limit is.
Two dead ends went with them: the custom-metric section on the measurements
page linked every row into a route the shell turns a delegate away from, and
the wrench glyphs point at settings pages sharing does not cover.
The two calendar-day cases built their due instant with setHours on the
process clock, which vitest pins to UTC, while the component floors the due
date in the profile zone. Those two disagree about the date for the two hours
before UTC midnight, so both cases went red every night in that window and
green again by morning.
Deriving the date parts from the display zone removes the disagreement rather
than hoping the suite never runs at night. Noon is chosen deliberately: at any
offset the zone uses, 12:00 UTC lands on the same calendar date there, so the
instant cannot slide either way.
Verified inside the failing window: red before, green after, and putting
setHours back turns it red again.
The phrase read the device clock while the date printed under it read the profile clock. Set a laptop to another zone and the card could say a checkup was tomorrow above a date that was today. The zone is now something a caller has to state, and both screens state the one their dates are printed in.
The dashboard summary card and the checkups page each worked out "today",
"tomorrow", "in N days" and "overdue by N days" from the same due date, and
each did it with its own copy of the arithmetic. The page compares calendar
days. The card subtracted one instant from the other and divided, so a checkup
due Sunday at 01:00 read "tomorrow" when you looked on Friday evening, one due
Saturday at 23:59 read "in 2 days" when it was in fact tomorrow, and one due at
09:00 quietly stopped saying "today" as the afternoon wore on.
Both now read from one place, so they cannot drift apart again.
The reverse-coverage guard asserts that every key in the bundle is reachable
from a call site. It had stopped doing that for most of the bundle. A template
whose only static anchor was a namespace root registered that root as a subtree
base, so a single per-provider OAuth key marked every key under `settings.*`
reachable, and the same shape did it for `measurementReminders.*`. A cache-key
string that happens to start with a namespace word had quietly done it for
`insights.*` too. The guard stayed green throughout, which is the part worth
noticing: a pair guard degrades into a one-end guard without any signal.
Extract the matcher into a helper with its own unit tests, and change what it
accepts. A template carrying both a literal prefix and a literal suffix now
records the pair and requires both ends to match, so the OAuth template covers
the provider keys and nothing else. A template with no suffix whose anchor is a
bare namespace no longer whitelists anything — it fails the guard by name, with
the two honest remedies in the message. The same door is used by the template,
assignment and concatenation arms, so none of them can reopen it. Template
bodies are read by scanning forward from a known opening backtick rather than
by pairing backticks across the tree, which was drifting and dropping whole
files out of the scan.
Three call sites build their keys whole instead of interpolating onto a
namespace root: the two measurement-reminder due helpers, the mental-health
option labels, and the OAuth error copy, which now names its per-provider base.
Two audit-log action names that merely look like keys carry a written
exemption; the API error catalog is a runtime lookup and is allowlisted with
its reason.
With the matcher honest, 149 keys had no call site left. They belong to an
explainer popover, a hypnogram, a passkey table and an insights hero that were
all rebuilt or removed, and they are deleted from all six locales. The
explainer guard stops pinning its title leaves, which no surface has rendered
since the popover went.
Document vault: card list drops a stray glyph and gives the real filename
its own line (was truncating on the meta row); the detail sheet's close
button aligns correctly — ResponsiveSheet reserved the right gutter for a
close button it wasn't rendering, now conditional on showCloseButton. The
open-in-coach action reads 'Coach fragen'.
Bulk share: a multi-selection in the overview shares several documents in
ONE link (documents-only privacy preserved, owner-scoped, up to 50 per
link; >50 refuses with a toast rather than silently truncating).
Reminders can link to WAIST_CIRCUMFERENCE (additive MeasurementReminderType
enum value, OpenAPI regenerated). The WHO-5 and SCI well-being reminder
labels were absent in all six locales and rendered their raw dotted keys
(the shared 'measurementReminders.types' prefix is what showed as the stray
'Measurement Reminders Types'); added in every language. A new
type-labels-i18n test asserts every dynamic reminder-type key resolves
non-empty in all six bundles — the static coverage guard cannot see the
dynamic key that let this through.
Client-rendered times fell back to the hardcoded display zone
(Europe/Berlin) because neither useFormatters() nor the legacy format.ts
helpers ever received users.timezone. A third localStorage mirror
(timezone-mirror.ts, following the time-format/date-format pattern)
carries the validated profile zone from /api/auth/me into both read
paths; the fallback chain is strictly profile → Europe/Berlin — never
the browser zone, so screen, PDF, and export cannot disagree.
makeFormatters validates its zone before Intl (a poison value falls back
instead of throwing inside every render).
Same pass, per the two-document audit: formatUpdatedLabel boundary and
clock now resolve one zone in every state (the undefined-tz browser hole
is closed); @db.Date calendar fields render UTC-pinned so profiles west
of UTC cannot shift a day; dose-history day headings and GLP-1 weekday
labels derive from the display zone; chart day/month labels are
encoding-derived and UTC-pinned (bucket math untouched, byte-identical
for Berlin profiles); the clinician share view renders in the patient's
timezone instead of the container's; Playwright pins Europe/Berlin.
Server day-key semantics, streak anchoring, insight cache keys, and
verbatim wall-clock strings are untouched by construction and pinned by
the new Auckland/New_York/Manila + DST test matrix.
The landing slims to intro + instrument cards: the combined history card
leaves the main page entirely. Each card body now opens a per-instrument
detail sheet (the med-/Vorsorge card interaction) carrying the last
score + band, the score-over-time trend chart (restored from the
pre-v1.27.6 chart, lazy Recharts, bands surfacing discreetly in the
tooltip), the dated result history with the crisis re-surface, the Start
action, and the instrument's required attribution line — the Verlauf is
deliberately opt-in behind a click, never pushed onto the page someone
arrives at to take stock.
The wizard drives from the registry: option values in source order (the
WHO-5 leads with 5 = all of the time, the SCI with 4), shared vs
per-item anchor groups, and recall-stem captions above the WHO-5 / SCI
item fragments. WHO-5 items and anchors ship VERBATIM from the official
WHO publication in all six locales; the SCI is validated in English, so
every non-English locale presents the English items plus a localized
'validated in English' note (a self-made translation would invalidate
the instrument — the leak-guard allowlist documents this as intended).
Result view: direction-aware follow-up hints (WHO-5 <= 50 points gently
to the PHQ-9 check-in; SCI <= 16 carries the paper's neutral band
wording; PHQ-9 / GAD-7 keep their >= 10 hint). The intro states each
recall window honestly (two weeks vs past month). The Vorsorge screening
group offers all four instruments. Chrome localized in de/en/es/fr/it/pl.
The Vorsorge card resolves both rules at once: the action button keeps
its constant look in every state (v1.27.5), and a screening reminder's
action reads 'start the check-in' instead of the numeric measure
wording (this wave).
PHQ9_SCORE / GAD7_SCORE join the Vorsorge reminder type allow-list as
their own "Seelisches Wohlbefinden" group, with a 4-week default
cadence (both instruments cover the last two weeks; 28 joins the
interval presets). The reminders ride the existing activity-aware
engine unchanged: they fire only when overdue, and a completed check-in
auto-satisfies them — the assessment route now kicks the eventful
reminder-satisfy worker, which resolves against the server-written
score row; the 15-min cron stays the idempotent safety-net. The
screening reminder's primary action opens the check-in page (a score is
never typed in), and the dispatcher gates the reminder on the opt-in
mental-health module so a disabled module never nags. OpenAPI spec
regenerated for the widened MeasurementReminderType enum.
The card and list variants of the reminder action button washed green
(bg-success) once the next-due date arrived. A button that changes
colour by schedule state reads as a different control; the rule that
keeps the medication-card surface constant applies to actions too.
Due / overdue keeps reading through the discreet coloured next-due
text and the adherence meta line — the button itself never changes.
Appearance ("Darstellung") stacked every module's view and ordering
settings on one long page. Split it into a hub that lists the modules as
navigable rows, each opening its own /settings/layout/<module> subpage
with a back link — the per-module settings move verbatim, no restyling.
The module routes now redirect to those subpages instead of an in-page
anchor, and the in-app cogs link straight to them.
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.
Add isError branches with a retry affordance to the mood, illness, and
vorsorge lists so a failed read no longer renders as "no entries yet"
and invites duplicate re-entry, matching the labs and cycle pattern.
Make the admin backups table reachable on a phone with a card fallback,
name the admin language and timezone selects and the AI raw-data switch
for screen readers, and replace the touch-unreachable title= help on the
glucose clinical panels with a tap-capable info popover.
Route the password-strength label through the AA-tuned semantic tokens,
underline the register login link so it is not distinguished by colour
alone, give the labs load error alert semantics, and explain why the
target editor Save button is disabled with aria-invalid and a live
alert.
Move the preventive-care surface from the German-derived slug /vorsorge to
the English /checkups and make the feature read consistently across locales.
The route directory, the nav entry, the dashboard card link, and the
onboarding tour step now point at /checkups. A permanent redirect keeps old
bookmarks, the PWA's cached navigation, and push deep-links resolving.
Display label is now "Checkups" in English and the equivalent term in
Spanish (Revisiones), French (Examens), Italian (Controlli), and Polish
(Badania); German keeps "Vorsorge". The internal id stays "vorsorge" so
saved layouts, module-order preferences, and tour state keep resolving
without a data migration.
Swap the medication scheduling, inventory, illness, vorsorge, lab-OCR
and profile-birthdate date inputs from the native field to the
DateField primitive, which paints the chosen DMY/MDY/YMD/AUTO order
over an ISO yyyy-MM-dd value. The committed value contract, min/max
bounds and aria wiring are unchanged.
Align the Vorsorge settings card to the canonical pattern, neutralize the danger
zone card, mark the bug-report feature deprecated, make ipwho.is the default geo
resolver, and keep the mobile More-hub labels on one line.
Adopt the standard SettingsCard + SettingsCardHeader contract for the
three Vorsorge settings groups (view, order, manage): bordered card
surface, neutral muted section icon, text-lg title, and a short muted
description, replacing the bare sections with text-sm headings so the
surface reads like every other Settings section. Add a reorder
description string across all locales.
Drop the grey cadence pill from the card heading. The cadence now reads as
part of the metric line, styled like the metric label itself (no shaded
background), via a new MedicationCardHeader metricMeta slot. Nothing else
sits beside the heading.
Link a measurement-linked reminder's heading to the measurements history
pre-filtered to its type, reusing the existing kebab target. Free-text
reminders keep a plain heading.
Remove the 7-day bar strip under the Vorsorge card last-done line and its
backing component, query key, and i18n strings. Render the last-done value
as a relative date (today / yesterday / absolute) through a shared
relativeCalendarDate helper that reuses the medication today/yesterday keys.
Show the next-due status as discreet coloured text - green today, orange
overdue - leaving the card surface neutral.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Bring the medication card, scheduling, wizard, and detail component
files in line with the kebab-case file-naming convention. Component
export identifiers are unchanged; only filenames and the import paths
that point at them move.
Sweep the primary touch controls that sat below the 44px tap-target
floor on phones onto the canonical `min-h-11 sm:min-h-9` (text) /
`size-11 sm:size-9` (icon) pattern, condensing back at `sm:` upward:
- Coach composer mic / stop / send icon buttons (were `size-9`).
- Dashboard hero CTA, link and action variants (were `size="sm"`).
- Vorsorge list-view "mark done" button (was `min-h-9`) and its kebab,
which now uses `size="icon-lg"` so the rendered control is 44px
rather than the icon variant's 40px.
- Illness list-view "log day" button (was `min-h-9`).
- Medication wizard Step 3 injection-sites disclosure (was `h-9`).
The Vorsorge dashboard-card capture action was raised alongside the
ListRow migration.
The bordered list-row "mini-card" appeared three times with three
shapes: the Vorsorge dashboard row (`rounded-md p-2.5`), the daily-
briefing rows (`rounded-md p-3`), and the measurement-list mobile row
(bespoke `rounded-lg p-3` div). Introduce `ListRow` (`rounded-lg border
p-3`, `asChild` per the shadcn convention) and migrate all three to it.
The primitive owns only the box shape; each caller keeps its layout,
colour, and interaction classes, so this is structural normalization
with no content or behaviour change.
Add aria-expanded to the coach drawer's history-collapse control so its
expanded state is exposed, matching the expand toggle. Give the Vorsorge
trend strip an sr-only summary of the latest value and seven-day
direction, and key the bars on measuredAt rather than the array index.
Enrich the Vorsorge cards with a discreet 7-day trend strip and a path
into the underlying readings.
- A quiet seven-bar strip of the reminder metric's last seven readings
sits under the metric context on both the dashboard summary card and
the full-grid cards, mirroring how a dashboard tile carries a small
trend. Neutral muted bars, no status colour. A free-text reminder or a
metric with fewer than two readings shows nothing, so card height stays
stable. The strip reads a bounded last-7 window per type through the
existing measurements endpoint, cached by type under the measurements
invalidation prefix.
- The card kebab gains a 'Show measurements' item for measurement-linked
reminders that opens /measurements pre-filtered to that type.
- The measurements list accepts a ?type=<MEASUREMENT_TYPE> deep link that
seeds the existing client-side type filter while leaving the rail
interactive; unknown tokens are dropped. The ?add= behaviour is
unchanged.
- New labels added across all six locales.
Mirror the medication card's name + cadence + status layout:
- the cadence ("alle 7 Tage") renders as a chip directly to the right of the
metric name via a new MedicationCardHeader nameChip slot, instead of on the
state-badge row below the category;
- the due / overdue status renders as plain text, not a chip;
- the "zuletzt erledigt" line always renders, falling back to an em dash when
there is no last-done value, instead of being hidden.
The green action button is unchanged. Also drop the per-module tour trigger
from this surface (the tour is first-time-only now).
The diabetes PATCH parsed its body without a size cap; bound it through
safeJson like the sibling routes. Label each reminder's enable/disable switch
by its own name span so screen readers hear the reminder rather than one
generic label repeated per row. Point the chronotype and illness disclosure
triggers at the regions they reveal via aria-controls.
The Vorsorge "Jetzt messen" / mark-done button hard-coded white text on a
solid green fill. In the dark theme the fill is the bright Dracula green, so
white-on-green reads about 1.37:1 — below the WCAG AA floor.
Add a theme-aware `--success-foreground` token (near-black glyph on the dark
fill, white on the darker light-mode green) mirroring the existing
`--primary-foreground` pattern, and use `text-success-foreground` on both the
card and list variants. Green stays only on the action button; the card
surface remains neutral.
- add a wrench beside each module's add button linking to the module's own
settings page; rename every add button to a plain "hinzufügen"
- give each module a settings page: view (cards/list) + manual reorder; Labs
also gets a sort-order choice (by last reading) and biomarker CRUD/reorder
- collapse the Illness "abgeschlossen" list by default
- rebuild the Vorsorge card on the medication card's building blocks: a green
"Jetzt messen" action, the interval as a chip beside the name, and an
in-interval progress signal; drop the kebab "anpassen" and stray actions
- persist per-module view/order/sort client-side; add reverse-coverage-clean
strings across all six locales
Add a 'Diese Tour zeigen' affordance to every module header that opens the
tour narrowed to that module's card in place, plus additive data-tour
anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/
insights/coach/integrations/export/achievements) so the cross-page walk
has a real target on each page. Consolidate the full-tour replay onto a
single Settings -> Advanced card, relabel it, and drop the duplicate from
About. The replay reset clears the stored progress so it restarts cleanly.
Each reminder is now its own med-styled card reusing the medication card
primitives (MedicationCardHeader / MedicationNextLastSlot) on a neutral
surface with a single bottom-pinned action and a kebab carrying edit +
delete. The mark-done action branches on the linked measurement type:
- a free-text / self-planned exam keeps the silent satisfy ("Done");
- a measurement-linked reminder opens the real measurement-entry form
("Log value") pre-filled with its type, and on a successful write
satisfies the reminder in the same action (forward-only guard keeps
the cron a no-op).
The create/edit sheet gains a first-due date (anchorDate), a free
custom interval, and an RFC-5545 RRULE option, all already accepted by
the create/update schema. A wrench/customize affordance beside the page
Add button re-homes the per-reminder enable toggles moved off the card.
Vorsorge also joins the dashboard grid layout like medications: a new
chart-row summary card listing the next few due reminders with inline
done/capture buttons, default-off so existing dashboards are unchanged.
No migration and no API schema change; OpenAPI stays in sync.
Twice-daily BP protocol (RRULE BYHOUR=7,19) now fires at every clock
hour: byHourTimesOfDay expands BYHOUR into the engine's timesOfDay so
the label, RRULE, and engine output agree instead of collapsing to one
nudge at notifyHour.
Vorsorge surface:
- move create/edit into a ResponsiveSheet with a pinned Cancel/Save
footer, matching Labs/Illness/Medications
- wire the unused PATCH support: Edit affordance opens the form
pre-filled; an enable/disable Switch drives the enabled flag
- translate COACH-origin labels (the row stores an i18n key, not prose)
and render a neutral Coach provenance badge + an until-<date> line
- surface origin + endsOn on the hook type
- pull-to-refresh + always-on rationale documented on the nav entry
- NativeSelect-vs-Radix choice documented as deliberate
Coach:
- expired COACH course-window reminders self-clean: the cron tick
soft-deletes finished finite cadences instead of leaving dead rows
- accept-dedup gains a partial unique index (user, metric) where
origin=COACH and not deleted, with a P2002 catch in the route
- per-user rate-limit bucket on the suggestion-action endpoint
- SUGGEST-REMINDER parser keeps prose written after the block
- document POST /api/coach/reminder-suggestions in OpenAPI
Coach chrome:
- HistoryRail hideHeading prop drops the duplicate Conversations
heading on the page surface
- align the rail-tray strip inset to px-3 sm:px-4 so the dividers line
up; reword the FAB inset comment
satisfyReminder closes the forward-only TOCTOU via a conditional
updateMany (count===0 no-op). Migration headers corrected to v1.18.1.
The reminder type allow-list exposed 7 of the full MeasurementType enum.
Widen it to the ~15 active-measurement set: vitals (weight, BP, pulse,
glucose, SpO2, temperature) plus the body-composition family a scale
produces (fat mass, fat-free mass, muscle, lean body mass, bone mass,
body water, visceral fat, BMI). One step on the scale satisfies every
linked reminder at once. Passive scores, cumulative activity, and device
events stay excluded — nudging someone to 'go measure' a passive sample
makes no sense.
App-layer only: the column is the full enum, so no migration. Group the
dropdown (Vitals / Body composition) and add the labels across all six
locales.
mood, medications, Vorsorge, the health-record export panel and the export
section hand-rolled their card headers; route them through the shared settings
card-header primitive so every settings card header is structurally identical.
The single-row delete on lab results and on preventive-care reminders
fired immediately on click, unlike the measurement and mood lists which
gate the same action behind a confirm dialog. Route both through the
shared DeleteButton so a stray tap can no longer drop a reading or a
reminder, and add the per-surface confirm copy across all locales.
The reminder list read isLoading but rendered nothing, so the page
showed a header over blank space while fetching.
- Loading now paints a card-shaped Skeleton stack via the shared
primitive.
- The no-data case routes through the shared EmptyState with an icon
and an add action, matching the lab empty.
- Reminder cards title themselves with CardHeader/CardTitle + CardAction
and the form card drops its pt-6 override, so the card token owns the
vertical rhythm and the double-pad goes away. The card stays neutral —
due state still reads only through the discreet badge.
Add the Vorsorge page and section: an upcoming-reminders list sorted by
server-computed next-due, with a create form (measurement-linked or
free-text, cadence, notify hour, location) and a per-card Erledigt and
delete action. Cards stay neutral-coloured regardless of due state — the
status reads through a discreet badge only, never an alarming tint. Copy
ships in all six locales.