Switching a chart from 7 to 30 days moved steps, active energy,
flights and distance totals onto neighbouring days. The 7-day range
buckets raw rows by the user's calendar day; the 30-day range read the
DAY rollup tier, whose buckets are UTC days, and the chart labelled each
bucket's UTC-midnight start in the user's zone. East of UTC a sample
shortly after local midnight counted on the previous day; west of UTC
every bucket read one day early.
The daily series (the chart read and the batched dashboard read) and the
aggregate=daily|weekly|monthly list now fold live in the user's zone and
return the instant of local midnight, so every range and every summed
metric shares one day boundary. The fold runs in Postgres and returns
one row per day; about 0.1 s for 90 days of per-minute samples. Windows
longer than the DAY cap still step up to the coarser rollup tiers.
Refs #1026
The Coach availability fold and the AI analysis carry-over for people
who hid the Coach are read out of the migration and run twice against
rows shaped like an upgrading instance, including a malformed
preference blob and the concurrency token.
Each state changes one layer against a fully set-up account and checks
the capabilities it owns: master switch, Reading documents, no
provider, the operator's key with and without consent, hiding the
Coach, the AI analysis opt-out, the workouts module, the single Coach
switch, a delegate, and a guardian limited to the operator's key.
AccountPayload gains ai with the AiCapabilities, AiCapabilityState,
AiUnavailableReason and AiProviderState components. The feature-flags
route is marked deprecated, drops correlations and documents
documentAi. The error envelope's meta documents capability, reason and
module, lists the assistant.disabled switches, and the catalogue
carries the three new ai codes.
GET /api/auth/me gains ai: the state of every capability and the
provider block, resolved for the active record and masked to the
grant like moduleAccess. The authority comes from the switch the
payload already resolved, so a delegate reads not_permitted_for_record
and a guardian is limited to the operator's key.
useAiCapability is the web reader and fails closed until the payload
arrives. GET /api/feature-flags becomes a deprecated projection of the
operator switches with a Deprecation header and a link to its
successor.
loadAiCapabilityInputs reads the switches, module access, provider
presence and active consent kinds for one record in parallel, memoised
per request, and fails closed to check_failed. requireAiCapability,
getAiCapability and aiCapabilityForJob resolve for the request's
record, masked to the active grant like moduleAccess, or for a job's
record under the worker's authority.
apiHandler renders AiUnavailableError as one envelope with errorCode,
capability, reason and module. Existing codes stay: the operator
family keeps assistant.disabled.<switch>, modules keep module.disabled,
consent keeps consent.ai.required. New codes are ai.record.notPermitted,
ai.provider.none and ai.unavailable.
probeProviderChain describes the chain that would serve a record
without decrypting, building a client or refreshing a token: which
entries hold a credential, in chain order, whether each reads images,
the in-browser OCR opt-in, and where the credential comes from. It
applies the provider-work authority, appends the central Codex only
behind the opt-in, and falls back to the legacy provider only for an
empty chain. hasAnyConfiguredProvider and resolveProviderAvailability
now answer through it.
The operator switches become master, Coach, Daily Briefing, Status
notes and Reading documents. Correlations goes: it gated statistics,
not anything a model writes. Reading documents covers the document
vault, lab scans and medication extraction.
The Coach module's operator availability now follows the Coach
switch, so there is one place to turn the Coach off server-wide. The
migration folds a stored Coach availability of false into the switch,
and gives everyone who hid the Coach the AI analysis opt-out, since
hiding the Coach no longer stops nightly briefing and status notes.
A switch read that fails now fails closed. The insights module is
described as AI analysis in every locale, and the switch guard now
keeps both retired switches out and freezes the callers of the old
operator-only gate.
A pure resolver turns operator switches, module access, provider-work
authority, provider presence and consent receipts into one state per
capability, reporting the outermost reason. The document provider rank
moves to its own module so the resolver and the document routes read
the same order.
One frozen table names every AI capability with the operator switch,
owning modules, consent rule, modality and provider order that decide
it. The reason enum is listed in precedence order. No I/O, so the
resolver, gates, contract and guards can all depend on it.
The Verlauf ledger dropped doses the full history showed. Three windows
ended too early or started too late:
- The server clamped the window floor to the medication's creation
instant and filtered recorded doses by the same floor, so a dose
logged for earlier the same day on a medication added afterwards
fell out of "Last 90 days". The creation floor now bounds minted
slots only; recorded doses use the requested window.
- A dose taken inside its window before the slot's own time is stored
on the slot anchor, which still lay past the window end. The read
horizon now reaches the latest recorded anchor within a day, without
minting later upcoming slots.
- The client pinned from/to at mount and reused them on every refetch,
so a dose added after the tab opened stayed hidden until a reload.
The window is now resolved when the request fires and only "from"
travels; the query key carries the window length instead of instants.
Refs #1028
The iOS client left public beta. The badge row, the navigation lines and
the iOS section named TestFlight only, which now reads as the single way
in. Both routes stay, with the price and the free-code offer stated
rather than left for someone to discover on the store page.
The archive member was matched against the English lowercase spelling
alone. iOS writes that file name in the phone's language and some
versions capitalise it, so an ordinary export could arrive as
Export.xml or a translated name and was refused with a message telling
the reader their export was invalid.
The basename now matches in any capitalisation, and a single XML member
at the top of the archive is taken as the translated spelling. Apple's
clinical-document file never qualifies: it sits beside the real export
and would parse to nothing. When no member qualifies the error lists the
XML members the archive does hold, so the next report arrives with the
answer in it.
Refs #1008
Picking a symptom without tapping an intensity is how most of them are
logged. The sheet sends severity null for those, the schema took a number
or nothing at all, and the day log came back 422 with two issues for two
symptoms. All the sheet could say was "Could not save. Try again.", and
the cycle saved fine as soon as the symptoms were removed, which sent
people looking in the wrong place.
Nothing else disagreed: the link row has always stored an unrated symptom
as a plain presence, and the writer already maps a non-number to NULL.
Only the gate at the door refused it, on both the create and the patch
path, and no test touched either schema.
Refs #1001
Shipped iOS builds multiply oxygen saturation and body fat by 100 before
upload. The importer scaled them again, so 0.97 was stored as 9700, the
plausibility guard rejected the row as out of range inside an HTTP 200,
and the client treats that skip as final and moves its HealthKit anchor
past the sample. Every reading of those two types was lost, silently, on
every install that syncs them.
The contract says values travel raw and the server scales, and that is
what the OpenAPI description publishes. One comment in the schema said
the opposite, that the client must multiply by 100 first, and that is
the line the app was built against. It is corrected here.
The five percent-shaped HealthKit identifiers now take either spelling:
a value above the fraction ceiling is already a percent and passes
through, anything at or below it is scaled. The domains do not overlap,
and the per-type range guard still rejects what is impossible either
way. Current app builds stop losing readings without an app release.
The tests pin both spellings, the range after conversion, and that the
tolerant converter covers the percent class and nothing else: waist
circumference uses the same factor to turn metres into centimetres, and
a blanket change made a 1.02 m waist into 1.02 cm.
Refs MBombeck/healthlog-iOS#7