Commit Graph
880 Commits
Author SHA1 Message Date
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 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 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 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 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 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 c145843aa3 fix(insights): let a read's ApiError reach the failure primitives
Five queryFns caught the ApiError and threw a plain Error with a
translated line, so the read-failure primitives always saw a generic
failure: an ended session offered Retry instead of Sign in, and a
refused share drew an error card. The queryFns now pass the error
through; the message lives on the primitive. The ECG list also says a
failed read instead of rendering nothing.

The guard now also reads every queryFn and refuses a catch that
re-throws a plain Error.
2026-10-20 09:38:57 +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 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 dee6a27ae4 Merge branch 'fix/v1437-data' into release/v1.43.7 2026-10-20 08:12:32 +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 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 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 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
Marc-André Bombeck 862361a224 fix(recovery): publish why the recovery page is empty from the server
The page worked out its empty note on the client from the integrations
status, so every client had to repeat the rule. The analytics summaries
slice now carries `recoveryEmpty` (general, oura_permission or
oura_no_resilience, null when any recovery type has a reading), decided
once in resolveRecoveryEmpty from the gated summaries and the stored
Oura grant. The page renders that value only, and its block list is
typed to the server's recovery types so the two cannot drift.

Each reason now reads as one sentence in every locale, and every reason
offers the same way on, the integrations settings.
2026-10-20 05:01:18 +02:00
Marc-André Bombeck 0ea8880923 chore: drop planning tags from comments the review touched 2026-10-20 04:23:58 +02:00
Marc-André Bombeck ad9c9cd0cd fix(day): head the since-last-visit card with TileHeader at the compact density
The card hand-built an uppercase h3 and ran a density of its own. It
now takes TileHeader size="sm" (which gains an h3 heading for a card
under a section that owns the h2) and the compact card rhythm.
2026-10-20 04:23:32 +02:00
Marc-André Bombeck 3cf67d05f9 fix(ui): let the read-failure primitives decide by the error
About forty compact rows and seventy cards still offered a retry on a
spent read budget and drew an error card on a refusal, while a third
primitive (ReadFailureRow) did the right thing at eight places.

QueryErrorRow and QueryErrorCard now take the query's `error` and read
it through readFailureKind, which moves to src/lib/api next to
ApiError: a refusal (403) draws the surface's refused state or nothing,
a spent budget (429) says so without a retry, an ended session (401)
says so and offers sign-in instead of going blank, and only a real
failure offers the retry. ReadFailureRow is gone and every site passes
its error; a guard test fails on a site that does not.

The coach read strip keeps its card in the error state and holds one
body height across loading, failure and a one-line read.
2026-10-20 03:38:57 +02:00
Marc-André Bombeck 7be3714ac2 Merge pull request #1246 from robdegraaf/feat/i18n-nl 2026-10-20 00:14:49 +02:00
Marc-André Bombeck f05b2f71e7 Merge branch 'fix/v1436-oura' into release/v1.43.6 2026-10-19 20:20:29 +02:00
Marc-André Bombeck 3ff860591e Merge branch 'fix/v1436-ui' into release/v1.43.6 2026-10-19 20:20:28 +02:00
Marc-André Bombeck 9095b1037e fix(ui): draw nothing for a refused read and no retry for a spent budget
The failure rows added for supplementary reads offered a retry for any
error. The usual-range strip reads at manage level, so a viewer on a
read-only share saw a permanent failure with a retry that could never
succeed; the visit preparation reads measurements under visits that
need only the profile section, which a share can grant alone. A refused
read (401/403) now draws nothing, a spent read budget (429) says so
without a retry, and only a real failure keeps the retry row. The
cycle strip holds no cell for a refused calendar read.
2026-10-19 19:49:14 +02:00
Marc-André Bombeck b5285f0f65 test(recovery): settle the connection status before reading the empty note
The recovery page now holds its empty note until the integrations
status has loaded. The data-gating test rendered without that status
and saw the loading placeholder; it now seeds an account with no
integrations, the settled answer the general note belongs to.

Refs #1204
2026-10-19 19:46:03 +02:00
Marc-André Bombeck c016ec8aec fix(recovery): claim no Oura permission the server cannot prove
The empty recovery note told every connected Oura account without a
recorded permission gap that its permission covers resilience. The
server cannot know that: the scope note is cleared on each reconnect
and written only by a sync that reached Oura, and a missing health
block says nothing either way. The note now asks for a reconnect only
when the gap is recorded and otherwise says only that no level has
arrived, pointing at the Oura card. It also waits for the connection
status instead of showing the general note first and swapping it.

Refs #1204
2026-10-19 18:31:45 +02:00
Marc-André Bombeck 0e6e50fc7b Merge branch 'fix/v1436-ui' into release/v1.43.6 2026-10-19 17:04:31 +02:00
Marc-André Bombeck 09c93cd547 Merge branch 'fix/v1436-oura' into release/v1.43.6 2026-10-19 17:04:29 +02:00
Marc-André Bombeck 38c106e358 fix(ui): draw no empty frame and say when a read failed
"Since the last visit" was wrapped in a card by the visits section, so
the card stayed on screen empty while the window loaded, when it held
nothing and when the read failed. The component now owns its card and
draws it only with a row; a failed read shows a compact error row.

The same shapes elsewhere: the caffeine card and the mood forecast no
longer open a loading card that most often collapses, and a failed read
on them, the mood tag catalogue, the usual-range strip, the cycle ring
and the measurement drill-down now says so with a retry instead of
passing for no data. The scores strip keeps the ring's cell on a failed
calendar read. The mental-health card no longer draws an empty action
slot for a viewer without the right to start.

The frame guard now also flags a frame whose only content sits behind a
condition.

Refs #1241
2026-10-19 16:43:52 +02:00
Marc-André Bombeck d38ceb4beb fix(sleep): say why consistency or timing has no value
A rhythm sub-score below three nights with a main sleep was dropped from
the blend and its row read "no data", although the nights were there;
since a nap-only day no longer has a place on the clock, a ring worn
for only a few nights reaches that state more often. Each withheld
consistency or timing sub-score now carries the reason
(`few_nights`, `no_main_sleep`) and the score value carries how many
nights the rhythm is built from, so the row reads "not enough nights
yet (2 of 3)" or "latest night had no main sleep" instead. Neither is
ever shown as 0.

Refs #1204
2026-10-19 15:15:46 +02:00
Marc-André Bombeck ff3559c169 fix(workouts): render workout dates in the profile timezone
The workout list, the workout header and the dashboard tile formatted
instants through the shared formatter cache without a timeZone, so they
followed the browser's zone. The year comparison in the list used the
browser's calendar as well. All three now take the profile zone, and the
timezone guard also matches getDateTimeFormat(locale, options) calls that
leave the zone out.
2026-10-19 14:56:53 +02:00
Rob de Graaf c6f61fad33 feat(i18n): add Dutch as an eighth end-to-end locale
Adds messages/nl.json (AI-initial, informal je/jij register, full key
parity with en.json) and registers nl everywhere the locale list lives:
config, loaders, Intl/date-fns mapping, zod enums, OpenAPI spec, scripts,
the managed-profile picker and the notification locale guards.

Dutch follows the Korean pattern for the AI surfaces (English body plus a
Dutch reply-language footer, no safety-contract matrix) and gets its own
banks in the outbound safety screen (dose, risk, causal, continuation),
the prose-grounding and citation-coverage tables, the reference-range
prose and the no-key fallback copy, each with Dutch test fixtures.

PHQ-9 / GAD-7 / WHO-5 are not marked validated for nl yet: the Dutch
wording has not been read against the published forms, so Dutch readers
get the English items plus the existing note until it has.
2026-10-19 11:06:55 +02:00
Marc-André Bombeck 40b1389970 fix(recovery): name what is missing on an Oura account's empty recovery page
An Oura ring fills the recovery page through its resilience level alone.
The empty note listed every source that could fill the page, so a
connected ring with no level read the same as no wearable at all. The
note now follows the server's view of the Oura connection: when the
grant leaves out the heart-health data the level sits behind, it asks
for one reconnect and links to the integrations settings; when the grant
covers it, it says Oura has not sent a level for this account yet.

Refs #1204
2026-10-19 10:53:59 +02:00
Marc-André Bombeck 8ba4d1b0d0 fix(nutrients): name the form an upper limit covers and phrase it per locale 2026-10-16 05:31:33 +02:00
Marc-André Bombeck b838ed4403 feat(dawarich): fetch daily movement values from a Dawarich instance
Connect, status, places, settings, test, sync and disconnect replace their
stubs. A nightly job (03:20) and the connect backfill read the timeline,
visits and residency endpoints through safeFetch and store one row of daily
values per local day: distances, places visited, minutes away from the home
place and the sealed country with its travel-day verdict. A private instance
needs its exact origin in DAWARICH_PRIVATE_ORIGINS.

The connection joins the shared ledger and the connection health model,
watched for errors only. The day carries a movement block, the model
projection drops the travel country, the two movement channels join the
correlation matrix under the environment module, travel days become trip
suggestions, and the days travel in the environment backup section.
2026-10-16 01:20:53 +02:00
Marc-André Bombeck 3fc37c9183 Merge branch 'v143/ios' into release/v1.43.0 2026-10-16 00:28:40 +02:00
Marc-André Bombeck 01fef691fa feat(web): edit a supplement's composition, read it from a label photo, and show nutrient sources with the upper limit 2026-10-15 23:48:43 +02:00
Marc-André Bombeck 79f7eb199b feat(insights): resolve direction verdicts, chart reference lines and mood patterns on the server 2026-10-15 23:39:11 +02:00
Marc-André Bombeck b91328d67e feat(records): publish an age- and sex-aware benchmark beside each personal record 2026-10-15 23:18:58 +02:00
Marc-André Bombeck 6979928d73 chore(web): drop toast imports the error toast helper made unused 2026-10-15 22:36:34 +02:00
Marc-André Bombeck f7386742b4 feat(web): show the error id on server failures and add the support info and report surfaces 2026-10-15 22:31:51 +02:00
Marc-André Bombeck 6313c0174a Merge branch 'v1424/readiness' into release/v1.42.4 2026-10-15 15:24:29 +02:00
Marc-André Bombeck 73922c039e fix(insights): say what fills the recovery page when it is empty
The empty note told a connected Oura account to connect Oura. The page
charts the WHOOP and Polar recharge, strain and load readings and the
Oura resilience level; Oura's readiness is the recovery score linked
above. The note now says so.
2026-10-15 15:10:18 +02:00
Marc-André Bombeck 7c905249fb fix(medications): page the side-effect list and name every GLP-1 medication in the timeline 2026-10-15 15:05:51 +02:00
Marc-André Bombeck b7eef0dff4 feat(charts): drop the explanatory line under day-linked charts 2026-10-11 13:39:20 +02:00
Marc-André Bombeck 7a43131883 fix(db): track the time-zone pin's failure log as background work
The pin's SET on connect runs detached; its failure line now registers
with the background-task tracker like every other detached writer. The
coach thread test reads the new budget reset copy.
2026-10-11 13:18:08 +02:00
Marc-André Bombeck dcb7a0bc09 fix(ai): count the daily AI budget on the account's own day
The token ledger keyed every day on UTC while the daily briefing and the
other per-day generations run on the account's local day, so east of UTC
the first generations after local midnight were refused against the
previous day's spent ceiling. The budget day is now the calendar day in
the account's time zone; each request still resolves it once and books
its reservation and reconcile on the same row. The medication extraction
reads relative phrases against the same local day, the operator's spend
card shows the operator's own day, and the limit copy says the budget
resets at midnight in the person's time zone.
2026-10-11 13:09:12 +02:00
Marc-André Bombeck 39fc4e4da2 feat(shell): run the docked strips the full height and make them the only fold
The docked panels' strips now run from the top edge of the window to the
bottom, and the top bar ends left of them. The panel header no longer
carries a fold button in docked mode: the strip opens and closes the day
and, on /coach, the conversations; opening moves focus to the panel's
heading and closing returns it to the strip. The sheets keep a close
button, the day's now an X at the end of its header.

The command palette's door is a magnifier at every width, named Search,
with the shortcut in its tooltip. g p opens or closes the day like a
click on its strip, and below the docking width opens the sheet on the
last day or closes it; the shortcuts list shows it under Day.
2026-10-10 20:04:19 +02:00