Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
f521b03
feat: let a guardian edit a managed record, and record its sex
MBombeck Sep 9, 2026
fb1cb4e
feat: gate a switched browser on the record's modules, not the actor's
MBombeck Sep 9, 2026
90ca2cd
chore(api): regenerate the OpenAPI contract
MBombeck Sep 9, 2026
bed2ab5
fix(test): read the audit details as the string the writer stores
MBombeck Sep 9, 2026
ccca056
feat: let the record's module map filter its own navigation
MBombeck Sep 9, 2026
617beb4
test(e2e): cover the medication-adherence journey end to end
MBombeck Sep 9, 2026
0cccb4d
fix(sharing): keep a scoped grant out of the record's module configur…
MBombeck Sep 9, 2026
8ff1b86
fix(settings): send a managed record's cycle flag only when it moved
MBombeck Sep 9, 2026
0bae481
docs(api): say which account payload fields describe the record
MBombeck Sep 9, 2026
038fc72
fix(settings): keep a managed record's edit form across a refetch
MBombeck Sep 9, 2026
c673255
fix(settings): stop a timezone edit writing the actor's language into…
MBombeck Sep 9, 2026
0c46df1
fix(api): put a ceiling on editing a managed record's identity
MBombeck Sep 10, 2026
7b06a23
test(e2e): cover doctor-report generation end to end
MBombeck Sep 10, 2026
d529d59
test(e2e): move the adherence journey onto its own account
MBombeck Sep 10, 2026
f544cae
test(e2e): read the card's rate once, and keep the journey inside one…
MBombeck Sep 10, 2026
c39bf0d
test(e2e): name the tile's disagreement about a skip, and assert both…
MBombeck Sep 10, 2026
db8ac95
test: cover the tile's rate attribute in the unit gate, and correct t…
MBombeck Sep 10, 2026
33f5bed
test(e2e): give the doctor-report journey its own account
MBombeck Sep 10, 2026
19bee96
test(e2e): prove the report card is closed by the destination, not th…
MBombeck Sep 10, 2026
094eb13
test(e2e): read the report's period line in the account's date order
MBombeck Sep 10, 2026
923b832
test(e2e): correct three claims the report journey's comments make
MBombeck Sep 10, 2026
50a65b6
test(e2e): cover the second-factor journey end to end
MBombeck Sep 10, 2026
08100b6
fix(api): put Retry-After and the rate-limit triple on every 429
MBombeck Sep 10, 2026
1d2744a
fix(api): give every offset-paged list a unique tiebreaker
MBombeck Sep 10, 2026
3e063f4
fix(medications): a skipped dose leaves the dashboard tile's denominator
MBombeck Sep 10, 2026
fd8fc0e
docs(api): publish `details.issues` and open `meta` on the error enve…
MBombeck Sep 10, 2026
0cd996e
docs(api): declare the two sync domains that were already on the wire
MBombeck Sep 10, 2026
4993352
test(api): parse real responses against the schemas that publish them
MBombeck Sep 10, 2026
a7bdf14
test(api): pin the pagination tiebreaker in the two route tests
MBombeck Sep 10, 2026
06d2104
Merge managed-profile editing, sex at creation and module switches pe…
MBombeck Sep 10, 2026
89ec49c
Merge the medication adherence end-to-end journey
MBombeck Sep 10, 2026
a792ac0
Merge the doctor-report end-to-end journeys
MBombeck Sep 10, 2026
18302c6
Merge the skip parity between the two compliance engines
MBombeck Sep 10, 2026
21dd7c7
fix(api): give the generic auth refusals a stable errorCode
MBombeck Sep 10, 2026
47a5079
feat(api): say why a module's surfaces are not there, not only that t…
MBombeck Sep 10, 2026
7c9740c
Merge the per-module access reason on the account payload
MBombeck Sep 10, 2026
3641083
fix(settings): close a confirmation when its request settles
MBombeck Sep 10, 2026
b428fcf
test(e2e): read the replay verdict instead of inferring it from a 401
MBombeck Sep 10, 2026
78789e0
test(e2e): measure the login cap the journey keeps clearing
MBombeck Sep 10, 2026
01fe007
test(e2e): take the TOTP parameters and the field hooks from the source
MBombeck Sep 10, 2026
5cb5c7d
test(e2e): drive backup and restore end to end through the settings s…
MBombeck Sep 10, 2026
8865e1b
Merge the second-factor end-to-end journey and the confirmation dialo…
MBombeck Sep 10, 2026
dd3844a
fix(api): dress only the 429s a limiter actually refused
MBombeck Sep 10, 2026
dd8a724
fix(api): floor Retry-After at one second on a refusal
MBombeck Sep 10, 2026
2c63b0c
fix(api): give the remaining 401 throws a code, and say what the code…
MBombeck Sep 10, 2026
42100d9
fix(api): finish the pagination tiebreaker, and state the tie premise…
MBombeck Sep 10, 2026
cf9e8b8
docs(api): declare the 429 headers instead of only describing them
MBombeck Sep 10, 2026
6f71812
test(api): use the real caps in the limiter mocks, and drop a stale p…
MBombeck Sep 10, 2026
6a2282f
Merge Retry-After on every refused request, stable list pagination an…
MBombeck Sep 10, 2026
6b366b4
fix(backup): make the console restore leave instance settings alone
MBombeck Sep 10, 2026
d3f0874
fix(backup): refuse a stored copy that will not open with 422
MBombeck Sep 10, 2026
fd176f6
fix(backup): say what the backups console actually does
MBombeck Sep 10, 2026
6e4f3fb
docs(ops): what a restore replaces, and the console path
MBombeck Sep 10, 2026
8be51d9
test(e2e): cover notification preferences through to the dispatch dec…
MBombeck Sep 10, 2026
343d639
test(e2e): say what the backup journey really touches, and pin it to …
MBombeck Sep 10, 2026
f3321b8
feat(api): let the admin reminder sweep name one account
MBombeck Sep 10, 2026
fd6fa18
feat(api): publish the resolved notification preferences on the accou…
MBombeck Sep 10, 2026
ee73e84
Merge the backup and restore end-to-end journey, the restore that lea…
MBombeck Sep 10, 2026
74dbb1a
test(e2e): give the notification journey a server and a scope of its own
MBombeck Sep 10, 2026
8b1ca7e
Merge the notification dispatch end-to-end journey, the account-scope…
MBombeck Sep 10, 2026
c64708c
test(ci): pin the third end-to-end server the notification journey dr…
MBombeck Sep 10, 2026
9e1f64e
fix(admin): keep the worker-status row readable with no worker present
MBombeck Sep 10, 2026
17671d9
Merge the AA-contrast status rows on the admin page
MBombeck Sep 10, 2026
8d418bc
chore(release): v1.38.15 — the rest of #939, one adherence figure, an…
MBombeck Sep 10, 2026
56b6ec7
test(e2e): read the tile's rate after the skip parity, one figure on …
MBombeck Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,167 @@
# Changelog

## [1.38.15] — 2026-09-10

The rest of #939 for managed profiles, one figure for medication adherence
on every surface, and four more journeys in the gate.

### Added

- **A guardian can edit a managed record, and record its sex (#939).**
`PATCH /api/managed-profiles/{id}` accepts name, date of birth, language,
timezone and sex; the creation form asks for sex as well, so cycle tracking
can derive from it. Both arms require the fresh second factor the create
and delete routes already do, refuse with 404 rather than 403 for a record
the caller does not manage, and the edit is capped at ten an hour per
account. Every field is documented; `rejectedFields` says which value the
server would not take.

- **Modules are switched per record.** `GET`/`PUT /api/record-settings/modules`
read and write the module map of the record the browser is acting on, and
`GET /api/auth/me` under an active switch reports `modules` and
`cycleTrackingEnabled` for that record, not for the caller. Navigation
follows the record's own map, so a guardian sees the doors the child's record
has open. Under a scoped grant the two fields are masked to the sections the
grant names: a delegate scoped to measurements does not learn whether the
owner tracks their cycle, screeners, illness episodes or supplements, and the
cycle flag, which derives from the owner's recorded sex, answers `false`
outside a grant that opens it. Own record and unscoped grants are unchanged.
Documented on the account payload, which now says which fields describe the
caller and which the record.

- **`moduleAccess` says why a module is off.** Beside the boolean map,
`GET /api/auth/me` carries one of `enabled`, `disabled`, `not_granted` or
`unavailable` per module, in that order of precedence from the bottom: the
operator's instance-wide switch, then the grant's sections, then the
record's own choice. The booleans keep their exact meaning (`modules[key]`
is `moduleAccess[key] === "enabled"`, asserted over every grant shape). The
module-off notice on the web names the reason and offers the switch only
when the record itself turned the module off; a native client can show the
same sentence instead of a silently missing row.

### Fixed

- **A skipped dose is a pause, not a miss, on the dashboard too.** The
medication card, the dose history and the doctor report have excluded a
deliberate skip from the denominator since v1.15.9; the dashboard tile's
schedule-anchored engine counted it as expected-and-missed, so the same day
read two different rates. The tile engine now subtracts the day's skips,
capped at the minted slots, and a wholly skipped day drops out of its
buckets exactly as it drops out of the ledger. One test feeds both engines
the same fixture and asserts one figure.

- **Saving the Modules card no longer freezes a record's cycle derivation.**
Every save sent `cycleTrackingEnabled`, which wrote an explicit boolean over
the derivation from sex; a record without a recorded sex that later got one
stayed off with nothing on screen saying why. The flag now rides only when
it moved.

- **A refused second-factor action no longer hides its reason behind the
dialog.** Regenerating recovery codes or disabling the authenticator asked
for confirmation in a dialog whose confirm handler kept it open; when the
server refused (a stale step-up, most often) the message painted on the card
underneath the overlay. Both dialogs are controlled now and close when the
request settles, whichever way it went. Found by the second-factor journey
below.

- **The managed-record edit form keeps what you typed across a refetch.** It
was keyed on the query's timestamp, so any invalidation of the profile list
remounted it and dropped the input; it is keyed on the record now. A save
that only moved the timezone also wrote the actor's language into a record
with none recorded; the language is compared against what the form seeded.

- **The admin status rows read at AA contrast in every state.** With the
worker absent (a split deployment serving the page from the web process,
or a dead worker) the status row rendered its text in the destructive
colour at 3.97:1 on the tile's wash, below the floor, and the
accessibility suite went red in exactly that state. Status text is
foreground now with the tone on the indicator, the pattern the medication
rows already use; the database and telemetry rows moved with it.

- **The "my phone owns the reminders" chip can render now.** The medication
notification settings read `notificationPrefs.medication.clientManaged`
from the account payload, which never carried it, so the chip that tells
a person the server-side switch decides nothing for their paired phone
could not appear; the write path and the dispatch decision were right all
along. `GET /api/auth/me` publishes the resolved `notificationPrefs` now.
The account-payload guard checked only that every published field has a
reader; it checks the other direction too, so a reader of a field the
payload does not publish fails a unit test.

- **The admin reminder sweep can name one account.**
`POST /api/admin/notifications/reminder-check` accepts an optional
`userId`; without it the sweep stays instance-wide as before. Documented.

- **A console restore leaves the instance's settings alone unless asked.**
Restoring an account's snapshot from `/admin/backups` also rewrote the
singleton instance settings row whenever the snapshot carried one, as a
side effect of a per-account action. The write is behind an explicit
checkbox beside the typed confirmation now (`restoreInstanceSettings`,
default off); the disaster-recovery case keeps the capability, the routine
case no longer touches what every other account shares. The runbook says
what a restore replaces (rows created after the snapshot are removed) and
describes the console path, which it did not before.

- **A stored copy that will not open is a 422, not a 500.** A tampered
archive, or one written under a key the instance no longer holds, made the
restore, the download and the preview fail as internal errors. They answer
422 with `meta.errorCode = backup.payload.undecryptable` and are in the
API document, the restore with its `Idempotency-Key`.

- **Every refused request says when to come back.** No 429 the rate limiter
produced carried `Retry-After`, and 129 of the 199 limited routes sent no
rate-limit headers at all, the four batch endpoints, `/api/sync/changes`
and `/api/auth/refresh` among them; an offline queue that tripped the cap
got prose and nothing else. The limiter's refusal is now recorded per
request and `apiHandler` attaches `Retry-After` (whole seconds, at least
one) and `X-RateLimit-Limit`, `-Remaining` and `-Reset` to exactly the
429s the limiter produced; a 429 relayed from an AI provider or raised by
a budget keeps its own shape. The 429 response in the API document lists
the headers.

- **Offset pagination cannot skip or repeat a row any more.** Seven lists
ordered by a timestamp that ties across types (measurements, labs, the
measurement drill-down, custom-metric entries, medication intake in both
arms, mental-health assessments) now break the tie on `id`, so two pages
of a history pull are disjoint and complete. A test seeds ties and pulls
two pages.

- **The published error envelope allows what the server sends.**
`ErrorEnvelope` closed the object and omitted `details.issues`, the field
the multi-issue 422 was built for; `SyncChangesResponse` omitted
`cycleDays` and `cycles`, sent on every pull. Both corrected, and a test
now parses real responses of `/api/sync/changes` and a multi-issue 422
against the schemas that publish them, so a shape drift fails the gate.

- **Generic auth refusals carry a stable `errorCode`.** The session, Bearer,
admin, step-up and MFA-code refusals name their reason in `meta.errorCode`
(`auth.session.missing`, `auth.mfa.code_invalid`, …), documented on the
401 and 403 responses; a route's own credential check may still answer
with prose alone, and the description says so.

### Changed

- **Four more journeys in the gate.** Medication adherence from the wizard
through take and skip to the card, the history and the dashboard tile, on a
two-weekday plan against its daily twin; doctor-report generation with the
PDF's text asserted (period, blood-pressure section, medication, allergy),
an empty window that still names its span, and the manage boundary for a
delegate; the second factor from enrolment through a fresh sign-in, the
step-up window (accepted while fresh, refused once the stamp is aged),
a wrong code, a replayed code, and a recovery code that works exactly
once, with the server's own replay verdict read from the audit log rather
than inferred from a 401, and the login limiter's documented ceiling
asserted with its headers; backup and restore through the settings
surfaces, with the archive header asserted as bytes, a reading and a dose
deleted and restored under their original ids, a row added after the
snapshot gone, a tampered copy refused, and a read delegate kept out;
notification preferences through to the dispatch decision, read off the
account's own delivery ledger (one email attempt against a local mail
responder, none for the switched-off channel, the APNs arm skipped for
the documented reason, and a private-range URL refused with nothing
written). Each on its own account, each with a refusing control, each broken
deliberately once to prove it can fail.

## [1.38.14] — 2026-09-09

Two reported bugs fixed at their class, and the release gate stops crying
Expand Down
Loading
Loading