Opening a sheet pushed a history entry, and closing it took that entry
off again with a history back. The browser answers that back later, so
when the close started a navigation it landed after the router's push
and undid it: the medication wizard stayed on the list instead of
opening the new medication, a section picked from the settings list on
a phone stayed on the account page, and a link followed from a sheet
could put the page under it back.
The sheet history goes, with its wiring in ResponsiveSheet, the
document sheet's opt-out, the account form's deference to it, Back
answering the discard question, and their tests. Back behaves as
before: it leaves the page and the sheet goes with it, and the account
form's own Back guard answers every Back again. A tap outside, Escape,
the X and a swipe still ask before dropping typed input. The two size
baselines return to their earlier numbers.
The symptom form and the dashboard's medication intake asked "Discard?"
on every close, untouched. The sheet's fallback check read the input
Radix keeps for the intensity slider (value "0" before anyone moves
it) and the dose the form fills in from the preselected medication as
typed input.
The check now skips a slider's own input and counts a slider moved off
unanswered instead, and compares a field against the value its form
says it filled in (`data-seed`). A controlled input's defaultValue
follows its value, so it cannot tell the two apart. The dose field
names the medication's dose, one line more for the form's baseline.
Merging the previous release in kept this branch's older numbers for
telegram-webhook-handlers.ts (1335 lines, handleCallback 590), although
the file is unchanged at 1319 and 582, the numbers the previous release
recorded. The gap sat inside the ratchet's slack, so nothing flagged it.
The register refusal codes, the end of the no-device rest, the
no-recipient annotation and the sign-in shell comment had each grown a
function the size ratchet holds. The codes become constants, the rest
ends through a helper beside the one that starts it, and the
annotation moves into that helper. The settings Web Push card dropped
under the limit when its subscription post moved out, so its entry goes.
NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false built a dashboard that read its
tiles from two analytics slices, the widget layout, the mood analytics
and the thresholds instead of the one snapshot cell. It took a rebuilt
image to reach, and the snapshot has been the only path the published
image runs since it became the default. The flag, its readers, the
legacy reads in the dashboard hook and tile context, the slim and thick
merge helper, the build-time toggle notes in .env.production.example and
the self-hosting guide, and the tests that pinned the legacy wiring go.
A swipe past the close point paints the sheet where the finger let go and
asks the owner to close it. A sheet holding unsaved input asks about
discarding first and stays open, so it sat half pushed off the screen
behind the question. One frame after the close, a sheet whose content is
still open now glides back to its resting place. The function's size
baseline rises by the seven lines this takes.
The illness episode, cycle day, custom metric, target, mood tag, symptom
and life event sheets open filled from a record or a fetch, so the body
check cannot tell typed input from what was loaded, and a stray tap
outside or a swipe dropped the change without asking. Each now keeps
what it opened with (the cycle day once its fetch has hydrated the form)
and passes whether the fields differ to the sheet's dismissal guard. The
custom metric and life event forms report it to their hosts the way the
workout form does.
The size baselines of the six sheet functions and the cycle sheet file
are raised by the few lines the comparison takes.
The sheet now owns its dismissal guard and the Back handling, and three pages gained one or two lines for the save and refetch order; each branch had recorded its numbers, the merge kept only one side.
Now that the BMI chart draws inside a managed record, a child's normal
BMI sat in the red under-17 band, and the weight page painted the adult
WHO band (about 26 to 35 kg for 118 cm), so a healthy 21 kg read as
underweight. The release's own weight-band test pinned that band.
No band here knows a child's age yet. For a record under 18, or a
managed record with no date of birth, the targets read names no WHO
weight range, no BMI range and no category; the record's own weight
target still shows, and the values and the chart stay. The BMI chart
and the weight band draw only what the server names. The weight-band
test now uses an adult height and gains the child case.
The targets builder and the vitals builder grow by two lines each for
the flag; their size baselines are raised to match.
A person on the OpenAI-compatible gateway lost every AI surface once
their chain was saved at all: the fallback to the picked gateway asked
whether the chain was the default object itself, so a chain reset to
the default, reordered, or written by the gateway form's reasoning
setting (which appended a disabled entry to hold the value) no longer
counted, and the chain resolved empty. Rows written that way before
this release broke on deploy.
The fallback now reads the chain's content: the picked gateway serves
when nothing else resolves, unless the chain holds the gateway switched
off. A disabled gateway entry that carries a reasoning effort is the
form's holder, not a switch; the chain editor drops the effort of a
gateway it switches off, so a switch-off stays one. Saving the gateway
form with the gateway picked writes its entry switched on. Resolution,
the presence probe, the account payload and the iOS availability flag
all read the same rule.
The medication detail tabs put German words into the address for
every language (`?tab=verlauf`, `?tab=bestand`). They are overview,
effect, schedule, supply, history, injection and advanced now. Every
old slug maps to its tab, so a bookmark, a link in an old report or a
low-stock notification sent before the change opens where it did. The
low-stock push, the list card and the efficacy summary link with the
new slugs. The slug vocabulary moves into its own module with a test.
The history tab asks for a window of at most a year, but the route read
every intake the medication ever had: a medication taken three times a
day for five years read about 5,500 rows on each open. The full read
also served to find the last intake for rolling cadences.
The route now reads the rows anchored or taken in the window, and of
the rows before it only the latest take, which is the anchor a rolling
grid re-starts from and the last intake when none falls in the window.
Earlier takes place no slot in the window, so the ledger is the same.
The read moves into its own function, which shortens the handler.
The Coach's "Choose from documents" picker reads the newest fifty
documents and dropped the route's nextCursor, so with a longer vault an
older report seemed not to exist unless one already knew to search for
it. A cut list now ends with the same line the document picker shows:
how many are shown and that the search reaches the rest. The document
row moves into its own component, which takes AttachmentPicker under
the function-size cap.
Seven places kept information in a hover tooltip, which never opens on
a phone: the measurement-diversity lightbulb on the metric pages, the
sleep-source warning on the dashboard tile, the score coverage
breakdown, the target status pill, the range bar's marker, the colour
hint of the dashboard averages and the full token and user names in
the admin token list. The lightbulb showed a button that did nothing,
the warning a sign nobody could read.
They open on a tap now: InfoPopover for the two icon buttons, a
Popover on the meter, pill, marker, averages and cells. Where the
trigger sits in a link (a dashboard tile, a lab row) its tap opens the
popover instead of the page.
A screening, an illness episode, a life event, a symptom, a custom
metric or an address-book entry created on a weak signal could be
stored twice: the POST committed, the answer missed the timeout, and
the second tap went out under a new key. Their routes already honour
the key; the forms now post through useIdempotentPost like the other
create forms, so the retry replays the first answer.
The screening's save moves into a small hook beside MentalWellbeing,
which sat at the function-size cap and now drops below it; the custom
metric form's create-or-update call moves into one as well.
Back on a phone (the Android button, the edge swipe of the installed
app) with a sheet open went back a page under it: the sheet and what
was typed in it were gone, in the edit sheets too. Only the account
form guarded Back, behind an option.
Opening a ResponsiveSheet now pushes one history entry with the page's
URL. Back lands below it and closes the sheet, asking first when it
holds unsaved input or when its owner keeps it open; a sheet closed any
other way takes its entry off again, so the next Back still leaves the
page. A history write made while that Back is under way waits for it, a
link followed from a sheet replaces the sheet's entry, and the page
entry keeps the URL the page has now, so a stripped one-shot `?add=`
does not come back. The vault's document sheet already owns `?doc=` and
opts out.
The eight-step medication wizard, the add sheets on /measurements,
/mood and /labs, and the visit, vaccination, practitioner and checkup
forms closed on a stray backdrop tap or swipe and lost what was typed.
The dashboard quick entry and the capture menu asked first, through two
hand-copied dialogs and a DOM check that read the first sheet body in
the document and counted the date field's prefilled "now" as input.
ResponsiveSheet now takes a dirty flag, or a check it hands its own
body, and asks with the existing discard dialog. Forms that keep a seed
pass whether they differ from it; the wizard compares what a save would
send. Plain add forms pass the DOM check, which skips the date and time
fields again. The three copies of the dialog are gone.
About forty refusal messages still printed the error string the server
sent: English in every locale, and for a dropped connection the
browser's own TypeError text. Mood tags, targets and thresholds, family
history, medication side effects, phases, API tokens and dose imports,
the Coach model picker, the AI provider, fallback chain, timeout and
Codex cards, the notification channel cards, the avatar, passkeys,
security keys, the authenticator card, the re-proof dialog and the
admin pages now resolve the refusal's code through localizedApiError
and fall back to the surface's own sentence.
Two refusals carried something a person needs and had no code: a
private AI base URL (the AI_PRIVATE_ORIGINS hint) and removing the last
passkey. Both now send a code with a sentence of their own. A throttled
re-proof says so in the reader's language. Raw fetch sites throw the
same ApiError as the verb helpers through the exported throwApiError,
which replaces the admin's getApiErrorMessage; the authenticator card's
copy of describeStepUp is gone, and describeStepUp no longer passes a
non-step-up refusal's prose through (its test pinned that).
The admin password reset keeps the server's verdict on a 422: the
server words the strength and breach verdict in the account's language.
Back from a detail page (a biomarker, a medication, a workout) opened
the list at the top. The shell scrolls inside `<main>`, which stays
mounted across routes, and the browser's own restoration only moves
the document. Nothing kept the list's offset, so `<main>` stayed where
the detail page had left it; and the one-shot skip on `popstate` was
cleared by a timer that could run before the restored page rendered,
in which case the forward-navigation reset fired anyway.
The shell now records `<main>`'s offset per address while scrolling,
reads it back on `popstate` and applies it once the page has rendered,
retrying for up to a second while a list is still shorter than the
offset. A forward navigation still starts at the top.
The nightly plan review read a before/after window of the plan's metric
and stored an English verdict in `outcomeEncrypted` for every plan. No
screen and no route returned it; only the Coach snapshot read it, behind
the `COACH_EXPERIMENT_VERDICT` flag that is off by default. The worker now
only moves an active plan whose review fired to `reviewed`, which is what
takes it out of the standing group. The flag, its snapshot block, the
three eval cases for that block and the compose and env entries go with
it. The Coach prompt keeps its experiment rules and now ties the
read-back to the plan's progress line instead of the removed block.
Existing stored outcomes stay in backups untouched.
The plans page and the Coach reminders card printed the metric as a raw
key in capitals (WEIGHT, BLOOD_PRESSURE) in every language. Both now show
the measurement's name from the catalogue, through the plan metric
resolver moved to `plan-metric.ts`; a key HealthLog does not read shows
as written. The unused `coach.plans.proposalTitle` string goes.
iOS offers Web Push only to a web app opened from the Home Screen. In a
Safari tab the push card said the browser does not support push
notifications, and the step that fixes it was only in the docs. On an
iPhone or iPad outside the installed app the card now says to add
HealthLog to the Home Screen, open it from there and turn push on.
WebPushCard grows by two lines for the second sentence; its baseline
goes from 201 to 203, still below the 215 it had before the previous
change.
Signing out ended the session and wiped the caches but left the Web
Push subscription in place, in the browser and on the server. The
browser kept showing the signed-out account's medication reminders,
also after someone else signed in on it, and the next account's
Settings showed push as already on.
Sign-out now runs the same steps as "Disable push" before the logout
call: DELETE the server row while the session is still valid, then
unsubscribe in the browser. Both callers share one helper. A failure
there does not block signing out. With the dispatcher no longer
counting "no subscription" as a failed send, signing out does not
start the five-failure countdown on the account's channel.
Each taken or skipped dose sent a `clear` push to every subscription of
the account. The service worker closed the pending reminder and showed
nothing. A subscription made with userVisibleOnly may not receive
pushes that show no notification: Safari revokes it after a few, the
next reminder meets a 410 and the channel is switched off, with nothing
in the app saying why.
The clear push, its three call sites and the badge count only it fed
are removed. A reminder already on screen now stays until it is tapped,
dismissed or replaced by the next reminder for the same dose. The
sender's badge field went too: nothing else ever set it. The parity
snapshots lose the two recorded effects.
Every new account finished setup on a full AI panel: a gradient badge, a
sample briefing, three ways to connect a provider, a one-tap offer for the
operator's key and a disclaimer, the most prominent block on the screen.
It is now one muted line with a link to Settings, AI, shown under the same
rule as the Insights hint (could be set up here, nothing configured yet,
not opted out). That hint and the consent request on Insights and
Settings, AI stay the places that invite.
The sample briefing card, the `onboarding.ai.*` strings in ten locales and
the end-to-end spec for the removed offer go with it. The consent-grant
guard's floor drops to the two callers left.
The resolver tests queue their fixture rows through one helper instead of
an `as never` per row, the no-provider status case also proves the
single-provider resolver is never asked, and the size and comment
baselines take the lower numbers the change left behind.
The day view, the day index and the visit preparation each built the
UTC calendar window for a date-only column by hand (documents had a
fourth copy). `dateOnlyWindow()` beside the other date-only helpers
replaces them. The report name select becomes one spread, the share
page builds its calendar formatter outside the view, and the PDF
section reads it off the context, so the functions these fixes touched
stay within their size budget; `DocumentRecordLinks` drops under it.
personal-record-badge.tsx, intake-history-editable.tsx and
intake-history-preview.tsx had no importer outside their own tests and
the preview, so they rendered nowhere while four guard tests kept them
listed as live surfaces. They go with their tests, their guard entries
and the strings only they used.
GET /api/personal-records stays: the native app reads it. The
resolved-field guard now records its benchmark band as a native-only
field instead of naming the deleted badge as its reader.
A mood, symptom, lab value, custom metric value, unscheduled dose,
allergy, vaccination, visit or measurement saved on a weak signal could
be stored twice: the POST committed, the answer missed the 15 s
timeout, the form said it could not save, and the second tap was a new
call with a new key, so the server ran it again.
useIdempotentPost keeps one key until a save lands and renews it for the
next entry, so the retry replays the first answer. The create forms and
the three create hooks post through it; the comment on the per-call key
in api-fetch now says what that key does and does not cover.
LabForm and MoodForm each gain the one line that mounts the key, so
their function-size baselines go up by one. The intake toast guard now
recognises a dose posted through the hook.
With the Medications module switched off, dose reminders, missed-dose
rows and low-stock alerts kept coming, a tap on one landed on
"Medications is switched off", and the Telegram buttons still booked
intakes. The reminder tick now drops the medications of an owner with the
module off, one module read per owner and tick, so neither a reminder nor
a missed-dose placeholder is written while it is off. The low-stock pass
skips such a user, as the mood reminder does for its module, and the bot
offers and books nothing.
A medication whose course had ended still counted as live: the low-stock
pass kept warning about its stock, and the bot's /add list offered it and
booked a dose after the end, taking a unit off the stock. The low-stock
pass and every bot path now skip a course that has ended on the person's
clock. A course that has not started yet keeps its reorder warning and
stays in the bot. A paused medication is already active: false, so the
existing filter keeps it out.
The bot's six copies of the medication lookup go through one helper.
Confirming a document's facts creates medications, conditions and lab
values, and the profile take-over creates a condition or an allergy.
Both evicted only the roots of those lists, so the dashboard and the
Today digest showed the old medication set until the 120 s poll, the day
and timeline kept the old state for five minutes, and adherence and
nutrient reads were not evicted at all.
Both now spread the bundles of what they create, and the condition paths
pair them with the inactive refetch of the daily reads.
Symptoms, workouts, lab results, allergies, documents and questionnaires
evicted only their own lists. An open day, its calendar dots and the
timeline kept the pre-write answer for up to five minutes, so "Capture
for this day" saved a symptom or a workout and the day showed the old
list, and a deleted row stayed on it.
Workouts, labs, allergies and documents get a bundle beside the seven
that already carry the day and timeline roots; the six lab lists become
one (the OCR dialog's own copy of the commit hook's list goes with it).
Symptoms and questionnaires add the two roots to their list. Symptoms
stay off the illness bundle on purpose: nothing on the dashboard or the
Today digest reads them. Custom metrics need nothing: neither the day
nor the timeline reads them. invalidateKeys now drops a key listed twice,
since spread bundles share the two roots.
GET /api/cycle/feed/<hlf_token>[.ics] answers any calendar app with the
record's forecast as RFC 5545: all-day events for the next few periods
over their widening windows, optionally past periods, and for a
conception goal that asks for it the next fertile window. Dates and a
title only; discreet titles and the calendar name "HealthLog" by default.
The body is built live on every fetch in the record's own zone and
language, never the request's. ETag is the content hash and a matching
If-None-Match answers 304; DTSTAMP moves only when the content does.
The feed is anonymous, so every gate is read per fetch: the auth-surface
limit before any lookup, the token's shape, the HMAC lookup of a live
feed, a per-token bucket, then the record and carrier not pending
deletion, the operator's external-access switch, cycle tracking for the
record, and for a feed a delegate minted, a live MANAGE grant covering
cycle. Every miss is the same 404.
/api/cycle/feed-subscription (status, turn on, options, turn off) and
its rotate are cookie-only through requireRecordAuth("manage", "cycle"),
with a fresh proof on turning on and rotating and a per-actor mint
bucket. The URL is returned once; the status carries the prefix, the
options, the fetch statistics as a calendar-app family and the
projection state.
The hlf_ family is registered with the path and token redactors, the
secret-shape refusal, the proxy's public paths and edge headers, the
URL-credential, sharing, recent-proof, step-up and module-gate guards,
the error-code catalogue, the activity feed and the destructive-control
registry. The cycle settings tab gains the subscription card and the
predictions panel lists the later cycles.
A merge into a target without a panel now takes over the first panel
a source carries, in the order the sources were given, so the merged
marker keeps its category. The result reports it as adoptedPanel, on
a dry run as well, and the merge preview says so before the person
confirms. A target with its own panel keeps it. The ledger does not
record panels; a merge has no undo.
The integration suite also proves the clear path end to end: an
explicit null on PUT clears panel, context and both bounds.
Refs #1349
The panel field offers the catalog panels in the page language and the
person's own panels, which GET /api/biomarkers now lists as
customPanels (one spelling per folded form). The field stays free
text; a catalog panel typed in any language lands in the same category.
An edit used to omit an emptied panel, context or bound, so the stored
value survived the save. The form now sends null for a field that held
a value and was emptied. A new marker still omits empty fields, and a
field that was empty when the edit began stays out of the body, so a
context the list could not decrypt is never wiped by an unrelated save.
Refs #1349
GET /api/labs/overview now answers categories and grouped next to the
markers. A marker's category is its panel: a catalog panel in any
shipped language folds to one key, any other panel becomes the
person's own category under an opaque hash key, and a marker without
a panel falls back to the catalog panel of its name. ALIAS_PANEL
places the ten analytes that have names but no seed. Each category
carries its label in the caller's language, its members, counts per
range status and one combined status (above wins over below; no
alarm tone). grouped is true only when grouping adds something.
The list renders this as given: with groupBy set to category it shows
a disclosure header per category in both views, the person's sort
applies inside each category, and groupsOpen decides which start open.
A category opened or closed by hand stays so in this browser until the
choice changes. Settings > Arrangement > Labs gets both choices. The
overview query key carries the locale, since the labels are localised.
Refs #1349
A report that prints a text result such as "SEE NOTE" with a numeric
range beside it minted the marker without bounds: the document commit,
the scan commit, the free-text lab write and the CSV import each passed
null bounds for a qualitative reading, and the CSV row reader and the
scan review payload dropped the range before that. The marker's first
number then had nothing to be judged against.
Every mint path now passes the printed window. A range that is text
itself yields no bounds, an existing marker keeps its own, a transposed
window is not minted, and the reading itself still records no window.
The scan review shows the range on a text row that mints a new marker.
Refs #1313
A blob: document inherits the policy of the page that minted it, and
WebKit applies the inherited frame-ancestors to the blob frame's own
parent. With 'none', Safari on macOS and iOS refused the PDF preview
iframe and showed an empty box; Chromium does not apply it there and
rendered the preview. 'self' keeps every foreign origin out.
Refs #1311
The draft state for the typed fields pushed the form past its size
baseline. The primitives own only their input state and read nothing from
the form, so they move out and the baselines tighten.
Weigh-ins: Garmin's document has no samplePk and gives kilograms, the live
service sends a samplePk and grams. A weigh-in now takes its id from
samplePk or else its own GMT instant, reads each mass by magnitude (above
1000 is grams), and is written whole or not at all: a missing weight, a
missing instant or one implausible value drops every row of it, counted
on the wide event by reason.
Refused resources: a lasting 4xx on one resource (a cycle summary for an
account that tracks no cycle) is noted once on the progress column,
counts as done for the backfill, is skipped by the hourly sync and is
listed in the card's details. It no longer holds the backfill open or
turns the connection red. The cycle leg never parks the connection,
whatever it is refused with.
Backfill: weigh-ins walk back to the start of Garmin Connect on their
own, so history before the first watch day and a scale-only account
arrive. Progress and the completion stamp are written only while the
connection the slice started on is still the stored one.
Key: connect sends the key to the one service the person picks (Garmin
Connect or Garmin Connect China), never to both. A parked key is asked
about again once a day with a single token exchange and reconnected when
Garmin accepts it. The key's end is read from the token's own iat and
exp, an estimate the key outlived is cleared after an accepted exchange,
and the validity can be corrected from the card's menu (PATCH
/api/garmin/key-expiry, with the day the key was created) without
pasting it again.
Sync: an incremental run reaches back to the last successful sync, up to
a year, so an outage longer than the 30-day window leaves no gap; a night
Garmin no longer reports (deleted, or turned into a nap) is retired.
Card: connect errors are translated from meta.errorCode in every locale.
Types: BODY_BATTERY, STRESS_LEVEL, TRAINING_READINESS and
ACUTE_TRAINING_LOAD get source ladders of their own (the device or the
bridge first, then the hand-entered sources in their usual order), join
the ranked types and the provenance picker, and the recovery-empty
description names them.
Garmin Connect now offers a Personal API: a person creates a key for
their own account and any client can buy short-lived tokens with it.
HealthLog takes that key in a new Garmin card, checks it with Garmin
before storing it sealed on the user row, and polls every hour.
- Transport: key exchange with a per-process token cache keyed on the
key, one re-exchange on a refused token and then reauth_required, the
429 Retry-After carried to the caller, the China host tried when the
global one does not know the key, answers read with a byte cap, all
egress through safeFetch to the two fixed hosts.
- Sync: daily summaries, sleep summaries, HRV, weigh-ins and the
activity search, chunked to each resource's documented range, read one
after another and stopped at the first rate limit; today's menstrual
cycle summary adds logged period days the person has not logged.
- Backfill: a resumable walk through the shared admission lane that
learns where the history starts from the daily summaries and the
activities, waits out Garmin's rate limit and stamps its marker.
- Types: GARMIN source; BODY_BATTERY and STRESS_LEVEL for Garmin's own
day scores, kept apart from the computed STRESS_SCORE; TRAINING_READINESS
and ACUTE_TRAINING_LOAD for figures a bridge pushes through the ingest
token. All four chart on the recovery page and sit on the ladders,
units, ranges, labels and report groups like the device-native scores.
- Key expiry: learned from a token capped short of a day, else estimated
from the validity picked at connect; the card warns a week ahead and
asks for a new key once Garmin refuses the old one.
- Ledger, health, freshness, watchdog, backup classes, wipe plan,
encrypted-column registry, rotation script, OpenAPI and all ten
locales carry the new integration. The old "no Garmin connector" note
is gone.