Commit Graph
3027 Commits
Author SHA1 Message Date
Marc-André Bombeck 8793bbf229 fix: close four small gaps around the managed-profile surface
- bound profile creation at ten an hour per account, the ceiling its
  guardian sibling has carried since it shipped
- distinguish a failed roster read from a slow one, with the way back
- cover the three caller states the roster read has to refuse: an
  unaccepted invitation, an expired grant, and MANAGE held elsewhere
- cover what deletion does to a guardian standing inside the record
2026-08-07 03:56:39 +02:00
Marc-André Bombeck 47c4560253 test: prove the banner names the kind of record, not just tags it
recordKindLabel chooses between two sentences that mean different things
and nothing could fail it: the journey asserted the data attribute, which
that function never reads, so inverting the arms left the suite green
while a guardian standing inside a managed profile was told it was an
ordinary shared record.
2026-08-07 03:56:30 +02:00
Marc-André Bombeck 29940afb57 fix: name the step-up gate on the two acts that carry one
- deleting a profile and ending a guardian's access resolve fresh MFA,
  and both sit behind a confirmation, so the request that reaches the
  server is minutes old and routinely outside the five-minute window
- an account with no second factor is told where to set one up instead
  of being asked to confirm one it does not have
- the arm is scoped to those two acts: a 401 on accept, revoke or
  renounce is a lapsed session and must not read as a factor prompt
2026-08-07 03:55:25 +02:00
Marc-André Bombeck 217b0f41d8 fix: say what is wrong with a field where somebody will read it
- a refused field gets its own announced node in content contrast; the
  standing hint stays muted beside it, rather than being overwritten
- the timezone refusal is the only explanation for a submit button that
  will not press, so it leaves the picker's muted hint slot
2026-08-07 03:55:15 +02:00
Marc-André Bombeck 6bfcccd755 feat: manage the guardians of a managed profile
- list who looks after a profile, invite another guardian and end an
  existing access from the profile's own row
- state the last-guardian floor from the real roster before the act, and
  say plainly that an invitation counts only once it is accepted
- give each of the invitation refusals its own sentence, branching on
  status for the two the route sends without an error code
- cover every call the card can make against the real routes
2026-08-07 02:22:30 +02:00
Marc-André Bombeck b3548f105e feat: delete a managed profile from its guardian's panel
- confirm before deleting, naming what goes and that it cannot be undone
- state how many people lose the record, taken from the guardian roster
  rather than assumed to be one
- say that deleting is the way out when you are the last guardian, since
  this route sends no last-guardian refusal
- announce a refused deletion on the profile's own row, retryable
2026-08-07 02:07:49 +02:00
Marc-André Bombeck 110511ff31 feat: create managed profiles from shared access
- add a managed-profile card to Settings -> Shared access, between the
  invitation card and the standing-state cards
- collect exactly the four fields the create route accepts, with the date
  of birth genuinely optional and never derived from a year
- refresh the account payload on success, so the new record reaches the
  switcher and the banner without a reload
- assert the section is reachable for an owner and refused for a Guardian
  inside a profile and for an adult manager
2026-08-07 01:53:26 +02:00
Marc-André Bombeck 6ae0cc993f fix: close four small gaps around the record surface
The privacy dashboard re-implemented the audit-retention resolver with its own
default and its own floor. Two implementations of one disclosure, quoted to the
same person on two pages and by the purge job; it uses the shared resolver now.

The record-content branch in the settings gate matched a classification rather
than a page, so a second manage-writable destination would have rendered the
allergy and family-history managers under its own heading without anybody
choosing that. It resolves through a slug map, and the contract test fails if
the map and the classification disagree in either direction — a destination
cannot join the kind without somebody deciding what it renders.

The managed-profile nav filter assumed a guardian grant is always manage. It is
today, which is why the check was easy to leave out; an entry below that level
would have listed destinations the gate refuses.

Interpolation used a replacement string, and String.replace reads $&, $`, $',
$1 and $$ out of one. A display name containing $' rewrote the sentence around
it — and display names land in exactly the sentences that say who did what in
somebody's health record. A replacer function has no $ grammar at all, which
removes the class rather than escaping the members of it somebody thought of.
2026-08-07 01:00:02 +02:00
Marc-André Bombeck c7e847a766 fix: bound the activity feed to the window it promises
The revoke dialog tells an owner that "who entered what" stays answerable for
N days, and the activity list prints the same number under its heading. The
query behind that list had no date filter: on an instance whose retention was
shortened it showed rows the sentence said were already gone, and the number
was a caption over an unbounded read rather than its limit. It is the filter
now, resolved once so the bound and the sentence cannot drift apart.

The list also stopped at a hundred rows and said nothing about it, which is the
more dangerous of the two silences — an owner who scrolls to the bottom reads
the oldest visible line as the beginning, and a busy household reaches the cap
in a fraction of the window. The response says which of the two limits it hit,
and the view states it only when it did: a caveat that is always on is one
people learn to skip.

The self-hosting page said an empty list means nobody opened the record. It
means nobody did so within the window. Corrected, with the row cap named
alongside it.
2026-08-07 00:48:58 +02:00
Marc-André Bombeck ef242f33bb feat: open the anamnesis record surface to managers
POST /api/allergies and POST /api/family-history have resolved
requireRecordAuth("manage", "profile") since the level shipped, while the only
form in the product that posts to either sat behind a Settings destination
classified unavailable. The write was admitted, audited and owner-scoped, and
nobody could reach it — the one-ended change this repository keeps
rediscovering, with the permission shipped and the surface left as a follow-up.

The destination now carries a classification that means what the routes mean:
reachable by everybody they already admit, which is a guardian inside the
profile they administer and an adult delegate holding manage. The four places
that decided visibility on the guardian-only literal were each updated
deliberately, and the shell that refused every settings path in an adult share
now admits exactly the destinations classified as record content. Record
configuration — modules, thresholds, notification routing — stays with the
owner at every level.

The page inside a record is its own component holding the two managers whose
routes answer for a delegate. Mounting the personal section would have put
three more forms on screen that refuse on submit. The family-history card says
whose statement the row is, which the route comment could not.

Both routes carried a long paragraph arguing the write was deliberately
unreachable. It is gone rather than left to send the next reader back to close
what this opens.

The integration coverage moves from the undeclared-mode refusal to a manage
contract: a manage delegate and a guardian each write and the row lands under
the owner with the delegate named as actor, while read and write delegates get
the insufficient-access refusal. Nothing exercised either route at manage
before.
2026-08-07 00:01:27 +02:00
Marc-André Bombeck 0cdfb97a45 test: cover sharing accessibility states
The shared-record accessibility suite had four tests for five states: success
and empty shared one, and neither of them ever entered an empty state. A
browser run reports what it ran and never what it did not, so a state nobody
wrote a test for produced exactly the same output as a state that passed.

The five states are enumerated in a fixture, each test names its state, and a
fast guard reads the spec back against the list — a state that loses its test,
its anchor, or one of its proofs now fails in the ordinary gate rather than at
the point somebody notices the spinner is silent.

Every state runs four proofs: an axe scan, the document reading order, either
a keyboard reach with a visible focus indicator or a live-region announcement
for the one state with nothing to press, and, where the copy is something a
person has to act on, that it renders as content rather than as muted meta and
clears the contrast floor. The retry control on the read-failure card gained a
stable slot so the keyboard proof targets the affordance rather than its
wording.

The eight release journeys are enumerated in the same fixture.
2026-08-06 23:46:43 +02:00
Marc-André Bombeck 71ad8d66b6 fix: preserve sharing failure states
Accepting, revoking and handing back a grant had no failure rendering at all.
The request came back refused, or never left the machine, and the panel did
nothing: the confirm dialog had already closed, the list did not change, and
the only signal was the absence of one. The two refusals that most need a
reader were the two that arrived most quietly — an invitation that had lapsed
before it was opened, and the last guardian being told they may not hand back
a profile that would then have nobody looking after it.

Each refusal now renders on the row it belongs to, announced, in content
contrast, beside a control that runs the same act again. The sentences are
split by what the person does next rather than by status code, so lapsed and
withdrawn read differently and the guardian refusal names the way out. A
request that never reached the server says so instead of reporting a refusal
nobody issued.

The failed state is read off the mutation rather than copied into local state
from onError: the copy is what goes stale, and the mutation already knows
which row it was for. The invitation form keeps every field it had, and only
a successful send clears them.
2026-08-06 23:39:39 +02:00
Marc-André Bombeck c92a80ee4c feat: describe managed sharing activity
Every action a manage-level route files now renders as its own sentence in
the owner's activity view, in all six locales. A write delegate can only add,
so the generic "made a change" was at least the right shape of fact; a manager
changes and removes entries the owner made themselves, and the generic line
withholds the only part that matters — whether a reading was corrected or
deleted, whether a dose history was purged.

The inventory reads the routes and calls the map rather than matching `case`
in its source, so an action that falls through to the fallback fails, and it
follows each resolved key into all six bundles. Every sentence interpolates
the actor's name and nothing else: the audit table is not encrypted and a
value in a verb line would be a health figure in a second store.

The revoke dialog's retention disclosure no longer depends on which of two
requests answers first. `retentionDays` rides the grant list, so a client
holding a row to revoke holds the window it must state, and the branch that
omitted the sentence is gone.
2026-08-06 23:30:07 +02:00
Marc-André Bombeck 73e73469e3 fix(02-12.1): fetch a document's original once, and only to show it
The detail sheet loaded the original twice on open — the inline preview
and the download href each fetched and decrypted the whole blob — and
for a non-inline document the old href had cost nothing until clicked.

- one loader, lifted to the sheet, and only for a document that renders
  inline; everything else fetches at click time
- make the download a real disabled control that reflects a failed
  fetch: href="#" with aria-disabled announces unavailable and still
  activates, and it ignored the failure entirely
2026-08-06 23:04:29 +02:00
Marc-André Bombeck 1d575cf1ae fix(e2e): target the visible date control, not its hidden mirror
data-slot rides {...rest} onto DateField's hidden ISO input, so the
invitation spec asserted visibility on an element that can never be
visible and typed into nothing. Use the primitive's data-testid, which
lands on the overlay a person edits, and assert the mirror took the
value.
2026-08-06 20:58:30 +02:00
Marc-André Bombeck 731203bdf4 fix(02-12.1): reconcile without firing the reads it is holding
- keep the record entries until /api/auth/me has answered: clearing the
  cache under a mounted observer made the held tab refetch at once
- exclude the account cell from the cancellation, and swallow a repeat
  trigger, so a burst of 409s cannot kill the request that ends the hold
- read the account payload network-only; the default staleTime would
  have returned the very context the browser was told is wrong
2026-08-06 20:45:07 +02:00
Marc-André Bombeck 0fee005683 fix(02-12.1): give an unprovable record context a way out
A selector pointed at a record whose grant has lapsed reports the same
disagreement on every /api/auth/me, so routing it to the hydration gate
rendered a permanent spinner with no controls. Expiry reaches this state
on its own: revocation clears the selector inside its transaction and
expiry does not.

- treat an unprovable context as a refusal, which paints the door with
  the button back to one's own record
- check the refusal ahead of the hydration gate, so neither trigger can
  render behind a spinner again
2026-08-06 20:40:31 +02:00
Marc-André Bombeck ffe38c9bb6 fix(02-12.1): reach fenced document routes through the app transport
A browser-issued <img src>, <iframe src> or <a href download> cannot
carry the record-session assertion, so the fence refused all three on
any session that had been inside a shared record — permanently, since
none of them will ever send a header.

- add a structural guard: no client file may name a record-fenced route
  in a src=, href= or window.location sink
- fetch the thumbnail, the inline preview and the download through the
  app transport and hand the DOM an object URL, revoked on unmount
- make the two API probes in the managed-profiles browser spec assert
  the same context the app beside them asserts
2026-08-06 20:36:45 +02:00
Marc-André Bombeck 31b4857d8b feat(02-12.1): carry and verify the record context from the browser
- attach the asserted context to every same-origin request inside the
  three fetch entries, so no call site classifies itself
- discard a response whose echoed context contradicts the adopted one;
  serve one that carries no echo, which is what keeps the public poll
  and every actor surface working
- adopt from /api/auth/me and the switch response, and from nothing else
- send the epoch on every switch and retry once after reconciling
- hold and reconcile on the 409 instead of leaving the record
2026-08-06 19:10:02 +02:00
Marc-André Bombeck 38214e4726 fix(02-12): hold peer tabs during record switches
- Clear scoped reads before the server session changes
- Reconcile every tab with a fresh access response
2026-08-06 15:06:12 +02:00
Marc-André Bombeck a636a3b4c5 fix(02-12): gate shared record navigation
- Wait for the shared-record banner before generated reads
- Keep the profile doorway in the navigation translation namespace
2026-08-06 14:35:02 +02:00
Marc-André Bombeck c42df8cbc3 fix(02-12): gate record preload scope
- Wait for a validated account scope before warming record reads
- Abort scoped preloads and discard handoffs across record transitions
- Cover malformed scope reloads and fresh own-record recovery
2026-08-06 14:32:32 +02:00
Marc-André Bombeck 21e111c06c fix(02-12): refuse malformed record access
- Preserve absent-server compatibility while rejecting published invalid blocks\n- Partition refused record reads and withhold protected presentation
2026-08-06 14:03:13 +02:00
Marc-André Bombeck b844d04676 fix(02-12): reject divergent shared-record state
- Verify the active record matches its canonical switcher entry\n- Skip the owner OCR probe in delegated Labs records
2026-08-06 13:39:37 +02:00
Marc-André Bombeck e568caa2f1 test(02-12): make record switcher journeys deterministic
- Address granted records by their published username\n- Start the profile error mock before record navigation
2026-08-06 08:25:42 +02:00
Marc-André Bombeck c21957db9f fix(02-12): keep record switcher entries reachable
- Constrain the shared-record submenu to available viewport height\n- Scroll long grant lists without hiding delegated records
2026-08-06 08:19:43 +02:00
Marc-André Bombeck 123bc15f16 fix(02-12): restore shared banner contrast
- Use the foreground token for warning context copy\n- Serialize shared-session browser journeys
2026-08-06 05:34:16 +02:00
Marc-André Bombeck 7104578d27 fix(02-12): honor shared scope in module pages
- keep scoped record pages out of actor module redirects\n- suppress unrelated illness reads from document-only records
2026-08-06 05:12:23 +02:00
Marc-André Bombeck acd96a80e9 fix(02-12): gate protected routes on record scope
- resolve account capabilities before protected page queries mount\n- leave public routes outside the record loading gate
2026-08-06 05:04:26 +02:00
Marc-André Bombeck b358cfa3b8 test(02-12): cover record capability hydration
- hold protected content until account capabilities resolve\n- keep public routes outside the protected loading gate
2026-08-06 05:03:22 +02:00
Marc-André Bombeck 6c4dd2d96b feat(02-12): route scoped records to granted domains
- keep shared navigation independent of actor module preferences\n- open scoped records at their first allowed destination\n- announce record access and kind in doorway names
2026-08-06 05:02:37 +02:00
Marc-André Bombeck 34073cf8dc test(02-12): cover scoped navigation presentation
- prove profile uses a read-only shared route\n- cover scoped navigation independent of actor module flags\n- require action, access, and kind in record button names
2026-08-06 05:00:33 +02:00
Marc-André Bombeck 9eab70f032 feat(02-12): add scoped profile summary
- expose bounded read-only profile facts for shared records\n- keep profile navigation separate from Settings mutations
2026-08-06 04:59:25 +02:00
Marc-André Bombeck e2d308ec31 test: cover read-only profile summary
- Render bounded profile details\n- Exclude profile mutation controls
2026-08-06 04:53:28 +02:00
Marc-André Bombeck f0bcb5456c test: name managed settings module key
- Avoid a reserved module variable in the settings inventory test
2026-08-06 04:29:38 +02:00
Marc-André Bombeck 02aa61c9db fix: preserve access payload readers
- Read resolved level and record kind in the record switcher\n- Keep payload consumer coverage aligned with record presentation
2026-08-06 04:27:06 +02:00
Marc-André Bombeck f69e1d7dec feat: add managed settings navigation
- Show only guardian settings for managed records\n- Keep record-scoped settings and exit context visible
2026-08-06 04:23:17 +02:00
Marc-André Bombeck ba3be7ba9b feat: distinguish managed record access 2026-08-06 04:18:05 +02:00
Marc-André Bombeck 908897cc6b test: cover shared record modes 2026-08-06 04:17:58 +02:00
Marc-André Bombeck afae57207c feat: scope shared record navigation 2026-08-06 04:13:59 +02:00
Marc-André Bombeck 437d916fc6 fix: guard managed setting label inventories
- Treat inherited property names as unavailable settings

- Preserve legacy tile aliases only for declared entries
2026-08-06 04:01:00 +02:00
Marc-André Bombeck 6a1d1dd6e8 fix: preserve managed target defaults
- Return effective threshold ranges after managed updates\n- Localize closed Coach and Insights display inventories
2026-08-06 03:52:49 +02:00
Marc-André Bombeck 247ac8494c fix: complete managed record settings
- Expose canonical defaults and reject invalid settings families\n- Localize Guardian controls and refresh dependent caches
2026-08-06 03:40:13 +02:00
Marc-André Bombeck e788675b82 feat: configure managed record settings
- Scope writable settings to the selected managed record\n- Render typed Guardian controls with separated audit attribution
2026-08-06 03:21:02 +02:00
Marc-André Bombeck 03ee01ba56 feat: show managed integration status
- Render a record-keyed status view for managed profiles\n- Refuse shared Settings links before actor controls mount\n- Localize intentional unavailable states
2026-08-06 02:38:54 +02:00
Marc-André Bombeck d86358f3e4 fix: recover managed sharing boundaries 2026-08-06 00:19:35 +02:00
Marc-André Bombeck 8669df4cda feat: add scoped record invitations 2026-08-05 23:58:30 +02:00
Marc-André Bombeck 4923db6464 fix: preserve account access compatibility 2026-08-05 23:58:26 +02:00
Marc-André Bombeck ab14fdb6f3 Let an invitation name its sections and offer management
An owner can now narrow a grant to the parts of their record they mean to
share, and can offer the third level. Both decisions are made once, on the
invitation, and neither widens afterwards.

The invite schema takes `scope`: absent or null is the entire record, which
is what every stored grant means and what an owner who does not narrow still
gets. An empty array, a key outside the vocabulary and the whole-record
sentinel are each refused with a 422 naming the field; `inviteGrant` refuses
the same three underneath, and its `invalid_scope` now has an HTTP mapping
instead of falling into the generic refusal.

Offering MANAGE asks for a fresh second factor when the account has one. That
gate resolves through the session cookie and has no Bearer fall-through, so a
token caller is refused first with `sharing.invite.manage_browser_only` rather
than being told it is not authenticated by a route that had just
authenticated it. The consequence is deliberate: management of a health
record is offered from a browser.

The account payload gains `sections` and `level` beside `access` and
`canWrite`, resolved server-side and ordered as the consent screen reads.
`access` was `grant.access === "WRITE" ? "write" : "read"`, which would have
published a grant that can delete entries as read-only; it is a total map
now, so the next level cannot ship silent. The capabilities hook carries both
facts and turns neither into an affordance yet — the edit and delete controls
come back in the release where the routes behind them answer.

The consent surface asks two questions in order rather than laying out a
nineteen-cell grid. The level is three radios, narrowest preselected. What
they can open is two radios, the entire record preselected, and only the
narrow branch expands the eight sections with a sentence each. Both honest
limits are stated above the checkboxes, where the decision is being made: a
section shares everything written in it, and the cross-record surfaces do not
appear for a narrowed grant. Documents carries its own warning, cycle appears
only for an owner who tracks it, and Manage collapses the question to the one
line that answers it.

The Manage sentence says the other person can add, change and remove entries,
including ones the owner entered themselves, and says in the same breath what
it does not reach. The acceptance side gets its own responsibility line rather
than the write one wearing a stronger adjective.

Six locales.
2026-08-05 23:37:00 +02:00
Marc-André Bombeck d07b9249c8 fix: complete dashboard hydration 2026-08-05 23:03:17 +02:00