Four fixed rounds could all finish one rotation before the other started on
a fast runner, so the guard that the race really ran failed with eight 201s.
Rounds now continue until a 409 shows up, with the mint bucket emptied
between them, and the guard still fails if no pair ever collides.
Next 16.4.5 drops content-length, content-encoding and transfer-encoding
from every route handler response, so the header the export route sets
never reaches the browser. A truncated body still loses its %%EOF
trailer, and that is what the spec now looks for.
16.4.5 is a security release with seven moderate advisories. Most are about 'use cache' and Cache Components, which the app does not use; one covers self-hosted apps that forward fetched responses.
The plural tier comes from CLDR, and in Russian the one category also
holds 21, 31, 101, while French and Brazilian Portuguese file 0 there.
Thirty One strings in those bundles spelled out the 1, so 21 days read
as one day. They now take {count}, as do two Russian One strings that
spelled the number as a word, and every direct call of such a key passes
the count. A guard finds the locales whose one is wider than 1 from
Intl.PluralRules and holds both ends.
The cycle read returns the projected period starts after the next one,
each with its band, under upcoming; the tool description for MCP clients
and the Coach did not say so.
The comments still named X-Frame-Options DENY as what refuses the vault
preview embed and claimed the serve route keeps a sandbox directive. A
browser that reads the CSP's frame-ancestors 'self' ignores
X-Frame-Options, and the document CSP deliberately carries no sandbox.
No behaviour change.
A link subscribed on a phone, a tablet and a computer at a five-minute
refresh came close to the 30 fetches an hour a link was allowed, and a
household behind one address shared 120. The per-link bucket is now 120
an hour and the per-address one 600. The token is a random secret checked
by its HMAC, so guessing links is not what these limits stop.
The 730-day clamp subtracted 730 times 24 hours from the end, which lands
an hour off local midnight when an odd number of clock changes lies in
between. The start is now the end's wall clock 730 calendar days back in
the account's zone.
Two rotations at once both revoked the same live feed, and the second
successor then hit the one-live-feed index with an unhandled unique
violation. The losing rotation now gets the mint path's 409
cycle.feed.exists, and the winner's link stands.
updatedAt is stamped when a write is built, not when it commits, so a
transaction in flight when the cursor was handed out could commit a row
stamped before it, and a watermark at serverNow skipped that row for
good. The watermarks now sit one minute back; client upserts are
idempotent, so the overlap is harmless. The token's issue time stays
serverNow.
The dry run showed the printed range beside a text result, which the
write stores on the new marker and not on the reading. The preview and
the write now read the reading's window from one helper, and the
placeholder marker of a dry run takes its bounds through the same rule
as a real mint.
A marker minted from a text result that stated no unit kept the printed
numbers as its range, and the first numeric reading then gave the marker
its unit without touching them, so a "30 - 100" printed in ng/mL became a
window in nmol/L. Numbers stated without a unit are dimensionless: they
stay when the adopted unit is the unity "1" and otherwise give way to the
window the first number printed in its own unit, or to none.
A cycle the person deleted, or whose first day she deleted or cleared,
left its wearable days without a cycle when nothing lay before them, and
the boot-time repair took them for the pre-fix backlog and reopened it on
every deploy. A day within the hard minimum cycle length after a removed
start is no longer a candidate. The repair also skips accounts with the
cycle module off, as the syncs do, and their rows no longer make a boot
enqueue the job.
Garmin's period end was written on any cycle starting on Garmin's start
date, a cycle the person opened herself included, even when her own days
ran past it. The stamp now requires the cycle's first day to be the
wearable's own live row.
A custom symptom could feed pattern discovery and the Coach but could not
be looked at over time. The timeline now offers every symptom the record
has logged, hidden ones included, beside the measurements and lab markers,
and draws a chosen one as a value line.
`values` takes `symptom:<definitionId>`, checked for its shape and capped
like a lab marker; an id the record does not hold draws nothing. A day's
value is its highest intensity on the record's local day; a point is the
mean of the bucket's day values over the days logged, with the number of
those days, and a day without an entry is not a zero. The line sits on a
fixed 0 to 10 scale. Symptoms are read under the illness lane's gate, the
module and sharing domain the symptom routes use. An `all` window reaches
back to a chosen symptom's first entry.
The response carries `symptomSeries` and `availableSymptoms`; the OpenAPI
contract is regenerated. The page words a point's count as days and the
value out of ten. The date-key schema moves to its own module so the
symptom schemas can share it.
A calendar feed is a standing credential whose contents end up in a
third party's calendar cloud. Putting a record's cycle dates there is
the decision of the person they are about, or of the guardian of
somebody who cannot make it, and no delegate of an adult's record,
MANAGE included.
The subscription routes resolve through requireGuardianAuth instead of
requireRecordAuth("manage", "cycle"): the owner on their own record, or
a MANAGE grant on a record carrying the managed-profile marker. Every
verb follows, the revoke too. The feed re-reads the marker on each fetch
as well as the guardian's grant, so a row minted for somebody else on an
adult's record is a 404 whatever grant stands behind it. The settings
card is offered only where the routes answer.
The routes move from the delegable and MANAGE literals to the guardian
list, and the MANAGE handler inventory is back to 93.
A how-to for subscribing from Google, Apple and Outlook and what the
feed shows; a section on the reproductive-health page saying that once
it is on, the dates also sit with the calendar provider and turning it
off does not reach those copies; a reverse-proxy note with Caddy and
Nginx examples for keeping path credentials out of access logs; and
API_TOKEN_HMAC_KEY rotation named as ending every feed link.
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.
The calendar read, the Coach and MCP cycle block and the coming calendar
feed now share one loader for the forecast's inputs (cycles, day logs,
overnight wrist temperatures) and one gate (`gatedPrediction`: no
forecast while prediction is off or raw-chart mode is on). The Coach
block reads the calendar's default day-log window, so its next period
is the date the calendar page shows, and resolves the luteal length
through the engine's own clamp.
`projectCycles` extends the engine's next-period forecast a few cycles
ahead. Cycle 1 is the engine's result unchanged; later starts are
anchored on it from the fractional median length, so six cycles out
carry no rounding drift; later bands widen as the SD of a sum of k
lengths with an estimated mean, with the engine's sparsity penalty, and
never narrow. The list stops where a band passes ten days or half a
cycle, holds the next period only while the engine is still learning or
for perimenopause, and is empty once the next period's band has passed
with nothing logged.
`/api/cycle/calendar` publishes it additively as `projection` and
`projectionState`; the Coach block carries it as `upcoming`.
A record's cycle calendar subscription is a URL credential: one row per
feed, only the HMAC of the raw hlf_ token stored, and at most one live
feed per record through a partial unique index on user_id where
revoked_at is null. Both foreign keys cascade, so deleting the record
or the carrier who minted the feed ends it.
The row is a credential everywhere else too. It stays out of the
backup (a restore would bring back a revoked link), Delete All Data
takes it on both sides, a credential remediation revokes the feeds on
the record and the ones the account minted elsewhere, and a
managed-profile handover claim revokes any feed a guardian minted.
backupVerdict moves beside the plan into its own module so the plan
stays the lists and their reasons.
A per-envelope guard walks the category leaves of LabOverviewResponse,
customPanels on ListBiomarkersResponse and adoptedPanel on
BiomarkerMergeResult out of the Zod schemas and ties each to the web
reader that renders it, so a new leaf without a reader fails. The
resolved-field guard gains a rule that bans the category ingredients
(catalogKeyForName, resolveMarkerCategory, summariseCategory,
categoriseOverview, ALIAS_PANEL, buildLocalisedLabelIndex) from client
code: the category, its order and its status are read, never rebuilt.
Refs #1349
A draw label such as "Annual blood panel" is a panel the labs list
would show as one category holding every marker, so the demo list
would stay flat with categories turned on. Without a panel each marker
lands in its catalog category by name, both before and after the boot
backfill links the rows.
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