Commit Graph
8506 Commits
Author SHA1 Message Date
Marc-André Bombeck eebe4019af fix(insights): ring tiles use TileHeader
The wellness score tiles and the cycle ring tile drew their own icon
and title row instead of TileHeader; the skeleton of the score tile was
a hand-rolled card shell and now composes Card.
2026-10-20 13:36:59 +02:00
Marc-André Bombeck f2af0c9650 refactor(insights): one CrossLinkCard for the card-sized page pointers
The VO2 max pointer on the pulse page and the ECG pointer on the heart
pages were two hand-rolled link cards; the ECG one had no md:p-6 step
and sat narrower than every card beside it. CrossLinkCard composes Card
(TileHeader, one meta line, the call to action) inside the link that
carries the focus ring.
2026-10-20 13:36:58 +02:00
Marc-André Bombeck e7ee416e19 refactor(ui): move 62 hand-written empty states onto EmptyState
The admin sections, the cycle, illness, medication, mood, records and
settings cards, the pickers and dialogs said nothing-here-yet in a
hand-written muted paragraph in five sizes and three alignments. List
and section states now render through EmptyState (plain, compact; the
three with a next step carry it as the action), and the six chart
bodies through ChartEmptyState at their former height. The thirteen
that stay are a field without a value or a hint inside an editor, not
a list state. The ratchet ceiling drops from 75 to 13.
2026-10-20 13:31:02 +02:00
Marc-André Bombeck fe021c0ffd test(ui): freeze the hand-written empty states with a ratchet
Seventy-five surfaces say nothing-here-yet in a hand-written muted
paragraph instead of EmptyState. The ratchet counts them, fails on a new
copy, and fails when a site moves over until its ceiling is lowered.
2026-10-20 13:28:09 +02:00
Marc-André Bombeck a97abe64b9 refactor(insights): one TrendTile frame for the trend tiles
The dashboard KPI tile, the derived-insight sparkline tile, both
skeletons and the three still-building states of the vitals board each
spelled out the same tile frame by hand, and some clipped their content
while others did not. TrendTile owns the frame; the vitals board's
three building states share one BuildingTile. The rendered classes are
unchanged.
2026-10-20 13:26:47 +02:00
Marc-André Bombeck 3aadc03424 refactor(insights): one MetricCrosstab for the three crosstab boards
The mood tag, rated mood factor and cycle phase boards were three
copies of the same list: pair label, neutral signed delta, confidence
chip, detail line, optional dismissal. MetricCrosstab owns that shape;
each board keeps its labels, unit conversion and number formatting.
The cycle board's own confidence strings were identical to the shared
ones and are gone.
2026-10-20 13:24:01 +02:00
Marc-André Bombeck ee2bc8019c feat(ui): ScorePill for every grade chip
Confidence levels, the weekly score gain, target bands and lab range
flags were drawn by hand in two looks: a /15 wash without border in the
mood boards, a /10 wash with a /40 border on the correlation and trend
cards, and medium confidence was blue on one card and amber on the
next. They now render through one ScorePill (rounded, xs, soft wash,
tone success / info / warning / destructive / neutral, optional tabular
figures) with one confidence scale. The target band keeps its own
AA-tuned tokens in the shared anatomy. A structural test refuses a new
hand-rolled grade chip.
2026-10-20 13:24:00 +02:00
Marc-André Bombeck ab3f21c58c refactor(ui): one Segmented control instead of four
The timeline's Segmented, the document sources' SegmentedChoice, the
medication detail's SegmentedToggle and the icon-only ViewToggle were
four implementations of the same control in two looks, two at 36 px on
a phone. ui/segmented.tsx replaces all four, plus the hand-rolled range
rows above the lab and custom metric charts, the mood dimension window
and the inventory quantity mode: a muted track with the chosen segment
raised, radio semantics with roving focus by default, pressed-button
semantics for view switches and form sheets, icon-only segments, and
44 px per segment on a coarse pointer. A test holds the definition to
that one module.
2026-10-20 13:15:29 +02:00
Marc-André Bombeck c849619bee fix(settings): one action row on the Dawarich card
The connected card showed the home country field and a second row of
buttons (Choose home, Save) above the card's own row of details, menu
and primary action. The home editor now opens from the card menu, or
from a prompt while the home place or country is missing, and carries
its own Cancel and Save only while it is open.
2026-10-20 13:15:29 +02:00
Marc-André Bombeck ff583104a8 fix(ui): wrap lab names and token names between words
The lab list wrapped a long analyte name with overflow-wrap: anywhere,
which cut a word that would have fit on the next line in the middle
(Hämo|globin). It now wraps between words and hyphenates in the page
language, cutting only a word wider than the column. Token and client
names in the API and MCP settings used break-all for the same reason
and now wrap the same way; addresses and codes keep break-all.
2026-10-20 13:15:28 +02:00
Marc-André Bombeck 60be63bcd1 feat(ui): InlineAction for small text actions, with a 44 px touch floor
Thirty-three hand-rolled underlined text links and buttons (Open
schedule, Select all, View all, Retry, Learn more, the device-code copy
action, ...) carried three different focus rings, none on half of them,
and a 16 px target on a phone. They now render through one InlineAction
primitive: tone primary / muted / foreground, size xs / sm, a focus ring,
and min-h-11 on a coarse pointer so the desktop layout does not move. A
structural test refuses a new hand-rolled copy.
2026-10-20 13:03:15 +02:00
Marc-André Bombeck ce8074ad08 fix(ui): keep a wrapped label's colon on its last word
The label primitive was an inline flex box, so its ::after colon was a
flex item of its own: when a long field name wrapped in a narrow column,
the colon floated beside the whole text block instead of following the
last word. The label now lays out inline and the colon wraps with the
text. Call sites that set flex themselves keep their own layout.
2026-10-20 13:03:12 +02:00
Marc-André Bombeck 537bad4212 fix(ui): print every displayed decimal in the reader's notation
toFixed() prints a full stop in every language and a locale-less
toLocaleString() follows the browser, so a German reader saw 37.5 next
to 70,5 on the same screen. Fever in the illness log, the cycle phase
crosstab, custom metric values, nutrient day totals, the mood chart and
heatmap tooltips, the mood takeaways, the elevation chart axis, the BP
30-day average, the host metrics clock and the encryption row counts now
go through useFormatters().

The new healthlog/no-unlocalised-decimal rule refuses toFixed(),
toPrecision(), a locale-less toLocaleString() or Intl constructor, and a
quotient rendered straight into text under src/components and src/app.
Rounding through Number(...) stays legal; SVG path data and number-input
values carry a reasoned next-line directive.
2026-10-20 12:55:07 +02:00
Marc-André Bombeck 56daa57350 refactor(format): one byte formatter for every surface
The document vault and the admin area each carried a formatBytes; the
same backup read 1.25 MB in one and 1.2 MB in the other, and only the
vault knew gigabytes. The vault rule (one decimal below 100, up to GB,
digits in the reader's locale) now lives in src/lib/format/bytes.ts and
takes the locale, so the server-rendered share page uses it too. A test
holds the definition to that one module.
2026-10-20 12:55:06 +02:00
Marc-André Bombeck 67cc56f707 Merge pull request #1252 from MBombeck/release/v1.43.7
release: v1.43.7
v1.43.7
2026-10-20 11:38:37 +02:00
Marc-André Bombeck 1e3eed23ab chore(release): v1.43.7 2026-10-20 11:14:20 +02:00
Marc-André Bombeck 71fa8ad7b3 Merge branch 'fix/v1437-r3' into release/v1.43.7 2026-10-20 11:13:27 +02:00
Marc-André Bombeck 5a6c78efe5 fix(jobs): give the full rollup fold an expiry, a budget and a lock
The fold was declared short on the strength of its 60-second
statements, but four aggregates and four persist transactions of up
to 120 s come to about thirteen minutes at worst under the defaults,
and nothing bounds them once an operator lifts the statement timeout.
Past pg-boss's fifteen-minute default the retry folded the account a
second time. The send now carries an hour's expiry, the fold stops
between granularities on the job's budget, and the binding holds a
per-account lockedPass outside the lane. The runtime guard now requires
every heavy queue to be declared long.
2026-10-20 11:06:17 +02:00
Marc-André Bombeck 7e6ed676e0 docs(insights): describe the ECG section's failure row as it is
The page comment still said the overview section unmounts silently on
a read error; it shows a QueryErrorRow, which is what the UI standards
ask of a supplementary block. The page keeps the full error card.
2026-10-20 11:03:18 +02:00
Marc-André Bombeck bd00845be3 fix(strava): clear the backfill markers when the connection is removed
Disconnecting Strava, or deleting its credentials, cleared the tokens
and the athlete but kept stravaBackfillCompletedAt and
stravaBackfillResumeBefore. A later connection inherited them: a
completed marker kept its history from being read, and a resume point
started its first walk below the previous account's oldest activity.
Both paths now clear one shared set of columns.
2026-10-20 11:02:50 +02:00
Marc-André Bombeck 742705eadd docs(audit): say why two import refusals stay audit rows
The export imports log a refused upload as a signal. The admin import
and the medication intake import kickoff keep audit rows on purpose:
one is an administrator acting on another account, the other is shown
to the record's owner in the activity feed.
2026-10-20 11:01:19 +02:00
Marc-André Bombeck 9a5c2b8ec1 fix(api): keep data.truncated on the account activity feed, deprecated
v1.43.6 shipped truncated as a required field of the activity
response's data, and moving it to meta alone would break a client
built against that in a patch release. data.truncated is back with the
same value as meta.truncated and is marked deprecated in the spec; the
web client keeps reading meta.
2026-10-20 11:00:25 +02:00
Marc-André Bombeck 2be262d574 refactor(api): drop apiPostList, which no capped list reads
Every capped route is read with GET, so the POST reader had no caller
and failed the dead-code gate. The list-cap guard now binds a capped
route to apiGetList alone, which is the reader that exists.
2026-10-20 10:58:03 +02:00
Marc-André Bombeck b08d527045 test(auth): mint session fixtures in the shape the lookup accepts
The session time-zone test stored sessions under readable secrets that
the cookie shape check now refuses before the query, so it tested the
refusal instead of the expiry. A shared helper mints hls_ plus 32 hex
bytes, and the two route tests that set a session cookie use it too.
2026-10-20 10:56:55 +02:00
Marc-André Bombeck 4f02004146 test(guards): derive the rounding guard's directories from the connection keys
The guard kept its own provider list and its own comment stripper next
to the shared PROVIDER_DIRS and stripComments helpers, so a new
integration could slip past it. It now uses both, plus the file
importers, which are not a connection.
2026-10-20 10:55:42 +02:00
Marc-André Bombeck c50007ef61 fix(day): read the notable history through the fenced cache path
The comparison history blocks were read with cache.get and stored with
cache.set, outside the single-flight slot that an invalidation fences.
A read that spanned a write cached the days from before it for the
block's whole TTL, and two concurrent reads of a block both queried.
Each block now goes through cache.wrap; the blocks missing at the time
still share one read of their span.
2026-10-20 10:55:09 +02:00
Marc-André Bombeck 19732b5aa1 fix(jobs): let a sliced import give way to a waiting heavy job and warn on long lane waits
A job sent back by the heavy lane now leaves word that the lane is
wanted. The holder, and only the holder, sees it through laneWanted();
the Google Health backfill asks it where its slice budget already
stops resumable walks, after the first page, ends the slice there and
queues the rest past the deferred job's return. Holders that cannot
stop part-way run to their end as before.

The way back to the queue now returns when the oldest deferred job was
first queued; a job that has stood in line for two hours is reported
once per holder as job.heavy_lane.waiting_long at warn, so a lane held
for hours reaches the alerts.
2026-10-20 10:52:54 +02:00
Marc-André Bombeck 9c89503019 fix(migrations): bound 0412's lock wait where it takes effect and retry a lock timeout on boot
migrate deploy runs a migration file statement by statement, so the
top-level SET LOCAL lock_timeout in 0412 did nothing: the migration
waited as long as the lock was held, with every write to measurements
queued behind it, and a file that failed half-way stayed half-applied.
0412 now takes its locks inside one DO block with a statement-scoped
timeout, which rolls back whole, after an idempotent CREATE OR REPLACE.

The entrypoint runs migrate deploy through scripts/migrate-deploy.mjs.
A 55P03 failure of a migration marked -- @retry-on-lock-timeout is
marked rolled back and retried after 15/30/60/120/240 s, read from
Prisma's report or, after P3009, from the stored row. Every other
failure stays fatal. A guard holds migrations to the shape that makes
the marker true, and 0411/0413 add their columns with IF NOT EXISTS so
a manual re-run is safe too.
2026-10-20 10:45:14 +02:00
Marc-André Bombeck 47f20b773d fix(auth): answer 503 for every session lookup failure that is not about the value
The catch kept a denylist of "store unreachable" signals and folded
everything else into a missing session. The pool's own connect timeout
carries no code, and 53300, 53200 and 55P03 are about the store, so all
four answered 401 and signed the client out. The shape check already
refuses crafted values before the query; the catch now keeps only a
short allowlist of input refusals (SQLSTATE class 22, Prisma
validation) as null and raises 503 for the rest.
2026-10-20 10:36:16 +02:00
Marc-André Bombeck 2b8defcbd2 test(insights): mock the list reader the ECG and rhythm reads now go through 2026-10-20 10:22:26 +02:00
Marc-André Bombeck e5b7eebe64 Merge branch 'fix/v1437-r2-jobs' into release/v1.43.7 2026-10-20 10:17:01 +02:00
Marc-André Bombeck 86eb743a69 Merge branch 'fix/v1437-r2-errors' into release/v1.43.7
# Conflicts:
#	src/components/charts/health-chart.tsx
#	src/components/insights/ecg-cross-link.tsx
#	src/components/insights/ecg-section.tsx
#	src/components/insights/rhythm-events-card.tsx
2026-10-20 10:17:01 +02:00
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 3e6a33df35 fix(i18n): one sentence each for the read-failure lines
The ended-session and spent-budget lines each ran to two sentences; the
action sits on the button, so each is one sentence now. The sign-in
label's key is common.signIn, matching what it says. The French strings
added in this release use vous like the rest of the bundle, and the
Korean sleep-score gap labels take the polite form the bundle uses.
2026-10-20 10:10:57 +02:00
Marc-André Bombeck 923249dbf6 fix(ui): draw every failed read through a read-failure primitive
Around thirty surfaces drew a failed read their own way: a bare
destructive paragraph with no retry, a muted line saying the data was
still being learned, a skeleton that never ended, an error card nested
in a tile, or nothing at all. None of them told an ended session from a
refusal. They now use QueryErrorRow or QueryErrorCard with the query's
error and a retry, inset where a card already frames them, and keep
their card and title where the page would otherwise jump. Hydration and
caffeine now fail the same way.

The recovery page no longer points to Integrations while Oura's first
resilience level is simply on its way.

A guard reads every read hook's isError branch and refuses hand-rolled
markup there, and refuses a framed error row inside a card.
2026-10-20 10:10:49 +02:00
Marc-André Bombeck a1b56f0b75 fix(visits): keep the since-last-visit card on a failed read
A failed read replaced the card and its title with a bare row, so the
section changed height and the reader lost what had failed. The card now
keeps its header and shows the error inside. The visit card's header
moves onto TileHeader instead of its own icon and title row.
2026-10-20 10:10:36 +02:00
Marc-André Bombeck 3fbd16a0a6 fix(ui): let the chart error card decide from the error too
ChartErrorState took no error, so a chart whose read was refused or
whose session had ended offered a retry that could not help. It now
takes the error and branches the way QueryErrorCard and QueryErrorRow
do, the chunk boundary hands it the error it caught, and the error-prop
guard covers it.

QueryErrorRow gains an inset form for a failure inside a card body, and
its frame takes the md:p-6 step. QueryErrorCard drops its fixed px-6 for
the card's own inset and both icons move onto the size scale.
2026-10-20 10:10:31 +02:00
Marc-André Bombeck 985372f711 fix(insights): count failed fetches toward a bounded poll's ceiling
The status and advisor polls counted attempts on dataUpdateCount, which a
failed fetch does not move, while the last good payload stayed in place and
still said preparing. A request that kept failing polled an open page for
as long as it stayed open. Attempts now include the failed fetches.

The poll inventory moves from a comment into a guard held to every
refetchInterval in the tree; four polls were missing from the comment.
2026-10-20 10:06:11 +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 b12bece491 fix(import): log a refused upload as a signal, not an audit row
An upload refused for its size, a broken stream or a stopped worker is an
operational outcome, not something done to the account. The admin import
keeps its audit rows, since an admin acting on another account is.
2026-10-20 10:03:30 +02:00
Marc-André Bombeck 689796db33 fix(integrations): log a failed sync as a signal, not an audit row
The audit log is the trail of what a person or an admin did to an account.
Every failed provider sync wrote a row there, burying that trail, and kept
the provider's error message in plain text while the status row seals it.
A failed sync now emits an integrations.sync.failed warning with the same
counts and no message.
2026-10-20 10:03:30 +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 537a73d3b2 refactor(measurements): drop the in-memory range aggregation nothing calls
It bucketed by UTC day and was described as the route's fallback; no route
has called it since the aggregation moved into SQL.
2026-10-20 09:58:03 +02:00
Marc-André Bombeck d91eb905fe refactor(compliance): require the zone a dose window is read in
The UTC reading of a window's HH:mm was right only for a UTC user, and the
one production caller always passes the zone.
2026-10-20 09:58:03 +02:00
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 6e54f33901 refactor(i18n): ask the catalogue for a sentence in one helper
`t()` answers a missing key with the key, so "does this code have a
sentence" was a comparison written inline at every site that wanted a
code's sentence or a fallback, twenty-odd times in several spellings.
translateOrNull, translateCodeOrNull and translateCode in
src/lib/i18n/translate-code.ts now hold that probe, every site uses
them, and a guard refuses the inline comparison anywhere else.
2026-10-20 09:53:31 +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