Commit Graph
8446 Commits
Author SHA1 Message Date
Marc-André Bombeck 2f32a4458c fix(environment): end the nightly window on the person's today
The default lookback ended on the UTC date, so east of UTC the local today
stayed out of the window until the next night's run.
2026-10-20 09:58:02 +02:00
Marc-André Bombeck 2372fee376 fix(coach): count the daily question limit from the person's midnight
The limit on clarifying questions reset at UTC midnight, which is the middle
of the night in Europe and the afternoon on the American west coast. It now
counts from the start of the person's own day.
2026-10-20 09:58:02 +02:00
Marc-André Bombeck f4262e2cc6 test(guards): see a squared two-argument distance as a hand-rolled spread 2026-10-20 09:51:13 +02:00
Marc-André Bombeck 5cbe52cada test(guards): count the allowed sites per file instead of exempting the file
An exempted file covered every bare formatter or clock read written into it
later. Each entry now states how many sites it holds, and a file with one
more fails like any other.
2026-10-20 09:51:12 +02:00
Marc-André Bombeck 9b05d8c215 test(guards): derive the provider directories from the connection keys 2026-10-20 09:51:12 +02:00
Marc-André Bombeck 5fdddaec0a test(guards): strip comments through the shared scanner 2026-10-20 09:51:12 +02:00
Marc-André Bombeck 5d1d6aa176 perf(day): cache the notable history in fixed blocks
Each cell ran from a 90-day block boundary to today, so paging back through
days left a nearly full copy of the two years per boundary in an unweighted
bucket, and a cold day long ago read everything from its window to today.
The history is now cached in 90-day blocks that end before today (the block
holding today ends at today and carries it in its key), a window is put
together from the blocks it touches, and the blocks it misses are read in
one query. A cell holds at most one block of days per type.
2026-10-20 09:47:03 +02:00
Marc-André Bombeck 6eb7e508df test(measurements): a re-import that changes nothing keeps its syncVersion
The second import of the same export writes every column of the step row
as it already was (compared column by column), so the update is a no-op
and migration 0412 keeps updated_at and sync_version. The test expected the
version to move on every write; it now pins that it never goes backwards
and stays where the first import left it.
2026-10-20 09:45:19 +02:00
Marc-André Bombeck c782fa060a fix(migrations): bound 0412's lock wait on measurements
CREATE TRIGGER waits for an autovacuum that is preventing wraparound, and
every write to the table queues behind the waiting request, the running
release's included. SET LOCAL lock_timeout fails the migration after ten
seconds instead, and the file rolls back whole.
2026-10-20 09:38:29 +02:00
Marc-André Bombeck a7f1e72bad fix(maintenance): leave vacuuming measurements to autovacuum
Migration 0412 starts autovacuum on the table at 5 % dead rows, and the
nightly check vacuumed at 10 %, a point autovacuum had always passed first.
Two triggers for one task only hid which of them did it. The nightly check
now measures and changes nothing: it warns when the dead rows reach four
times autovacuum's threshold, which means autovacuum is not keeping up, and
when the heap has outgrown its live rows. The operator's run keeps its
steps.
2026-10-20 09:38:29 +02:00
Marc-André Bombeck d770f81278 fix(integrations): end a page walk on an empty cursor
WHOOP and Oura answer the last page with an empty next_token as well as
with null. The walk followed the empty string as a cursor, the client sent
no token, read the first page a second time and the walk ended as a repeat,
so the records came in twice and the backfill stayed incomplete for good.
An empty cursor now ends the walk, for every provider that walks through it.
2026-10-20 09:34:21 +02:00
Marc-André Bombeck 34eea153a8 fix(strava): resume an unfinished backfill where the last run stopped
One run reads at most forty pages of a hundred activities. A longer history
ended as incomplete with no note of how far it got, so every boot read the
same newest pages again, spent up to 190 of the instance's 200 requests per
fifteen minutes on them, and never reached the older activities.

The run now records the start of the oldest activity it read and sends the
next run for after the rate window; that run asks Strava for the activities
before it. A run that gets no further records nothing and sends nothing. The
place is cleared when the backfill completes.
2026-10-20 09:33:15 +02:00
Marc-André Bombeck 456976175b test(sql): allow the lane's clock read against pg-boss's timestamptz column 2026-10-20 09:32:34 +02:00
Marc-André Bombeck bfeac46f06 fix(jobs): send a heavy job back to its queue instead of waiting in the handler
pg-boss counts a job's expiry from the fetch and ends the handler at it, so
a job that waited for the heavy lane spent its expiry in line: a long import
let the weekly backup expire through every retry, and an upload that waited
past its own expiry was failed, stopped counting as in use and lost its
staged file to the sweep. A job that finds the lane taken now returns to
created with start_after a minute ahead, at the same attempt, in one
statement, and runs when the lane is free.

The full-history integration drain joins the lane, and the guard checks that
every heavy queue's policy lets a job go back to created.
2026-10-20 09:25:52 +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