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.
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.
The mint card called every connector token read-only while the
health:write toggle sat directly beneath it — a token minted with the
toggle on writes measurements and mood entries. Rewrite the card
description and scope note in all six locales so the copy matches the
mint: read is the floor, write is a per-token choice made at mint time.
The copy guard now pins the pair so the read-only claim cannot return.
The residual scan from the report is a CCITT Group 4 single-image PDF,
the codec class pdfjs rasterization is known to fail on. Chain checks:
a pdfjs open failure or throw already lands as render-failed with the
error text annotated; the zero-pages shape now annotates too instead
of returning silently. An end-to-end test drives the REAL rasterizer
through prepareVisionInput and pins the rasterFailed mapping; the test
header documents why a tiny in-repo CCITT fixture is deliberately not
checked in (probed against this pdfjs version, it renders the page as
blank instead of throwing, so the fixture cannot deterministically
fail and would be flaky across upgrades). The detail view's
raster-failed line now names scanned fax compression formats as a
known cause, in all six locales.
Refs #776
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
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
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
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
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.
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.
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.
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.
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
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.
The consume hook now loads the user's zone, the schedules, and the
intake's scheduled anchor to resolve per-slot units, so the fake
client grows the schedule read and the batch scheduledFor lookup. The
Italian per-dose card label reworded so it no longer echoes English.
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.
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.
- the single- and bulk-delete flows on the measurement list both say
Messwert now, instead of switching to Messungen for the bulk case
- the practitioner field on the visit and vaccination forms reads
Praxis oder Arzt on both, dropping the slash form and the lone feminine
variant so it matches the practitioner list heading
- the visit saved/updated toasts lose their trailing period to match the
vaccination toasts they ship beside
- measurements subtitle drops to a single sentence and loses the phrase
that named Insights twice
- the mood self-rating card takes a short title so it stops wrapping three
lines and stops sharing the word Einschaetzung with the AI assessment
beside it; its noPattern and expectation lines move to direct address
- the daily-briefing empty state drops the worked example, leaving the
setup instruction on its own
The sub-page explainers had grown to four sentences of guideline detail that
pushed the stat strip and chart below the fold; the contract is a one- to
two-sentence definition, with the long form living behind the knowledge
disclosure. Each body now states what the metric is and why it matters.
Also aligns the readiness explainer to the score title across locales
(Bereitschaft / Disposición / Disponibilité), retiring the stray Tagesform.
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.
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.
Manual water entry is gone from the app: the water item on the capture
picker, the dashboard quick-add menu and the hydration card's add button
are removed, and the manual quick-add sheet is deleted. Water that syncs
in through Apple Health still arrives and still shows on the hydration
card with its daily total, history and reference line.
The POST /api/nutrients/water endpoint and the batch sync path stay
untouched. The nutrients page's module-enable error, which had borrowed
a water-quick-add string, gets its own key across all six locales.
Input-only water keys (nav.capture.water*, nutrients.hydration.quickAdd*)
are removed.
Three small copy corrections that share the locale bundles:
- Resolve the formal-address outliers on the owner sharing and report
scopes to the informal register the rest of those surfaces use.
- Add the direct-address comma to the personalized dashboard greeting.
- Split the managed-record form error: a client-side validation failure
no longer surfaces as a network error, and a failed save gets its own
line.
The note above the write toggle claimed tokens are read-only in every
case, which the toggle directly below it contradicts by minting a
read_write scope. Rewrite it to state the real model: read-only by
default, write opt-in per token, never admin. Resolve the pronoun that
read as a formal address, and promote both scope notes to foreground so
this security copy no longer renders as muted meta.
- freeze the booster-mint and upload-suggestion routes into the sharing
surface guard with their why sentences; delegable-write and mutation
inventories move by the two routes they add
- inventory the two new routes as module-exempt data routes (SURFACE-gated,
like the medications posture)
- give the booster-plan audit action its own owner-facing verb in six locales
- register the dose delete as a destructive control; route the vaccination
success toasts through the outcome module
- move the report leaf counts (structured 15→16, all 92→93) and the history
group count in the panel; regenerate the OpenAPI selection bound
- seed the immunization rows in the export-route mock so the aggregator no
longer throws for a fully-selected report
- the ~35 antigen and combination display names, medical orthography per
locale, umlauts intact end to end; disease and antigen names only, never a
trade name
- the three clinical proper nouns whose written form is identical across
locales (Hib, Hepatitis A/B in Spanish) are recorded as legitimate matches
- IMMUNIZATIONS report leaf in the history group, gated both ways: the
vaccinations module and the leaf selection each exclude independently
(WR-7 seen red on the selection gate, reverted by content hash)
- collector loads the whole live history (reference data, not windowed, like
allergies) with server-resolved series positions; the PDF renders a compact
per-dose table — date, vaccine, dose, lot — with no due-status or gap analysis
- added to the default template so a new report offers it; stored selections
and frozen share links never widen
- leaf label and section copy in six locales
- from the record: a document link picker on the capture form, gated on the
documents module (blanked when off, never post-filtered)
- from the upload: a VACCINATION scan offers to file against a dose recorded
within the shared ±7-day window; a single candidate pre-selects, two offer a
picker with nothing pre-selected (WR-8 seen red on the pre-select rule,
reverted by content hash), zero shows nothing, and it never blocks the upload
- the suggestion rule reuses the visit moment's window constant rather than
forking it; it registers with the shared filing module when that lands
- suggestion and link copy in six locales
The write and manage levels each carried a paragraph of capability text
under their label, stacking into walls of text on the invite form. Cut
each to a single consent sentence inline and move the full enumeration
behind the info affordance at the option-heading height — what exactly
write may add, and the fence manage does not reach. The consent line
stays inline where it has to be read; the detail is one tap away for
anyone who wants the whole list. Six locales.
The scope question defaulted to whole record, which reads as the form
nudging the choice. Preselect neither option: an untouched fieldset now
blocks the form with a quiet hint instead of falling through to a
whole-record grant. The wire contract is unchanged — scope-absent still
means the entire record on the server, so the unset state deliberately
does not omit scope, it blocks the client until the owner picks. The
empty-selection and manage floors stay fail-closed as before.
- rung 2: a prefilled, user-confirmed, editable, declinable prompt writes an
ordinary VORSORGE reminder keyed on the dose's primary antigen; the
published origin enum and reminder DTO are unchanged
- idempotent by antigen — a second confirmation re-anchors the existing
reminder rather than minting a second (WR-9 seen red on the reminder count,
reverted by content hash)
- rolling day cadence so logging the next dose re-anchors the due date through
the Phase 1 satisfy matcher; the whole loop proven through real routes
- booster prompt copy in six locales, plural-aware
The score-composition settings grouped its topics under domain headings
(Heart and metabolism, Activity, Sleep, Body shape, Wellbeing). Remove
them and list the individual topics one under another with hairline
dividers, the way the health profile lists its facts. Each row keeps its
domain data attribute, so the flattening preserves the registry order and
tests still address a pillar by its domain. The now-orphaned domain-label
map and its six-locale strings go with it.
- compose rung-1 sentences from the seed's series length, booster interval and
category over a handful of i18n templates, with the per-entry source as a
verbatim footnote; nothing personal, no age or history conditioning
- surface it in the picker's chosen-entry detail and behind the antigen group
header's info affordance
- schedule templates in six locales, plural-aware
- one sheet for create and edit, saving with a date and one identity arm
(catalogue pick or free text); nothing else can block the save
- catalogue picker searches localised names and generic synonyms over the
static antigen list, never overwriting the free-text arm
- optional dose/series, lot, site, practice combobox, visit suggestion and
note, all reusing the encounter-form primitives
- write hooks fan out through the dose dependent-keys and the reminder reads
- form/site/sheet copy in six locales
- read the immunization log through the query-key factory, unwrapping the
envelope; the series arrive resolved per component antigen from the DTO
- render one group per antigen, a combined dose appearing in each of its
component groups with that component's own position; free-text and dead-slug
rows fold under their verbatim name
- neutral cards, meta in muted, no due status on the page
- add the dose-list dependent-keys bundle, evicting the preventive-care root
alongside the dose list so a satisfied booster never lingers on checkups
- series/lot/error copy in six locales
- register /vaccinations in the clinical nav spine, module-gated on
vaccinations (default-on) and part of the shared record
- join /vaccinations to the profile shared-record route family so a scoped
profile grant can present the page it is allowed to read
- add the page shell: authenticated, module-gated, with the standard
module-off redirect the medications surface uses
- add the nav label and the surface title/subtitle/empty copy in six locales
The visit stage's second half landed while this was in flight and both
touched the same three places. Resolved as one set: the link facade
carries the reverse document-to-visit direction and the new dose pair
side by side, the sharing literals hold both sets of admissions with the
count re-added on top, and the language bundles take the shorter module
description the trunk settled on.
Three lines in a 240 px menu: the name, the access level, the kind of
record. The middle one truncated more often than it read, and it is what
the warning banner states in full once you are inside the record, where it
governs what the page lets you do. Two lines left, and the level stays on
the row as `data-access-level` for the browser journeys.
The switcher's own "Read only" / "Can add entries" strings had no other
reader and leave all six bundles.
The ECG surface opened with three lines of muted text saying what HealthLog
does not do, before a single strip. Two of them said the same thing — one
from the page's explainer slot, one hand-rolled beneath it — and the third
repeated it under the list. The device attribution on the recording itself
already carries that claim where it means something, so the three go and the
page opens on the recordings.
- the device's verdict rides the timestamp as a tag instead of taking a line
of its own beneath it. It qualifies the recording; it is not a second fact
- a recording is a route now, `/insights/ecg/[id]`, with the back link ABOVE
the heading exactly like a workout detail. The detail used to swap itself
into the list card with its back button under the page title, and the
recording had no address at all: nothing to deep link, nothing for the
browser's own Back to return to. It reads by id, so a cold link works
- the list paints the five most recent strips. A watch that records every
morning turns an unbounded list into a scroll, not a history
`ecg-classification.ts` holds the verdict label map the list, the detail and
the cross-link card each used to carry a copy of.
Research Mode was the opt-in behind the estimated GLP-1 drug-level curve:
turn it on, read a disclaimer, get the chart. The chart stopped consulting
the flag several releases ago and has painted for every account since, so
the switch on Settings → Advanced changed no screen in either position. An
inert control that promises to hide a chart is worse than no control.
The curve is simply part of the medication page now. Gone with the switch:
the acknowledgment dialog, `/api/auth/me/research-mode`, the shared status
DTO and its gate helper, the disclaimer-version stamp, the query key, and
the three user columns the acknowledgment wrote (migration 0321). The
Privacy page's second button pointed at the retired control and goes too.
Settings → Advanced is now what it reads as: reset the data, delete the
account.
Environment, external assistant access, Documents, mental wellbeing and
nutrient intake ran 120-165 characters in German where their thirteen
neighbours sat at 30-75. Every one of the five carried the same second
sentence, "off by default", which restates what the switch beside the row
already shows.
The mental-wellbeing row also named two screeners where the module ships
four, and its second sentence held the one statement in the app about what
happens to the answers. That statement moves to the mental-wellbeing page
itself, where somebody is about to answer them.
A guard holds every module row to one sentence in all six locales, and
under the 120-character guideline in English and German.
Two cards on Settings → Account were asking questions that already have a
home somewhere else.
- cycle tracking is a module, and the Modules hub carries the switch that
drives the same `cycleTrackingEnabled` column. The Account card was a
second door to one decision, so it goes, and the "manage in Account"
deep-link beside the module switch goes with it. That link pointed at a
`#cycle-tracking` anchor no element ever rendered, so it never landed
anywhere in the first place
- changing a password belongs with the second factors and the passkeys.
The card moves to Settings → Security and leads that page. It renders
before the factor status resolves: it is now the only way to change a
password and must not depend on a read it does not need
Account keeps the profile, the photo, the identity fields and the AI
context. The `settings.cycleTracking.*` strings leave all six bundles.
- a query-key module and a dependent bundle that also evicts the
preventive-care root, because logging a dose settles a booster and the
checkups list goes stale in the same tick
- the OpenAPI module publishes the resolved series with the contract
written into the description: render text from these numbers, never
recompute them
- the health-background consent sentence now names the immunization
history in all six languages, so the widening is stated where it is
agreed to rather than inferred from a code comment
- no server-side cache helper: nothing cached reads a dose, and a sweep
over caches nobody reads would look like coverage without being it
The VISITS leaf ships in the release that made a visit possible, because a
selectable checkbox that produces nothing is worse than no checkbox. Leaf,
collector and PDF renderer land together, and the leaf carries no LEAF_MODULE
entry — like ALLERGIES beside it — because a visit is core and the whole report
is already gated by doctorReport. The absence is commented at the map so a
later reader does not read it as an oversight.
The rows print the date, the practice with its specialty, the kind, the reason,
the outcome and the conditions the visit was filed against. The two encrypted
columns decrypt at render, fail-soft, and never enter the stored selection. A
condition the person has since deleted is not named: the link row survives a
soft delete, and printing a label they withdrew would put it in front of a
doctor.
No FHIR resource type is added. That deferral is already written at the
Encounter model, and the bundle builder now says so at the point a reader would
look for the missing section: FHIR splits the future from the past across
Appointment and Encounter, which is a serialisation question this release does
not answer.
Two properties are free and worth stating: a stored selection never grows
(unknown ids drop) and a clinician share link's sections are frozen write-once,
so no existing report and no already-shared link gains a section.
The panel also offers a booked visit as the report's reason — one query, one
prefilled window end, one dismissible offer that names its own source and never
applies itself. A window control that moves on its own is what makes an export
panel untrustworthy.
- four route files: list and create, read and edit and delete, restore,
and the link pair; every delegable one is classified as health
background, in each leg of the sharing guard, with its reason written
- link and unlink stay bare, consistent with the document and visit link
routes; the three are revisited as one decision
- vaccinations becomes the nineteenth module key, default-on and
surface-gated like medications: the data routes stay exempt so a
restore keeps working and re-enabling finds every dose intact
- a delegate's dose entries get their own sentence in the owner's
activity view rather than reading as a generic change
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.