Skip to content

Vaccinations: the immunization record, visible and reachable - #751

Merged
MBombeck merged 18 commits into
mainfrom
feat/vaccinations-phase-2
Aug 8, 2026
Merged

Vaccinations: the immunization record, visible and reachable#751
MBombeck merged 18 commits into
mainfrom
feat/vaccinations-phase-2

Conversation

@MBombeck

@MBombeck MBombeck commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Phase 1 laid the data layer, the catalogue and the satisfy matcher without a surface. This makes the record reachable.

A vaccinations page lists a lifetime immunization record grouped by antigen, with each series resolved server-side (position, total, booster). A combination shot appears in every component group. A record whose catalogue entry no longer resolves, or a free-text entry, reads under its own name.

Capture is a sheet: pick from the catalogue (searching disease and antigen names, never trade names) or type it as the pass shows it, either alone is enough. Date plus one identity arm saves; nothing else is required. Lot, site, practitioner, an optional visit link and a note are all optional.

Where the catalogue carries a schedule, the entry shows it with its source, as information rather than a personal recommendation.

After a dose whose catalogue entry carries a booster interval, the record offers a prefilled reminder. Confirming mints an ordinary preventive-care reminder, so it lists on the checkups page, the digest and the report with no special case. Logging the next dose re-anchors it forward, and one antigen never holds two minted reminders.

A vaccination scan filed in the vault can attach to its record, and the record can link documents it already holds. The doctor report gains an immunization section, gated by the module and by its own selection, and no existing report or share link widens.

Six locales throughout, including the catalogue names. End-to-end coverage exercises transcribe, the combination case, the mint and satisfy loop, the module-off state and accessibility.

MBombeck added 18 commits August 8, 2026 20:24
…red-record family

- 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
…series

- 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
- 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
- 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
- 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
- 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
- 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
- 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
…aces

- 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
Five Playwright flows over the built app, each asserting stable data-slot
attributes rather than viewport text: transcribe a catalogue pick with a lot
into its antigen group, a combination dose rendering under each of its three
component groups, the booster prompt minting a Vorsorge reminder that shows on
checkups and moves its due date when the next dose is logged, the module-off
redirect with the nav entry gone, and an axe pass over the list and the form.

The catalogue option gains a data-catalog-slug hook so a flow can pick an exact
antigen deterministically. The spec runs in a single project and serial, and
clears its own rows before each test, so a grouped verdict counts only what the
test wrote.
… paths

Two record-scope properties through the real routes and a real Postgres. A
Guardian standing inside a managed profile writes a dose against the resolved
record — the profile's rows, never the Guardian's own — and reads it straight
back. A delegate holding a profile READ grant on an ordinary record lists and
reads the doses but is refused the PATCH with a 403, the escalation stopped at
the manage check before the row is looked up, and the row is left untouched.
The capture helper branched on an instant isVisible() read to pick between the
header add and the empty-state add. Right after a navigation that read races the
client render and loses, so it fell through to the empty-state button, which is
gone once a record exists, and the click timed out. The header add renders in
both states; clicking it directly lets click() auto-wait for hydration.
The booster mint adds one mutating handler, so the admitted inventory and its
strict-driver and effect mirrors are 75, not 74. The registry and the frozen
route count already moved; these three assertions had not.
…fter satisfy

A rolling reminder at the engine's ten-year maximum interval lost its next
due date the moment its cadence was satisfied. The rolling branch of
nextOccurrenceAfter bounded its search by the 10-year anti-spin hardCap, and
a satisfied reminder computes its next slot with after === lastSatisfiedAt,
so the single deterministic slot lands exactly after + 3650 days — equal to
the hardCap to the millisecond. The notify-hour slot then spilled a few hours
past the raw boundary and was clipped to nothing, leaving nextDueAt null.

The rolling slot is one deterministic instant, not an open-ended walk, so the
anti-spin cap must never gate it. Bound the rolling search by that slot's own
day instead; endsOn stays enforced inside expandRolling, so a course-window
reminder still self-expires.

This surfaced on a ten-year tetanus booster: logging the next dose silently
dropped the reminder instead of rolling it forward a decade.
…ur slot's own day

A rolling reminder at the ten-year maximum interval lost its next due when
the satisfy instant fell in the late-UTC evening. The next-occurrence search
bounded the single deterministic rolling slot by the end of the anchor's UTC
day, but the applied notify-hour slot lands on the following UTC day whenever
the anchor sits in the evening (the local morning is the next calendar day in
a positive-offset zone). The anchor-day ceiling was then a day short and
clipped the very slot it meant to admit, so logging the next dose set nextDueAt
to null and the booster silently disappeared. Key the ceiling off the applied
slot instant instead; endsOn stays enforced inside expandRolling.
The booster route was admitted into the MANAGE matrix but had no driver, so
the strict-driver and real-effect inventories stayed one short of the admission
count and the completeness guards failed. Register the write-effect driver that
mints the reminder against a tetanus dose and asserts the delegated WRITE actor
on the audit row, matching every other admitted write.
A role=combobox button is not named from its content, so the visible
placeholder text left both the vaccine-catalogue trigger and the shared
practitioner trigger without an accessible name, and the wrapping field label
pointed at an id the button did not carry. Name each from its field label. The
practitioner trigger is shared, so the visit form gains the same name.

Also settle CSS animations before the accessibility scan and poll the reminder
reads in the booster flow: the scan otherwise raced the capture sheet's fade
and measured a half-opacity blend as a contrast failure, and the flow read the
reminder list before the mint and the re-anchor had propagated. Both wait for
the settled state rather than relaxing any assertion.
@MBombeck
MBombeck merged commit 2f7f522 into main Aug 8, 2026
23 checks passed
@MBombeck
MBombeck deleted the feat/vaccinations-phase-2 branch August 8, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant