A walk that stopped for lack of progress ended its data type for good, so
a passing fault in Google's paging closed that history until a reconnect.
A walk that had written nothing yet was also noted as ending today.
The first end without progress now parks the data type with its edge: it
is not finished, not published, and is walked again a day later. Once
nothing else is left, the next slice is queued for when it is due, and
the marker is never stamped over a parked type. A second end without
progress is final; a second attempt that moves on is an ordinary walk
again. A walk that stalls before writing anything records no end and
fails the slice as stuck.
Refs #1220
The failure rows added for supplementary reads offered a retry for any
error. The usual-range strip reads at manage level, so a viewer on a
read-only share saw a permanent failure with a retry that could never
succeed; the visit preparation reads measurements under visits that
need only the profile section, which a share can grant alone. A refused
read (401/403) now draws nothing, a spent read budget (429) says so
without a retry, and only a real failure keeps the retry row. The
cycle strip holds no cell for a refused calendar read.
The SQL page labels a sleep session by the last row of any writer; the
reconstruction labels it by its winning writer. A watch stopping before
midnight beside a phone whose in-bed span ran past it gave two days,
and matching the two by label dropped the night from the list while
the total still counted it. Each reconstructed session is now filed
under the page night whose span holds it and takes that night's label,
which the drill-down also asks by.
A sleep drill-down scans 36 hours either side of its day instead of the
whole history. Both day branches and the sleep list resolve the zone
through the same stored-zone resolver, so a drill-down always cuts the
days its list row summed. The list description states the real page
bounds: a day-sum page takes `limit` as asked, a sleep page caps at
1000 nights.
Refs #1245
The recovery page now holds its empty note until the integrations
status has loaded. The data-gating test rendered without that status
and saw the loading placeholder; it now seeds an account with no
integrations, the settled answer the general note belongs to.
Refs #1204
The derived envelope types `value` as an open record, so the sleep
score's `rhythmNights` and per-sub-score `gap` were on the wire but in
no schema. `SleepScoreValue` joins `WellnessScoreValue` in the forced
components slot, the envelope's `value` description names it, and a
contract test holds the published field set to the one the engine
emits, for a scored rhythm and for one withheld for too few nights.
Refs #1204
runtime.heap.sampled is written every minute while the heap is above
70 % of its limit, which turned the 500-entry ring behind the admin log
page over in hours and pushed out the events an operator opens it for.
emitEvent takes a skipMemoryRing option; the sample sets it and still
reaches stdout and Loki, while the pressure and snapshot lines go
everywhere as before.
The snapshot directory was /tmp/healthlog-heap-<pid>, a predictable name
(pid 1 in most containers) that mkdirSync accepted when it already
existed or was a symlink. It now comes from mkdtempSync and is checked
with lstat to be a real directory owned by the process user and closed
to others before the snapshot is written.
The route is stateless and returns one buffered JSON response per call,
so it opens no server stream and holds no session. GET let the transport
open an event stream that the handler closed again at once; a client
read that as a dropped connection and reconnected about once a second,
each attempt costing a rate-limit slot and a module lookup. GET and
DELETE now answer 405 with Allow: POST after authentication, which is
what the transport specification asks of a server without that stream,
and an unauthenticated GET still gets the 401 challenge.
The empty recovery note told every connected Oura account without a
recorded permission gap that its permission covers resilience. The
server cannot know that: the scope note is cleared on each reconnect
and written only by a sync that reached Oura, and a missing health
block says nothing either way. The note now asks for a reconnect only
when the gap is recorded and otherwise says only that no level has
arrived, pointing at the Oura card. It also waits for the connection
status instead of showing the general note first and swapping it.
Refs #1204
Keeping a nap-only day off the sleep rhythm used three hours asleep as
the line, which also took every shorter night off the clock: someone
who sleeps two and a half hours a night (insomnia, a newborn, split
sleep) lost consistency, timing, chronotype and the health score's
regularity altogether. A session now leaves the clock only when it is
short AND centred in the daytime, outside 20:00 to 10:00 local time.
A short night keeps its place, a long daytime sleep after a night
shift stays a main sleep, and the withheld-timing note now reads that
the latest day had only a nap.
Refs #1204
The multi-year Google Health backfill run walked three and four years of
heart rate on every pull request, some 3.7 million readings in over twenty
minutes, which filled most of its shard's time budget. What a run costs is
its pages, so a shorter history only helps with shorter slices.
HEALTHLOG_BACKFILL_TEST_DAYS now sets the size. Unset, the runs walk 120
and 160 days in 50-page slices with the faults at the same places within
them and the light collections already walked, about a tenth of the
pages. The heap growth bound applies to the full run only. The full three
and four years run nightly and on demand in integration-long.yml.
Refs #1220
The two status routes publish backfillEnded: each data type whose
backfill stopped short of the start of its history, with the instant it
reaches back to and whether the horizon or a walk that stopped moving
ended it. The integration card lists them in place of an import that
would otherwise read as still loading, in all seven locales.
Refs #1220
A resumable collection counted the pages it had written across every
backfill attempt and stopped at its ceiling (10,000 for heart rate). A
watch that records heart rate every few seconds reaches that count two to
three years back, and since it never reset, every later slice stopped at
the same place, failed as stuck, and the backfill was never stamped
complete.
A walk is now stopped only when it stops moving: 20 pages in a row that
reach no further back than the pages before them end it. A history
backfill reads back at most GOOGLE_HEALTH_BACKFILL_HORIZON_YEARS (20 when
unset). A data type that ends either way counts as finished, and where and
why it ended is kept in backfillProgress past the completion stamp. An
oldest-first list is read whole, as before. The slice chain's ceiling is
derived from sixty days of back-to-back slices instead of the old page
count. An account left at the old ceiling continues from its edge with
the next slice.
Refs #1220
"Since the last visit" was wrapped in a card by the visits section, so
the card stayed on screen empty while the window loaded, when it held
nothing and when the read failed. The component now owns its card and
draws it only with a row; a failed read shows a compact error row.
The same shapes elsewhere: the caffeine card and the mood forecast no
longer open a loading card that most often collapses, and a failed read
on them, the mood tag catalogue, the usual-range strip, the cycle ring
and the measurement drill-down now says so with a retry instead of
passing for no data. The scores strip keeps the ring's cell on a failed
calendar read. The mental-health card no longer draws an empty action
slot for a viewer without the right to start.
The frame guard now also flags a frame whose only content sits behind a
condition.
Refs #1241
The client fetch-limit guard took any `offset` within three lines as a
pager. The day-grouped measurement list passed it with `limit` 5000
between a comment about pagination and `offset` pinned to "0", while it
drew a whole history on one page. Comment lines, trailing comments and
inline block comments no longer count, nor does an offset pinned to a
literal zero in any of the three spellings. The old list shape is a
test case and fails the guard.
Refs #1245
The day-grouped list read the newest `limit` raw rows (the web asked
for 5000), folded them into days and returned every day at once: one
page of thousands of rows, and every day older than the 5000th raw row
missing without a word. The sleep list did the same at a year.
Both now pick the page in Postgres. The day mode selects the page's
local days with the distinct-day total from `COUNT(*) OVER ()` in the
same query, then folds only those days into per-source groups in SQL
and lets the source ladder choose among the groups, so no raw row
reaches the process. The sleep mode cuts sessions with the same gap
rule as the reconstruction, pages wake days the same way and reads only
the page's stage rows; the 365-day cut and `meta.windowFrom` are gone.
A sleep page holds at most 1000 nights, echoed in `meta.limit`.
The web list pages both modes at 25 like the per-sample list. The
`partial` day marker is dropped from the contract; no day is cut any
more.
Refs #1245
runtime.heap.sampled records heap, old space, RSS, external memory and
the work in flight every five minutes, every minute at or above 70 % of
the heap limit. At 85 % runtime.heap.pressure lists the oldest requests
and jobs in flight and the routes most of them share, once, re-armed
only after the heap falls below 70 % and thirty minutes pass. A
heap-limit crash leaves no other record of what the process was doing.
HEALTHLOG_HEAP_SNAPSHOT=1 adds one heap snapshot at that moment into
HEALTHLOG_DIAG_DIR (0700 directory, 0600 file), off by default.
docs/ops/heap-diagnostics.md warns that a snapshot holds decrypted
health data and keys and says how to delete it.
Every HTTP request the server accepts is registered through Node's
http.server.request.start diagnostics channel and removed when its
response closes, finished or aborted, so pages, RSC payloads and server
actions are covered alongside API routes. Jobs register around their
handler in observeJob. The register holds strings only, cuts the query
string on entry, caps at 2000 entries and counts what it refuses; route
labels are scrubbed and collapsed only when a report is written.
A rhythm sub-score below three nights with a main sleep was dropped from
the blend and its row read "no data", although the nights were there;
since a nap-only day no longer has a place on the clock, a ring worn
for only a few nights reaches that state more often. Each withheld
consistency or timing sub-score now carries the reason
(`few_nights`, `no_main_sleep`) and the score value carries how many
nights the rhythm is built from, so the row reads "not enough nights
yet (2 of 3)" or "latest night had no main sleep" instead. Neither is
ever shown as 0.
Refs #1204
A red Next bump inside the production group held the twelve harmless
updates next to it red as well. The three Next packages open as single
pull requests, so each is judged on its own checks.
Raw NUL bytes in source files make grep and ripgrep treat the file as
binary and skip it, so sweeps silently miss it. The affected string and
regex literals use the escape now; behaviour is unchanged.
The workout list, the workout header and the dashboard tile formatted
instants through the shared formatter cache without a timeZone, so they
followed the browser's zone. The year comparison in the list used the
browser's calendar as well. All three now take the profile zone, and the
timezone guard also matches getDateTimeFormat(locale, options) calls that
leave the zone out.
An Oura ring fills the recovery page through its resilience level alone.
The empty note listed every source that could fill the page, so a
connected ring with no level read the same as no wearable at all. The
note now follows the server's view of the Oura connection: when the
grant leaves out the heart-health data the level sits behind, it asks
for one reconnect and links to the integrations settings; when the grant
covers it, it says Oura has not sent a level for this account yet.
Refs #1204
A day whose ring or watch recorded no night has a nap as its largest
session, and its afternoon midpoint sat hours from every other night.
One such day in a month pushed the midpoint spread past the 90-minute
full scale and read a regular rhythm as Consistency 0. A day's midpoint
now needs a main session of at least three hours asleep, the line
wearables draw between a nap and a main sleep. The day keeps its
minutes; it only has no place on the clock for consistency, timing,
chronotype and the health score's regularity.
Refs #1204
A failed run threw the bare Prisma error, which the support report showed
as "Error". The reason now carries the stage and the database condition
(statement_timeout, lock_timeout, pool_timeout, ...), and the report's
error class reads the SQLSTATE out of a Prisma message.
Refs #1239
The weekly copy read every stored document and ECG trace whole, then held
their base64, the section JSON and the preview copies beside them. A vault
of 190 MB peaked at 930 MB of heap and took a 524 MB process down on every
run. Documents now leave the database one at a time in 1.5 MB slices, the
traces a page at a time, every other section is written an element at a
time, and a sealed piece never exceeds its chunk size. The restore keeps
document bytes out of its first read and writes each document from a
second one. Same file format, byte for byte.
Refs #1239
The episode detail listed every symptom occurrence that fell inside the
episode's onset-to-recovery span as well as the ones filed against it, so a
long-running condition showed the symptoms of every illness overlapping it,
and picking an illness in the entry sheet changed nothing on the page. The
occurrence edit sheet carried no illness link at all, so once logged the
assignment could be neither seen nor changed.
- The episode card reads only the occurrences filed against the episode
(`SymptomEvent.episodeId`), with a guard against any other.
- The edit sheet loads the link and edits it like the time and intensity;
an unchanged link is not re-sent, so a link the list cannot show stays.
- One rule decides which episodes may take an occurrence
(`isEpisodeLinkableAt`): an open one, or a resolved one whose span holds
the occurrence's time. The server checks it against the time after the
edit, and the entry and edit sheets offer exactly what it admits, so a
backdated occurrence can be filed with an illness that has since ended.
- The illness page names the illness on each occurrence row.
- The day view opens a symptom's illness only while that episode exists.
The stored links were always written correctly, so no data repair is needed.
Refs #1237