Commit Graph
8446 Commits
Author SHA1 Message Date
Marc-André Bombeck cd0bbec66a Merge branch 'fix/v1437-r2-data' into release/v1.43.7 2026-10-20 10:11:09 +02:00
Marc-André Bombeck dc5120b59a feat(lists): read and show the list cap on every capped list
Twelve routes answer a capped list with meta.truncated / meta.limit on
the envelope, and the web client read only data, so no surface said a
list had stopped short. apiGetList / apiPostList are now the one client
reader of the cap and ListCapNotice the one line that shows it; every
capped list on the web reads its cap and shows the notice under the list,
the paged readers (side effects, illness episodes) stop on the route's
answer instead of a full-page guess, and the ECG list moves its five
readers onto one hook. The record activity feed sent its cap twice, in
data and on the envelope; it rides the envelope alone now. A guard
derives the capped routes from the tree and binds each to a client that
reads its cap.
2026-10-20 10:04:11 +02:00
Marc-André Bombeck be79603d53 chore(guards): give the linear-fit guard a match floor and keep SQL out of the i18n scan
The linear-fit guard floored only its walk; it now also requires the
implementation's call sites it expects. The per-type readings query
aliased its driving relation t, which the i18n call-site scanner read as a
translation call; it is mt now, as in readLatestEver.
2026-10-20 10:01:30 +02:00
Marc-André Bombeck 071d23537b fix(insights): read the mood fallback over the rollup's window
While the mood rollup tier is still empty the feature block falls back to
raw entries, and that fallback read one year where the rollup read five,
so the same block described a different history until the tier caught up.
2026-10-20 09:48:30 +02:00
Marc-André Bombeck 5fd4333017 fix(insights): read snapshots by time and per type, not by a shared row count
The pulse and per-metric status snapshots read the latest 365 rows, which
for a dense pulse stream is a few hours, so their 30-day figures were
computed over part of one day; they now read at least the last 30 days.
The general overview read the newest 5000 rows across every type, so a
dense stream crowded the other types out within days; it now reads the
latest 365 of each type with one index descent per type. The sleep summary
in the analytics slice read one year of stage rows, so its all-time count
and its year-ago average fell short; it now walks the whole history in
chunks.
2026-10-20 09:47:20 +02:00
Marc-André Bombeck bfd8639178 test(cache): prove analytics coalescing per cell, not by the bucket total
The analytics bucket holds two cells per account since the summaries slice
got its own: the default body and the slice it composes. A cold mount
builds each once, so the bucket-wide miss counter reads 2, and the test
read that as a second builder run. It now counts builder runs per key, one
each under ten concurrent reads, and two each after a write invalidates
both.
2026-10-20 09:47:19 +02:00
Marc-André Bombeck b402c65f6a fix(demo): seal the baked insights cache and carry it across a migration
The demo seed wrote the cached insights payload into the readable column,
and the edge migration copied only that column, so a migrated demo lost
any sealed payload. The seed now seals it the way the app does and the
migration copies insights_cached_text_encrypted beside it.
2026-10-20 09:46:48 +02:00
Marc-André Bombeck f0c20151a9 perf(reads): ask the latest reading per type with one probe per type
The briefing's last-seen read and the MCP search presence probe grouped
every live row of the account to learn the newest instant or whether a
type exists at all. Both now use readLatestEver, one index lookup per
type. The latest-per-type guard also catches the Prisma spelling: a
groupBy by type with nothing but _max, or no aggregate, and no window.
2026-10-20 09:45:51 +02:00
Marc-André Bombeck 05503e2b82 refactor(stats): one least-squares fit for every trend line
Six readers fitted a line by hand, four in the determinant form that
loses digits on a large x axis, and the two charts carried the same trend
function twice. linearFit / fitFromSums in lib/stats is now the one
implementation (centred sums, so a fit from points and one composed from
stored accumulators are the same arithmetic), trendLineByTime serves both
charts with the same two-decimal output, and a guard keeps hand-rolled
slopes out of the tree.
2026-10-20 09:43:47 +02:00
Marc-André Bombeck 0fe9d5d224 fix(import): round Health Connect values through importValue
The Health Connect import stored its unit factors' binary noise as is
(grams to kilograms, millis to minutes, the day sums) and rounded the
pulse means and nutrients privately. Every measurement value now takes
importValue at the write boundary, and importValue takes a precision for
the two places that need another one. The rounding guard now covers the
import directory and the scaled Math.round form.
2026-10-20 09:35:11 +02:00
Marc-André Bombeck 29c5b5b83b fix(crypto): one read order for a sealed column beside its readable one
Notes, workout tracks and the cached insights payload each read their
ciphertext and legacy column their own way: the cache read the readable
column first, and the two that read the ciphertext first disagreed on a
value that does not open. readSealedOrLegacy fixes the order (ciphertext
whenever present, the readable column only without one, never as a stand-in
for a failed open) and makes each caller state what a failed open means.
2026-10-20 09:29:03 +02:00
Marc-André Bombeck 63bbf0931c fix(sleep): one day total for every sleep reader, and the latest night is a night
A day's sleep is the main night plus its naps (daySleepMinutes). The
series, the sleep score and the composition already read it that way; the
sleep list, the CSV export and the correlation sleep channel read the main
night alone, so a day of naps listed as 0 h and an average over the list
counted it as a night without sleep. The list now carries the day total in
value and the main night alone in the new mainAsleepMinutes field; the nap
caption reads as included.

GET /api/sleep/night without a date answered the latest day holding any
sleep, so a nap after the last night left the hypnogram with no night to
draw. It now walks back to the latest night with a main sleep; nap days
stay reachable by date.

The SQL night page bounded a night by its asleep sessions only, so an
in-bed-only session of the same night fell outside a chunked read that the
one-night read counted. The span now covers every session of the night.
2026-10-20 09:28:40 +02:00
Marc-André Bombeck 3004f55875 fix(correlations): keep a chronic condition's logged days in the symptom channel
Excluding chronic episodes from the zero-fill also dropped them from the
read, so their day-logs never reached the series and an account with only
a chronic condition had no symptom channel at all. The read now takes
every in-window episode; only the ones that can trigger Rest Mode carry a
zero-filled span, and every logged day counts.
2026-10-20 09:16:43 +02:00
Marc-André Bombeck 67e2563c63 fix(insights): keep deleted mood entries and readings out of status snapshots
Four per-metric status snapshots read mood context without the tombstone
filter, and the blood-pressure snapshot read measurements the same way, so
a deleted entry kept shaping the text written about it. The breathing
screen and the HRV widget probe had the same gap.

One reader now serves the shared mood context, and a guard derives the
soft-deleting models from the schema and fails on any unfiltered read in
the analysis, insight and model-input directories.
2026-10-20 09:15:31 +02:00
Marc-André Bombeck fea1d1e0b5 chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment
lines in 1,308 files lose the work-item identifier at their head. Release
versions and issue references stay; nothing outside comment text
changes, which the script checks per file against a comment-free
printout. healthlog/no-planning-tags is enabled at error level with this
commit, so the tree it lands on is clean.
2026-10-20 09:08:40 +02:00
Marc-André Bombeck 892a819060 chore(lint): add a sweep and a rule for planning tags in comments
Comments used to open with the identifier of the work item that produced
them. The identifiers point into notes that never ship, so a reader meets
something that looks like a reference and leads nowhere.

eslint-plugins/healthlog/planning-tags.js defines the shape once: a head
of tag codes and planning words before an em dash, optionally after a
release version. The version and any issue reference stay; the tag goes.
A line without a version must carry a code that is neither domain
vocabulary (B12, PM2.5, UTF-8) nor used in the file's code.

scripts/strip-planning-tags.mjs locates comments with the TypeScript
parser, rewrites only their text, and refuses any file whose
comment-free printout changes. The healthlog/no-planning-tags rule uses
the same matcher, with an autofix; it is enabled together with the sweep.
2026-10-20 08:52:23 +02:00
Marc-André Bombeck b50b5fcd21 chore(deps): drop four dependencies nothing imports
react-hook-form, @hookform/resolvers, next-themes and @vitejs/plugin-react
have no import anywhere in src/, scripts/, tests/, e2e/ or the build and
test configs; knip only stayed quiet because each sat on its ignore list.
Forms keep a controlled draft and validate with the shared Zod schemas,
and the theme bootstrap is hand-written. The ignore entries, the comments
that still named the form library, and the guideline text go with them.
2026-10-20 08:39:06 +02:00
Marc-André Bombeck 9b93247ae0 test: prime only the EN catalog in the unit setup
The setup file runs once per test file and imported all eight message
catalogs, several hundred kilobytes each, which made setup the largest
share of a component-suite run. It now primes EN, the fallback floor of
the t() chain. A test that renders another locale primes it at module
scope through primeLocales(), which loads the catalog via the production
loader in a file-level beforeAll.
2026-10-20 08:38:08 +02:00
Marc-André Bombeck 5cfd129b02 Merge branch 'fix/v1437-gh-adopt' into release/v1.43.7 2026-10-20 08:18:05 +02:00
Marc-André Bombeck dee6a27ae4 Merge branch 'fix/v1437-data' into release/v1.43.7 2026-10-20 08:12:32 +02:00
Marc-André Bombeck 6e970076c7 test(insights): give the cache access guard a floor on its matches 2026-10-20 07:51:18 +02:00
Marc-André Bombeck 43da1739b2 fix(audit): drop the widget-layout breadcrumb that can no longer write
The layout write resolves the caller, so its refusal is always the
caller's own and auditLog never files it; the call, its dedup and its
tests go, and the refusal stays on the wide event. The series route
tests now pin the breadcrumb call instead of a ledger row the caller's
own request no longer writes.
2026-10-20 07:51:15 +02:00
Marc-André Bombeck 00dfc5a0c0 feat(insights): seal the cached AI insights payload at rest
users.insights_cached_text held the model's briefing, readings and
recommendations about the account in the clear. Migration 0411 adds
insights_cached_text_encrypted (AES-256-GCM, binary layout, purpose-bound
label) and a partial discovery index; the free-text encryption backfill
seals the existing rows one at a time, fail-closed, carrying updatedAt and
guarded on the value it read. Every reader, writer and clear goes through
cached-text-cipher.ts, and a guard keeps the column names there. The
column joins the encrypted-column registry and the key-rotation script,
stays out of backups as operational, and is reset by the data wipe.
2026-10-20 07:30:00 +02:00
Marc-André Bombeck eae2138f3e fix(audit): route the widget-layout refusal through auditLog
The dashboard layout route wrote its validation breadcrumb with a bare
auditLog.create, so it skipped the delegated-only rule and kept filing
the caller's own refusals.
2026-10-20 07:29:53 +02:00
Marc-André Bombeck 97d1052dd1 feat(api): say when a list was cut at its ceiling
A list that stops at its own take and says nothing lets the last row read
as the oldest there is. Capped lists now fetch one row past the limit and
answer meta.truncated and meta.limit on the success envelope, so data keeps
its shape. Applied to the capped lists without a total count: rhythm
events, ECG recordings, sharing grants and activity, illness episodes, side
effects, personal records, security activity, cycles, family history,
allergies and mental-health assessments. The activity feed no longer
reports a list that is exactly full as cut.
2026-10-20 06:53:17 +02:00
Marc-André Bombeck 2e92069c16 test(guards): name the list walk's exception instead of pending entries
The page-walk guard flagged the backfill progress codec's page > 0 check, a
validity test rather than a ceiling, and missed the list walk's own loop
(pageCount - skipped < maxPages). The matcher now reads a counter offset and
ignores a literal bound of 0 or 1. Strava and the rollup chunks walk through
walkPages, so their pending entries go. The dataPoints.list walk stays outside
as a named exception with a match allowance: it is bounded by progress rather
than by a cursor ceiling, streams pages, stops on a slice budget and keeps its
place, and its backfill decides with a sliced verdict.
2026-10-20 06:49:47 +02:00
Marc-André Bombeck 5c1b9889ec fix(google-health): page dailyRollUp chunks through walkPages
Each rollup chunk kept its own token loop with a hundred-page bound. It now
walks through walkPages: a token handed out twice ends the chunk at once
instead of re-reading the same page up to the bound, and a chunk cut off is
reported on the wide event as well as through the client outcome.
2026-10-20 06:49:45 +02:00
Marc-André Bombeck 93b3f36d96 fix(strava): walk activity pages through walkPages and gate the backfill on it
The activity loop stopped at its page ceiling and returned what it had read,
and the backfill stamped the account complete over it: a history longer than
forty pages was cut off for good. The walk now runs through walkPages, which
reports a walk cut off at its ceiling, and the backfill reads backfillVerdict
like the WHOOP and Fitbit backfills: a truncated walk leaves the marker unset
without a retry, and a connection waiting for re-authorisation returns
without walking.
2026-10-20 06:49:44 +02:00
Marc-André Bombeck 415da581f8 fix(client): bound the staged-attachment poll and list every client poll
The Coach polled a staged document every 2.5 s until its content index
existed, which for a format that is never indexed meant for as long as the
Coach stayed open. It now stops with the same upload processing window the
documents list uses. Every client poll, its interval and its stop
condition is listed beside the dashboard cadence constant.
2026-10-20 06:49:23 +02:00
Marc-André Bombeck 9bc95713cb fix(client): keep server modules out of the browser bundle
The side-effect taxonomy imported its enums from the Prisma client entry,
and the recurrence engine the medication wizard previews schedules with
imported the logging context, which brings node:async_hooks, node:crypto
and node:os along. The taxonomy reads the enums entry now, and a rule that
does not parse is reported through an observer the server registers, the
pattern the source-priority parser already uses. A guard walks the import
graph from every client component and fails on a server-only module.
2026-10-20 06:49:17 +02:00
Marc-André Bombeck 4979805d11 fix(audit): keep refusals and job telemetry out of audit_logs
A *.validation-failed row was written on every malformed request, the
caller's own included, and the PR detection enqueue wrote one per batch.
Neither is a security event and nothing reads them. auditLog now files a
refused attempt only when a delegate made it, for the owner's activity
feed; otherwise it rides the wide event as meta.rejected_attempt. The PR
detection enqueue moves to annotate().
2026-10-20 06:49:03 +02:00
Marc-André Bombeck 5eaa813b34 fix(google-health): round imported values through importValue
The mapper kept its own parseFloat(n.toFixed(2)), which rounds a half at
the third decimal down whenever the double sits just below it: a weight of
72 345 g stored as 72.34 kg, a distance of 1 000 005 mm as 1000 m. The
shared importValue rounds on the decimal the value prints as. The rounding
guard drops its last pending entry and also catches a round2 declared as a
const.
2026-10-20 06:27:58 +02:00
Marc-André Bombeck f0449574f8 i18n(nl): follow the session, sleep-rhythm and recovery copy 2026-10-20 06:20:43 +02:00
Marc-André Bombeck 45c027aee2 Merge branch 'fix/v1437-review' into release/v1.43.7
# Conflicts:
#	src/app/api/analytics/route.ts
2026-10-20 06:20:42 +02:00
Marc-André Bombeck 40cf10ba20 test: bring fixtures in line with the nap rule and the recovery read
Correlation fixtures wrote minute-long midday sleeps, which the nap
rule now reads as naps; they write nights. The analytics test mocks the
user read the empty recovery reason makes, and the control-byte guard
asserts the size of its sweep.
2026-10-20 06:06:25 +02:00
Marc-André Bombeck c2133203fb test(google-health): hold the parked walk to the consecutive-attempt rule
A day that brings one more page is progress, so the walk parks again
and counts afresh; only a day that ends at the same edge ends it.
2026-10-20 06:06:24 +02:00
Marc-André Bombeck ca937ba87a test(logging): type the probe handler's request argument 2026-10-20 06:05:56 +02:00
Marc-André Bombeck 1420641746 i18n(nl): add the last-entered-here freshness label 2026-10-20 05:58:27 +02:00
Marc-André Bombeck ea933b91cc Merge branch 'fix/v1437-jobs' into release/v1.43.7 2026-10-20 05:58:02 +02:00
Marc-André Bombeck 72214c0208 fix(logging): carry the route template in http.route
http.route held the concrete pathname, so every record id minted a route of
its own and grouping by route answered nothing. apiHandler recovers the
template from the handler's params and puts it there; http.path keeps the
concrete path, and path-secret redaction still runs on both.
2026-10-20 05:57:20 +02:00
Marc-André Bombeck fbbb535832 feat(api): give every error envelope a meta.errorCode
Roughly half of the refusals answered prose alone, so a client could branch
on the code for one 404 and had to fall back to the status for the next.
apiHandler now completes every error envelope with the status's generic
http.* code when the route named none; a route's own code still wins. The
family joins the published catalogue, and the profile form only shows a
code's sentence when the code has one.
2026-10-20 05:53:37 +02:00
Marc-André Bombeck c7f69fc212 fix(auth): answer 503 when the session store cannot be read
The cookie resolvers folded a failed query into null, so a database restart
answered 401 and every client discarded a good credential. The lookup now
raises SessionStoreUnavailableError, which reaches the wire as 503 with
auth.session.unavailable. Logout still clears the cookie on a failed lookup
and records a warning.
2026-10-20 05:43:58 +02:00
Marc-André Bombeck 9af22f2ab2 fix(account): give account deletion the wipe's transaction budget
The cascade behind user.delete removes every row the account owns, so on a
large record it outlived Prisma's 5 s interactive default and rolled back
with a 500 on every attempt. The deletion stays atomic with the Guardian
check and now carries the same budget as the data wipe.
2026-10-20 05:38:44 +02:00
Marc-André Bombeck f6e0d72193 Merge branch 'fix/v1437-reads' into release/v1.43.7 2026-10-20 05:35:06 +02:00
Marc-André Bombeck 5720b70e8e test(guards): drop the removed chip ledger from the course-anchor builders
The cadence chips no longer build a recurrence context of their own; they tally the compliance ledger, whose adapter is already on the list.
2026-10-20 05:33:21 +02:00
Marc-André Bombeck a6a106f763 docs(api): regenerate the OpenAPI contract 2026-10-20 05:14:07 +02:00
Marc-André Bombeck 248b5dfd6b fix(sleep-score): publish the rhythm floor and name it in the few-nights note
The note hard-coded "of 3" in every locale. SleepScoreValue now carries
rhythmMinNights, and the sheet reads the floor from it.
2026-10-20 05:14:03 +02:00
Marc-André Bombeck 4a8bf1963b fix(measurements): keep unchanged updates on their page and vacuum by threshold
`updated_at` sits in the sync-feed index, so every update that moved it
wrote a new row version on another page and an entry in every index.
Provider re-syncs rewrite many unchanged rows, which grew the table to
several times its live size and sent each unchanged row down the sync
feed again. Migration 0412 adds a row trigger that keeps `updated_at` and
`sync_version` when an update changes nothing else, so such an update is
heap-only and the feed stays silent; it sets `fillfactor = 85` and a 5 %
autovacuum threshold. The maintenance queue gains a nightly check that
vacuums when dead rows pass a tenth of the live ones and warns when the
heap is more than three times what its live rows need, which only a
`VACUUM FULL` in a window gives back. The admin card keeps showing the
operator's runs only.
2026-10-20 05:06:25 +02:00
Marc-André Bombeck 483c84b42e fix(integrations): report a page walk that stops short instead of ending quietly
The WHOOP, Oura and Withings list walks and the Fitbit exercise walk each
kept a page loop that, at its ceiling or on a cursor that did not advance,
returned what it had read and said nothing, and the backfill around it
stamped the history complete. `walkPages` keeps a ceiling but reports a
walk that stops with a page pending, or on a cursor it already followed,
as truncated. A backfill reads that through one verdict: a failure
retries, a truncated collection leaves the marker unset without a retry,
a clean run stamps it. The Fitbit exercise walk may read a hundred pages
during a backfill instead of twelve. A guard keeps page-counting loops out
of the provider directories, with Google Health and Strava listed until
their next change.
2026-10-20 05:06:21 +02:00
Marc-André Bombeck 9fe6f98fdb refactor(workouts): render workout dates through useFormatters
The workout list, the workout detail header and the dashboard's recent
workouts tile each rebuilt their own date formatter from the shared
cache, with a private copy of the current-year check, and so ignored
the reader's date-order preference. They now use useFormatters(), and
the copies are gone.

The timezone formatter guard now refuses any two-argument
getDateTimeFormat call in src/components, so a component cannot build
its own display formatter again. The medication card's day bucketing,
the one other such call, reads userDayKey instead.
2026-10-20 05:01:37 +02:00