Six Apple Health signals leave the deferred list and ride
POST /api/measurements/batch and the export.zip import end to end:
EXERCISE_MINUTES, STAND_MINUTES and STAND_HOURS (the exercise and stand
rings), MINDFUL_MINUTES, STATE_OF_MIND_VALENCE (iOS 18 State of Mind,
-1..1) and AFIB_BURDEN (AFib History, percent of the week). The ring
types and mindful minutes are cumulative, so the daily rollup is their
sum; AFib burden is scaled strictly from Apple's fraction and a value
above 1 is refused rather than guessed at. The archive import takes
mindful minutes from the session's dates and skips idle stand hours.
Every registry carries them: units, ranges, labels in all locales,
categories, chart tokens, report catalogue (mindful minutes and the
valence fenced as sensitive), signal registry with FHIR codings, and
MCP exposure for the three ring types only.
Workout GPS tracks are now an opt-in, off by default
(PATCH /api/auth/me/workout-routes, a switch on Data & Privacy). While
it is off, the workout batch route and the export import keep every
workout and drop its track with the same per-entry status. With it on,
the import reads the referenced GPX files from the archive one bounded
member at a time, thins each to the point cap and stores it sealed.
The climb a track carries is written to Workout.elevationM at write
time, so the workout insight no longer opens the track, and the AI
egress guard now freezes the set of files that read route geometry.
`User.glucoseUnit` has existed since v1.2 and around thirty surfaces read
it — the dashboard tiles, the glucose page, the targets panel, the CSV and
FHIR exports, the doctor report, the Coach snapshot, the low-reading push
copy. Nothing could ever write it. No route, no schema, no field on any
form, so every account resolved to the mg/dL default and the mmol/L half
of the app was unreachable code. Anyone who reads glucose in mmol/L
converted in their head on every screen.
This is the missing end: a `GET`/`PATCH /api/auth/me/glucose-unit` scalar
on the pattern the unit-preference endpoint next door established, and a
dropdown in the profile form beside it. Deliberately its own control
rather than a branch of metric/imperial: metric countries are split on
which unit they read glucose in, so one dropdown would get half of them
wrong.
Storage is untouched and stays untouched. Every reading is written in
mg/dL by every ingest path and the unit only selects a display branch, so
nothing already recorded is reinterpreted. The entry form moved with the
preference, which is the half that could have hurt: it asks in the chosen
unit and inverts through `toCanonicalMgdl` on submit, so a 5.3 typed by an
mmol/L reader is filed as 95 mg/dL rather than as 5.3 — a value the
plausibility band accepts in silence and every surface reads back as a
severe hypo.
The contract gains the endpoint and nothing else; the account payload
already carried the field.
Twenty-eight more verbs that exist, are called, and said nothing in the
contract: registration, password rotation, the profile patch, the SSO entry
point, passkey enrolment, the Codex device-auth flow, the device list and its
revocation cascade, four per-user preference scalars, the email channel and the
four channel self-tests, the record wipe, the privacy facts, the update check,
and the Web Push transport.
Four of them needed more care than the rest.
`GET /api/auth/oidc/login` is documented as the two-armed route it is. The
browser arm redirects to the provider; the native arm takes `client=native`
plus a mandatory S256 challenge and returns to a compile-time scheme with a
one-time code — or an MFA ticket when the account has a second factor. The two
PKCE exchanges in play are named apart, because confusing them is the way to
get this wrong.
`POST /api/auth/register` is the only genuinely anonymous surface in the set,
and its refusals are stated exactly: the SSO-only block, the 5-per-15-minutes
bucket that collapses rather than falls open, the invite that is consumed
whenever it is sent, the zero-user bootstrap that mints the first admin under a
lock, and the one 409 that covers a taken email and a taken username so it
cannot be used to enumerate accounts. `POST /api/auth/password` is a change
behind authentication, not a reset, so it is described as one.
`DELETE /api/settings/data` names what it takes and what it leaves: ninety-nine
models hard-deleted, the account's own personal columns reset, credentials and
interface preferences kept, one transaction, and a confirmation string that is
deliberately not the one account deletion wants.
Two collisions surfaced while wiring this up and are resolved rather than
worked around. `PUT /api/auth/profile` and `PATCH /api/user/profile` are two
doors onto one handler, so they now share one published request component
instead of two descriptions of one validator. Their RESPONSES differ, though —
one carries id and role, the other the display preferences, and neither is a
superset — so they are named apart and the older description no longer claims
to cover both.
The four Web Push and APNs surfaces land in their own route module rather than
beside the notification preferences: that module answers what gets sent and
whether it arrived, this one the plumbing a client sets up and then verifies.
Both self-tests report failure inside a 200, which the contract says plainly,
because a client reading only the status will tell someone their notifications
work when they do not.
The generated YAML is left out; it is regenerated once the remaining groups
land.
`pnpm openapi:check` compares the registry against the committed YAML and
never the routes against the registry, so a route that was simply never
registered stayed invisible to the gate. Sixteen of them are called by the
native client and describe nothing: sign-out, the three per-user preference
scalars, the two pre-session discovery reads, passkey login options, the
passkey rename and delete, registration status, the consent latest-receipt
reader and its revoke, account deletion, the instance service switches, the
API-token list and revoke, the threshold overrides, the version read, and the
assistant feature matrix.
Each contract is derived from the handler rather than from its docblock, so
the published statuses are the ones the code can actually produce — the 413 on
a 1 KB body cap, the 400 the consent family answers where the rest of the API
answers 422, the 403 an instance-wide API switch produces on a token READ, the
last-admin and last-Guardian refusals on account deletion, and the fresh-factor
arm that a Bearer transport cannot clear at any scope.
Four request bodies moved to the validation modules the handlers now parse
with, so the published wire cannot drift from the runtime one: the three
`/api/auth/me` preference scalars into a new `user-prefs` module, and the
passkey rename beside the login schemas. Behaviour is unchanged.
The generated YAML is left out; it is regenerated once the remaining groups
land.