Home monitoring reads morning and evening readings as separate sets, so
the blood pressure page now shows the morning, midday and evening means
side by side over the chart's range, each with its count and pulse, and
the morning-minus-evening difference. A control under them narrows the
chart to one part of the day; the choice lives in `?partOfDay=` so it
survives a reload and a shared link, and the whole day keeps a bare URL.
The split is resolved on the server in the record's own time zone and
published as one response, `GET /api/measurements/blood-pressure/time-of-day`,
so every client renders the same numbers. Morning is 04:00 to 11:59,
midday 12:00 to 17:59, evening 18:00 to 03:59: the guidelines define the
sets by waking and bedtime rather than by the clock, so the windows are
wide, and a reading after midnight is a late evening one. Pairs follow
the blood-pressure series' rule (same source within five minutes); the
pulse is the reading's own. A part shows a mean from three readings on.
`GET /api/measurements` takes `partOfDay` on the blood-pressure and pulse
series read: each local day folds only that part's readings, a day runs
from 04:00, and long ranges fold weekly instead of reading rollups that
cannot tell the hour.
Refs #1331
Roles are cluster-wide. Two HealthLog databases on one server shared
healthlog_app: each start set its own password, locking the other
instance out, and the role held grants on both databases. The role is
now healthlog_app_<database>_<hash>, marked with a comment naming its
database, its password is derived per database, and a role holding an
explicit CONNECT on another database is not used.
The role owned the pgboss schema, so it could plant a trigger, a rule or
a function body that a later owner session (a rollback, DB_APP_ROLE=off,
a fallback start) would run with the owner's rights. pg-boss changes its
schema only on install, on migration and for partitioned queues, which
the app does not use. The start now installs and migrates the schema as
the owner with pg-boss's own plans, and the worker runs pg-boss with
migrate: false when it does not own the schema. The role gets rows only,
read-only on pgboss.bam (SQL the migration runner executes) and on the
version column. Every start, on every path, takes back what a
HealthLog role owns there, drops triggers and rules, and after a
take-back restores the queue functions to pg-boss's own definitions and
retires the legacy role. Bloated queue indexes, which pg-boss only
reports when it does not own them, are rebuilt at start.
- the AI-state cases take their baseline in a world of their own
instead of from the S0 case;
- the live-window case drops a rollup tier the rollup case built;
- the job readers start each case on an empty queue, and the
no-schema case starts pg-boss again after it;
- the server move case reseeds the server it emptied;
- the extremes cases rebuild the tier one of them breaks on purpose;
- the first Google Health backfill seeds its own history;
- the batch budget warms the process caches as the reads do;
- the HOT-update count is read as a difference: since Postgres 15 the
transaction view also shows an earlier transaction's unflushed counts;
- the MANAGE matrix checks its effect set after all cases rather than
in a case of its own that a shuffle could run first.
The migration before-and-after files, the dump directory, the geo
backfill's three passes, the settings trip, the backup parity steps,
the lab conversion record and the pulse method's dry run before its
rebuild are each one sequence by design; they now say so and keep their
order with shuffle: false. The CSV import's zone and crash cases wrote
into the account the lab and medication walks count, and now use
accounts of their own.
A parity file is one account walked through a route's cases, each
request reading what the earlier ones stored, and the recorded files
pin that sequence. The nightly shuffle reordered the steps and every
snapshot after the first moved. Keep each walk in order with
shuffle: false, wrapping the files whose top-level blocks build on each
other, and note the rule for order-bound files in the testing guide.
Two draws of one analyte a year apart and two reports on the same day, against
Postgres: each report lists its own reading id, and fetching it returns that
reading with that report in its links, not the latest one.
A marker minted from a text result ("SEE NOTE") has an empty unit, and a
marker with a reading has its unit locked, so the first number to arrive,
a unitless ratio included, was refused against a unit nothing had chosen.
While every reading is text the unit has nothing to contradict, so the
first number sets it. A marker that already holds a number keeps its unit.
Refs #1313
After the migrations the entrypoint ensures healthlog_app, a login role
with row access to the app's schema, MAINTAIN on measurements and
ownership of the pgboss schema, and starts the app with a DATABASE_URL
for it in place of the owner's. The role cannot alter or drop tables,
create roles, schemas or functions in the app's schema, read server
files or run programs, or write the migration history.
The password is an HMAC of a fixed label under API_TOKEN_HMAC_KEY, so a
separate worker container derives the same one, and it is sent as a
SCRAM verifier. Every boot re-applies the grants in one transaction
under an advisory lock, and drops as the owner the indexes the
maintenance job may no longer drop. When the owner cannot create roles,
the server is older than Postgres 17, or the role cannot log in, the
boot says why and the app stays on the owner as before. DB_APP_ROLE=off
skips it.
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the
login, register, token and session paths, mirroring columns every
migration under prisma/migrations already creates. The entrypoint applies
those migrations before the server listens and stops the boot when one
fails, so the mirror never had work to do, and DDL on a request path
needs the table's owner: a connection that may only read and write rows
fails every login on it. Drop the module, its call sites and the mocks
that stubbed it out.
A ratio (cholesterol/HDL, albumin/globulin) or an index such as INR has
no unit, and every lab write path refuses a numeric value without one.
That refusal stays: an empty unit usually means the extraction dropped
it. Instead the value is stored as the UCUM unity "1", chosen on purpose.
The unit field when a marker is defined, on a scanned report's row and
in the document review edit now carries a "No unit" checkbox that sets
"1". The CSV import takes 1 in the unit column, as the template and the
import sheet say; an empty cell is still refused, and other spellings
such as "ratio" or "-" stay the literal unit they always were.
"1" compares as a unit of its own: it matches only itself, so it is
refused under a marker kept in mg/dL and the other way round.
Refs #1313
Source system and id without the address, the Documents switch, the empty
answer, the cap, document links to labs and medications, row ids, and that
another account's document never appears, not even through a link row that
points across accounts.
The hour grain of the dense series read truncated the local wall clock,
so the hour a fall-back night runs twice folded both instances into one
bucket: on that day a sensor stream showed one mean for two hours of
readings. An hour bucket now starts at the reading's instant less its
local minutes and seconds, which keeps the two hours apart and still
starts a half-hour zone's buckets on the half hour.
The new integration test pins a New York fall-back night and a
half-hour zone to fixed dates, so the case runs every day instead of
only when the rolling window of the existing tests covers one.
The fixtures built their rollups with the read refresh, which folds ninety
days. Under the stricter coverage a type folded only that far takes the
live path; the fixtures now fold the whole history first, as the boot
backfill does, so the walk still reads the rollup tier.
The analytics snapshots change only in what the old coverage hid: the
summaries count all 120 weight and blood-pressure readings instead of the
91 the folded window held, with the means and the all-time in-target share
that follow from them.
The coverage probe called a type covered as soon as one DAY bucket existed.
A write hook folds the day it writes, so a type whose history was not yet
folded (an account before its backfill, or a read racing the detached
refresh) had one fresh bucket, took the bucket-composed path, and its
summary counted that one day: three weight readings read as one.
A type is now covered when its oldest DAY bucket starts no later than its
oldest live reading inside the fold window. The bucket holding a reading
starts at or before it in any time zone, so the comparison holds without
knowing the account's day boundaries. Both sides are one index descent per
type; anything not covered falls back to the live aggregate as before.
The summaries read starts ensureUserRollupsFresh detached. When the third
row's DAY bucket landed before that refresh, WEIGHT counted as covered by
its one bucket and the read reported a count of 1; a CI shard hit exactly
that.
Migration 0491 cleared the shared "profile" placeholder to null. Those
connections sync normally, but the one-account-per-record check cannot see
them, so the same Google account could still be connected to a second
record until the first one reconnected.
After a clean sync cycle, a connection without an id now reads it the way
the callback does (profile, then identity) and stores it, guarded on the id
still being null so a reconnect landing meanwhile wins. An id another
record already holds is not written, and a failed read leaves the id for
the next cycle without touching the cycle's verdict. The outcome is on the
wide event as googleHealth.accountId.heal.
The profile's resource name is `users/{user}/profile`, and the connect
stored its last segment, the same constant for every account. Since the
one-account-per-record check, a second record on an instance could not
connect or reconnect Google Health: it was refused as `already_linked`.
The connect now stores the `{user}` segment, reads `users/me/identity`
when the profile answers through the `me` alias, and stores the id as
unknown when neither names the account. A migration makes the column
nullable and clears the placeholder, so the next connect stores the
real id.
The privacy check looks for the fixture's latitude 52.52 in every stored
row, and a timestamp written at :52.52x seconds holds the same digits. It
failed once in CI with no coordinate stored. Clock times are masked first;
a coordinate in any field is still found.
Every night's replace-by-window clean-up carried every fresh segment id of
the fetch. On a first sync that reads years of sleep that is one query per
night with tens of thousands of bound values, over Postgres's limit, and
the process spent minutes building them before each one failed. The sweep
can only reach rows inside the window, so each window now protects the
fresh rows inside it, from whichever session reported them.
Refs #1195
Hiding a marker takes it out of the labs catalogue and the entry
pickers, but the timeline still offered it among the lab values.
The timeline now offers only markers that are not hidden. A hidden
marker that was chosen before keeps drawing its line and stays in the
menu as a ticked entry, named from that line, so it can be unticked,
the way the entry form keeps a hidden marker that is already selected.
Once unticked it is no longer offered.
Restored rows keep the ids they had in the file. Once an erased
account's copy had gone into one account, the same file uploaded into a
second account restored into a primary-key conflict, reported only as
the generic "the database rejected a record" failure.
For a copy put into a chosen account, a primary-key conflict now ends
the restore with backup.restore.rows_exist_elsewhere and a sentence that
says the file was most likely restored into another account already.
The transaction still rolls back, so neither account changes. The code
is in the catalogue, the console names it, and the runbook says a file
goes into one account only.
The upload creates a passwordless account before it reads the file and
deletes it again when the file is refused. The only audit row was
written after a successful store, so a refused upload whose delete also
failed left an account nobody could sign into and no record of where it
came from.
The account and its admin.user.create row (outcome "created") now
commit together, and the discard writes admin.user.delete with outcome
"deleted" or "failed", so a leftover account always has a trace. The
runbook says how to remove one by hand.
A restore read the account settings under the operator's AI endpoint
policy whenever the receiving account was an admin. For a copy put into
a chosen account, the settings were written by the erased account's
owner, not by the operator, yet an admin account could be chosen as the
target, and an ordinary target promoted later got the same trust.
Admin accounts are now refused as a target, in the check and the
picker, with backup.target.admin_account. A copy uploaded into a chosen
account is stored as RETARGETED_UPLOAD_<ms>, and its restore never reads
its settings as the operator's, whatever the account's role is by then.
The empty-account check counted every backed-up model, and signing up
and answering the setup questions always leaves a setup row, plus a
cycle profile when the cycle area is chosen. The account of someone who
has signed up again, the case the restore target is for, was therefore
always refused.
Those two models are one row per account and are deleted and re-created
from the copy by every restore, so they no longer count. A reading or
medication entered on setup's last screen still refuses the account,
because the restore would delete it. A guard holds the exempt models to
that shape, and the erased-account integration file now drives the real
setup routes before the restore.
A values list naming more than 24 lab markers answered 422. The list is a
device preference, so once a 25th marker was ticked every later visit
failed: the page rendered its error card, and the values menu that could
untick it only renders on success.
The server now keeps the first 24 markers and leaves the rest out, as it
already does for a marker the record no longer has; a malformed key still
answers 422. The page applies the same cap before it asks, and the values
menu holds further unchosen markers back at 24 with a line saying why,
while a chosen one can always be removed.
The timeline showed a lab report as an event on the axis, but the lines
under the lanes could only be measurements. A lab marker can now be named
in `values` as `lab:<biomarkerId>`, beside the measurement types and
`MOOD`, so one choice, one request and one stored list carry every line.
`GET /api/timeline` answers with `labSeries`, one entry per named marker
with its readings in the window as single points (lab values come a few a
year, so they are never averaged into buckets), each dated by the record's
local day, judged against its own window and linked to the report it was
read from when the caller may see documents. `availableLabs` lists every
marker the record has a numeric reading for, so a client offers only
those.
A reading stored in another unit than its marker's is converted through
the unit-conversion registry where the pair is known, and otherwise left
out and counted in `excluded`, never drawn on a scale it is not in. A
marker id that is not in the record yields no line instead of a 422, so a
stored choice survives a deleted marker; ids are capped at 24 a request.
Refs #1299
After an account is erased the operator still has its disaster-recovery
file, and the upload refused it because its owner was gone. The refusal
now carries backup.owner.missing, and the file can be sent again with a
target in the query: an existing account that holds no records yet,
confirmed by typing its username, or a new account created for the copy.
The file's owner is rewritten to that account as it is read, so the
stored copy, its preview, its download and its restore are that
account's, and the restore that follows is the ordinary one.
The new account has no password and no email; an admin sets its first
password with the existing reset. A file whose own account is still here
is refused with a target, and an account made for a refused file is
removed again. The refusal of an unknown owner no longer writes the
owner's id into the audit trail, since the erasure took every row that
named it.
Under a loaded full run the wrapper around pg_dump and pg_restore crashed
with an unhandled stream error, and the dump under test reported only the
tail of that stack. The wrapper now leaves the exit code to the tool and
writes the reason to stderr.
The check whether an account's full rollup backfill is queued or
running answered false on any error, so a failed read let the dense
fold delete raw samples the backfill still had to read. A missing queue
table still answers false, since a web-only deployment has no backfill;
any other failure now counts as pending and is annotated. A backfill
that failed for good is still not pending, as the comment now says.
The admin recompute for one account called the full fold without a
statement budget, so on the large accounts it is meant for each yearly
statement ran under the database's ordinary limit and timed out, as the
boot job had before it got its own. It now passes the same per-statement
budget.
The MFA status read listed oidc among the proof methods for a Bearer
caller too, although the provider round trip stamps a browser session
and nothing else, and the API contract says a token is never offered
it. The status read now leaves it out on the Bearer path.