chore(release): v1.38.15 — the rest of #939, one adherence figure, and an API that says when to come back

Version anchors, the OpenAPI document and the changelog entry. Refs #939.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 07:45:55 +02:00
1 parent 17671d9dc1
commit 8d418bc6f8
4 files changed
+165 -3

No files matched your search

+162
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.38.14
version: 1.38.15
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.38.14",
"version": "1.38.15",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
+1 -1
View File
@@ -36,7 +36,7 @@ try {
// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild.
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
/* @sw-version-fallback */ "v1.38.14";
/* @sw-version-fallback */ "v1.38.15";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`