Commit Graph
9450 Commits
Author SHA1 Message Date
Marc-André Bombeck 798747f987 test(e2e): walk the app shell at every tablet size
tablet-shell.spec.ts runs on WebKit with an iPad profile (webkit-tablet,
in the WebKit job) and on Chromium with an Android tablet profile
(chromium-tablet, in the shards), across iPad mini to iPad Pro 13 in
both orientations, Safari's bars, Split View and Slide Over. It checks
that the shell fills the window and leaves the document nothing to
scroll, that every rail destination scrolls into view on top and takes a
tap, that the scroll cue follows the overflow, that main reaches its
last line clear of the bottom bar, that the page scrolls again after a
sheet and a dialog close, and that a tab strip leaves the vertical pan
to the page.

ui-standards records the shell and rail rules.
2026-10-23 18:23:49 +02:00
Marc-André Bombeck 22976337cb fix(tabs): leave the vertical pan to the page
touch-pan-x on the tab strip refused a vertical pan that began on it, so
a swipe up or down over a tab strip scrolled nothing. The strip already
pans sideways through its own overflow.
2026-10-23 18:23:49 +02:00
Marc-André Bombeck 1f4c1bd2f0 fix(rail): keep every destination reachable under a finger
A touch tablet keeps the rail's 44 px rows, so in landscape three to
seven destinations sit below the fold, and the list has no scrollbar at
rest. The edge fade alone did not read as "swipe here", and a swipe that
reached an end chained to the document, which held the next one.

The list region moves into its own module. Under a coarse pointer it
contains its overscroll, keeps a focused row clear of a new scroll cue,
and shows that cue at an edge with more destinations; a tap scrolls the
list most of a screen on. The cue stays out of the focus order and the
accessibility tree: the keyboard and screen readers already reach every
row. The rail takes the home indicator's inset so the account row stays
clear of it. A mouse sees no change.
2026-10-23 18:23:48 +02:00
Marc-André Bombeck 6ad61cdd1f fix(shell): take the window's edges and keep the document still
The authenticated shell was sized by h-dvh. On iPadOS Safari that value
and the area the browser lays out disagree after a rotation and under the
floating toolbars: a short value left a strip of page colour under the
shell with the last card cut off above it, a tall one let the document
scroll by the difference and take the swipes meant for main.

The shell now takes the window's edges (absolute inset-0 against the
initial containing block), and the document under it gets no scroll and
no rubber band (data-scroll="shell": overflow hidden, overscroll none,
height 100% so the box survives a dialog's scroll lock making the body
relative). Not fixed: a fixed box re-rasterises every page in a layer of
its own and traps the bottom bar's z-index.

The desktop shell's main now clears the home indicator of an installed
app, and the page frame clears the Coach button there with pb-24: the
button sits 5.5rem up, so pb-20 left the last 8 px under it. The
sub-shell grid floor follows the new padding.
2026-10-23 18:23:48 +02:00
Marc-André Bombeck 3b6e0bf0fb fix(i18n): write the locale to the account once per tab, not on every page
Every page load mounts the locale provider, and its mount effect sent
PUT /api/auth/me/locale with a value that had not changed. Every
authenticated write is charged to the account's shared write bucket, so
each page view spent one write. The mount write now happens once per tab
session and locale; a switch still writes at once.
2026-10-23 18:09:15 +02:00
Marc-André Bombeck c0d387025d test(guards): strip comments the way the compiler reads them, in every guard
Forty-two guard files carried their own comment stripper, block comments
first in one non-greedy pass, so a /* inside a line comment opened a
block that hid the code after it, and the stricter copies hid code
behind a /* inside a string, which every e2e route glob has. They all
read through the shared stripComments now, and it scans the way the
parser does: strings, template literals and regular expressions keep
every character, a comment inside a template expression goes, and line
numbers are kept. A parity test strips every source file both ways,
with the scanner and with the TypeScript parser, and fails on the first
that differs.

No guard reports anything new. Each one fails when the stripper returns
nothing; three of them passed on an empty file and assert that the code
they ban is read at all now.
2026-10-23 18:09:10 +02:00
Marc-André Bombeck 584f799da3 test: drop the timings of test files that no longer exist
Thirteen of the table's files had been deleted or renamed since it was
written. The entries were dead weight; nothing reads a timing for a file
that is not there.
2026-10-23 17:54:22 +02:00
Marc-André Bombeck 3d26cee566 fix(import): take the app's own full backup and JSON export back
The JSON import refused the app's own files: the full backup writes an
empty note, tag list or glucose context as null and the JSON export as
an empty string, and the import schema read either as a value of the
wrong type. The file is now read in the exporter's terms before it is
validated: the export's envelope is opened, every null or empty field
is treated as absent, a backup row carrying a deletion is left out and
counted as skipped, and a unit the app converts on every other way in
is converted to the stored one.

The round trip also showed the portable backup dropping what a reading
itself says: glucose context, sleep stage, range bounds, rhythm and
device. They ride the portable file now, and a portable restore puts
them back. Sync bookkeeping stays out.

An integration test exports an account three ways (full backup, JSON
export, measurement CSV), imports each into a fresh account and compares
what the import carries.
2026-10-23 17:33:58 +02:00
Marc-André Bombeck 8a11b7bfc6 fix(charts): measure axis labels at the size they are drawn under reduced motion
The reduced-motion backstop gives every element a near-zero transition,
and the default transition property is all, so the font size Recharts
writes into its measurement span started a transition and the read in
the same task returned the previous size. Labels were sized at whatever
the last one had, or at the inherited 16 px: the last date label moved
in from the edge and value labels that fit were dropped as colliding,
differently from one visit to the next. Nothing in the measurement host
is ever seen, so nothing in it transitions now. A browser test pins the
read under reduced motion and without it.
2026-10-23 17:21:06 +02:00
Marc-André Bombeck a0dce10c1a fix(activity): word the refusals that now reach a delegate's feed
The custom-metric create and update refusals and the medication layout
refusal are filed through auditLog now, so a delegate's refused request
reaches the owner's activity feed, where it read as the generic change.
Each gets its own sentence in every bundle; the refusal sentences live in
their own function. The audit write guard states its floor, and the
schedule-reader guard names the medication create and edit, which answer
through medicationDetail.
2026-10-23 17:19:21 +02:00
Marc-André Bombeck fb9cd9b965 refactor(ai): move the stored-key opener and the Anthropic host check out of provider.ts
Both are self-contained; provider.ts stays within its size baseline.
2026-10-23 17:19:21 +02:00
Marc-André Bombeck c82ed69549 Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-server
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
2026-10-23 17:18:53 +02:00
Marc-André Bombeck 51cba52751 fix(medications): answer writes with the shapes the contract declares
The create, its mirror replay and the edit answered the written row
without the read-model fields the declared resource carries (reminder
state, stock and runway, actionability, course status, composition,
links), while the detail read answered them. They answer the read model
now, built from the row they just wrote (medicationDetail), so a client
reads back exactly what GET /api/medications/{id} would say.

A dose write that converged onto an existing slot answered the slot
upsert's narrow row, without userId, medicationId, updatedAt,
autoRecorded, autoMissed or attributionSource. The upsert reads and
returns the whole event row, the same shape a write that inserted its own
answers. The parity walks drop their contract skips and are re-pinned.
2026-10-23 17:10:02 +02:00
Marc-André Bombeck 521b85bfbd fix(openapi): declare the mirror replay 200 of a medication create
A mirror create re-posting a pair the caller already holds has always
answered the existing medication with 200, as the description says; only
the 201 was declared.
2026-10-23 17:10:02 +02:00
Marc-André Bombeck 55a6287069 fix(audit): file every refused request through auditLog()
Twelve routes wrote their <surface>.validation-failed row with a bare
prisma.auditLog.create, so they skipped the two decisions auditLog makes:
an owner's own refused request stays on the wide event
(meta.rejected_attempt) instead of becoming a row, and a delegate's names
the delegate as the actor. The bulk medication and mood ingests, the
layout, overlay, device, health-sync, metric and ingest routes all go
through auditLog now, and a structural guard keeps raw writes to the one
writer and the briefing failure marker. The unit tests assert the auditLog
call, and the bulk walks are re-pinned without the owner's refusal row.
2026-10-23 16:54:57 +02:00
Marc-André Bombeck 89b6189206 test(measurements): call the fire-and-forget mock with a starter function 2026-10-23 16:54:56 +02:00
Marc-André Bombeck cf37f833e8 fix(insights): keep the briefing blocks of a reply that validates
The briefing prompt asks for dailyBriefing, trendAnnotations and
storyboardAnnotations beside the insight fields, and the briefing card,
the dashboard snapshot and the today read consume them. insightResultSchema
was a strict object, so it dropped exactly those blocks from a reply that
validated, while a reply that failed validation kept them. It is a loose
object now: the named fields are checked and the rest of the reply stays.
2026-10-23 16:44:50 +02:00
Marc-André Bombeck b02caed221 fix(ai): treat a stored provider key that cannot be opened as no key
A key id retired from ENCRYPTION_KEYS, or a corrupt row, made decrypt throw
inside the provider builders, and a briefing generation answered 500. An
unreadable key now drops its entry from the chain like a missing one, with a
warning on the event, so the generation answers the no-provider 422
(ai.provider.none). The connection test reads it as no stored key. The
parity walk holds that case to the contract again.
2026-10-23 16:44:50 +02:00
Marc-André Bombeck 551fbbbdec Merge branch 'release/v1.46.0' into feat/v146-ui 2026-10-23 16:44:26 +02:00
Marc-André Bombeck 976b2ba0bc fix(workouts): read the stored route sample shape for splits and the GPX export
Both ingests store each route point's sample as { t, speedMs?, hr? }, while
computeSplits parsed every sample as a bare instant and the GPX export
skipped anything that was not a string. Splits were therefore null for
every ingested track, and an exported track carried no times. One reader,
routeSampleInstant, takes either shape. The workout detail walk is
re-pinned: its ingested run now answers its first kilometre split.
2026-10-23 16:37:58 +02:00
Marc-André Bombeck 06d55afc46 fix(deps): raise the ip-address floor to 10.7.4
10.7.3 and earlier take exponential time on a crafted Address6 zone
identifier. The copy under the MCP SDK's rate limiter reaches runtime, and
the one under the registry client resolves to the same version now.
2026-10-23 16:33:05 +02:00
Marc-André Bombeck 4e484c3dd2 test(e2e): clear the account's write bucket when the setup flow resets
Every authenticated write is charged to the shared per-account bucket,
and the locale sweep drives one account through eighteen setup runs in
well under a minute, so its last case met the write ceiling. The reset
already cleared the flow's own buckets; it now clears that one too.
2026-10-23 16:32:50 +02:00
Marc-André Bombeck b4166cdc84 fix(logging): keep detached work from renaming the request's wide event
Work a request starts and does not wait for runs inside the request's
context, so an annotate({ action }) from it landed on the request's event
whenever it ran before the event was emitted. The measurement tail's
status re-warm logged a hand-entered reading as
insights.status.invalidate.skipped on some requests and as
measurement.create on others.

runDetached starts such work so that its action is dropped for the event
it was detached from; meta, warnings and the auth it reads stay as they
were, and an event the work opens for itself keeps its own action.
fireAndForget now takes a function and starts it detached, and every
trackBackgroundTask handle on a request path is built with runDetached,
which a structural guard holds.
2026-10-23 16:32:10 +02:00
Marc-André Bombeck cfa5fe537b Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-server 2026-10-23 16:15:37 +02:00
Marc-André Bombeck 419d47c862 test(e2e): hand the shared account back on the default vault layout
The layout journey left the shared account on the stacked arrangement,
and the month-grid tests that read the default flowing one failed
whenever they ran after it.
2026-10-23 16:15:36 +02:00
Marc-André Bombeck 453bf6c6f3 test(guards): let the file-size floor follow the ratchet down
The splits on this line took the files over 800 lines from 115 to 96,
under the liveness floor of 100. The floor guards the matcher, so it
moves to 80, below the real count.
2026-10-23 15:54:10 +02:00
Marc-André Bombeck 18ff14ba1a fix(openapi): declare the body-cap 413 and the outbound-screen 502 of a briefing generation
Both have always been answered; neither was declared. The parity walk now
checks them against the contract instead of skipping it.
2026-10-23 15:53:59 +02:00
Marc-André Bombeck 31048e5e63 test: tighten the size and version-stamp baselines after the insights moves 2026-10-23 15:45:16 +02:00
Marc-André Bombeck af15be4708 Merge branch 'feat/v146-server-insights' into feat/v146-server 2026-10-23 15:44:47 +02:00
Marc-André Bombeck 1a5d79f24e refactor(insights): move the briefing read and generation out of their route 2026-10-23 15:44:04 +02:00
Marc-André Bombeck c22c302ebe test(analytics): read the default body where it now lives 2026-10-23 15:41:54 +02:00
Marc-André Bombeck db623ef8bc test(insights): pin the briefing read and generation before moving them 2026-10-23 15:31:36 +02:00
Marc-André Bombeck 356d947ab0 refactor(dashboard): move the summary builder out of its route 2026-10-23 15:25:48 +02:00
Marc-André Bombeck 98875debf4 refactor(insights): move the comprehensive overview builder out of its route 2026-10-23 15:25:20 +02:00
Marc-André Bombeck 1b32217844 test(dashboard): pin the summary before moving its builder 2026-10-23 15:20:27 +02:00
Marc-André Bombeck af5cf1ace2 refactor(auth): move the SSO callback's account resolution and sign-in endings out of its route 2026-10-23 15:17:27 +02:00
Marc-André Bombeck d76497ec13 test(auth): pin the SSO callback before moving its sign-in resolution 2026-10-23 15:13:25 +02:00
Marc-André Bombeck 91a5464aa3 refactor(documents): move the vault upload reading and the list read out of their route 2026-10-23 15:10:26 +02:00
Marc-André Bombeck 2e13b7754e test(insights): pin the comprehensive overview before moving its builder 2026-10-23 15:05:10 +02:00
Marc-André Bombeck 69527d926d test(documents): pin the vault upload and list before moving them 2026-10-23 15:04:39 +02:00
Marc-André Bombeck b75219d704 fix(openapi): declare the duplicate flags and the missing-link 404 of a document upload
A duplicate upload has always answered meta.duplicate (and meta.deleted for
a deleted source key) beside the row, as the description says, but the
envelope schema declared neither. An episode or visit id that is not the
caller's has always answered 404, which no response declared.
2026-10-23 15:04:37 +02:00
Marc-André Bombeck de96891ff5 refactor(import): move the CSV import writer out of its route 2026-10-23 15:00:35 +02:00
Marc-André Bombeck 94ad53756e test(import): pin the CSV import before moving its writer 2026-10-23 14:58:14 +02:00
Marc-André Bombeck 5d9a9107fa test(import): record the status re-warm tail in the JSON import walk
Run for real it is detached from the request but inside its context, so
its annotate lands on the request's wide event on some runs and misses it
on others.
2026-10-23 14:58:14 +02:00
Marc-André Bombeck 7f06e28c37 test(kit): let callRoute send a raw text body 2026-10-23 14:58:13 +02:00
Marc-André Bombeck c6238cbe14 test: tighten the size and version-stamp baselines after the medication moves 2026-10-23 14:52:43 +02:00
Marc-André Bombeck 40eb1255b7 Merge branch 'feat/v146-server-meds2' into feat/v146-server
# Conflicts:
#	src/__tests__/external-id-stability-guard.test.ts
2026-10-23 14:52:03 +02:00
Marc-André Bombeck 11a7124d12 refactor(import): move the JSON import writers out of their route 2026-10-23 14:51:51 +02:00
Marc-André Bombeck a1ab7136f5 test(medications): reach the actions of moved handler bodies in the activity inventory
The inventory reads a delegated route and its direct imports. With the medication handlers moved into lib/medications, the lifecycle reconciliation a dose write files was two levels from its route and dropped out of the walk. A route's handler-body modules now count at the route's own level, listed in one map whose every entry must be a direct import of its route.
2026-10-23 14:49:42 +02:00
Marc-André Bombeck ed12625ab6 test(import): pin the JSON import before moving its writers 2026-10-23 14:48:51 +02:00