Commit Graph
1699 Commits
Author SHA1 Message Date
Marc-André Bombeck bed634fd67 Put the new workout blocks on the page's reading edge
SubPageShell insets its own heading to the card body edge rather than the
container edge, so a section heading or a meta line rendered flush left sat
16 px to the left of everything above it. The day-context heading, the sport
chips and the count line now take the same responsive inset.
2026-07-30 23:23:37 +02:00
Marc-André Bombeck 52cccb3835 Move the workout zone range label onto the type scale
The bpm range under each effort zone sat at an arbitrary text-[10px]. It is
a micro-label on a card, not a chart axis, so it takes the text-2xs token
the standards mint for exactly this.
2026-07-30 23:23:37 +02:00
Marc-André Bombeck a61cdae437 Offer the previous session of the same sport from the workout page
"How does this compare to last time?" is the most likely next question
after reading a workout, and answering it meant going back to the list and
scanning for the one before.

Resolve the previous session server-side, through the same canonical picker
the list uses, so the link can never land on a duplicate row recorded by a
second watch, and render it beside the own-history average line. Null when
nothing older exists, and then no link is shown.

The stats card's hand-rolled heading moves to the icon-less TileHeader
while the file is open.
2026-07-30 23:23:37 +02:00
Marc-André Bombeck 76b407b6de Render the day around a workout instead of linking away from it
The "That day" card took the workout, dropped it, and offered three chips
to Pulse, Sleep and Mood. None of those pages accepts a date, so every chip
landed on an all-time trend surface with the day nowhere in sight.

Replace it with the day itself: the intraday pulse chart pinned to the
session's day, the night the person woke from into it, and that day's mood
entries. All three reads are addressed by the server-resolved dayKey, so
nothing here decides which day a workout belongs to.

The two outbound links that remain are undated and say what they lead to,
Sleep trends and Mood history. Sleep and the intraday chart mount only when
their module is on, so a switched-off module cannot paint as a failed read,
and a query that has not answered yet shows a skeleton rather than claiming
the night was empty.

The detail page also drops the description it borrowed from the list.
2026-07-30 23:23:36 +02:00
Marc-André Bombeck a4cffbbea5 Keep the pillar reference formatter module-private
Only `pillarDetailLines` composes it, and the card reads the lines
rather than the individual band string.
2026-07-30 23:20:02 +02:00
Marc-André Bombeck eb55c235c6 Rebuild the Insights health score as one pinned card
The score had two mounts: a compact face in the hero band and a
nine-block stack further down the overview. The stack also rode inside
the daily-briefing branch, so an instance with the briefing switched off
lost the whole breakdown.

Fold both into one progressive-disclosure card pinned between the
greeting and the customizable region. Collapsed it reads as the score
face: the ring, the band sentence, the weekly delta or the honest reason
there is none, Rest Mode, the tension and return-to-baseline lines, and
the coverage meter. Opening it adds a three-across pillar grid, the
named list of pillars that are not scored yet, the personal weight goal,
and the method footer with its cited standard.

The card is deliberately not a section in the insights layout registry:
an id a saved layout has never seen merges in as default-invisible,
which would have deleted the score for every account that has ever
customised the page.

A pillar the server gated stays visible by name with its reason instead
of being drawn as a zero, safety signposting leads that list, and a
pillar whose read failed gets an error row with a retry rather than
silence. The hero band goes back to the full-width greeting, the shape
it already had for an account without a score.
2026-07-30 23:20:02 +02:00
Marc-André Bombeck a1c5d78f72 fix(nutrients): stop the /insights/nutrients overview swallowing a failed read as absence
The page's overview query shares queryKeys.nutrientIntake(30) with
MicronutrientsCard, which already renders its own QueryErrorCard on a
failed read. But the page's empty-state check ran first and treated
undefined data (the failed-read state) the same as a genuinely empty
window, returning "no nutrient data yet" and short-circuiting before
any of the three cards - including the already-correct one - ever
mounted. A self-hoster with HealthKit permissions granted and a read
that actually failed saw the same blank invitation as someone who
never logged anything.

Add the isError branch above the empty-state check so a failed read
surfaces its own QueryErrorCard with a retry, and cover it with a test
that renders the actual page (not just the card in isolation) so a
regression here can't hide behind a passing unit test again.
2026-07-30 23:11:36 +02:00
Marc-André Bombeck d7badf1512 fix(coach): keep the composer one row tall until someone types
On a phone the composer stacked its controls under the textarea, so an EMPTY
input was two rows tall: a text line, a 44 px tap-target row, and padding. That
is roughly a tenth of a phone screen spent before anyone has typed anything, and
it is the first thing on the screen when the Coach opens.

The auto-grow was never the problem. The textarea already starts at one line and
grows to six; it was the layout around it that reserved the second row up front.

The stacking existed to protect keyboard focus order. That argument does not
hold: focus follows DOM order, and the DOM order is textarea, actions, Send in
both layouts. Nothing jumps. `items-end` keeps the controls beside the last line
as the textarea grows, so the send button stays where the thumb left it.

One component serves both surfaces — the conversation view and the hero, which
re-parents the same live composer — so this is one fix rather than two.

The test that asserted the stacked layout now asserts the row, with the reason
written down, and it fails when the stack comes back.
2026-07-30 15:59:36 +02:00
Marc-André Bombeck 8a2bb70dbd test(ui): gate the two card-header classes that have to agree
The header fix needs two declarations to hold together: the second grid row is
conditional on a description, and CardAction spans the full row set. The second
is the trap. `row-span-2` against a single explicit row mints an implicit second
row and brings the phantom gap straight back for every header that carries an
action but no description.

Reverting it broke nothing. Nothing in the suite asserts a `row-span` class, so
the regression is DOM geometry the unit tests cannot see, and it would have come
back silently at the next tidy-up. This is a literal-class assertion in the shape
`assessment-spine-order.test.ts` already uses, because the question has an exact
answer.

Also finishes the compact-card tier while it is open: three cards on the metric
surfaces still carried `gap-1.5` where the assessment cards carry `gap-2`. One
tier, one value, no `gap-1.5 py-3` left in the tree.
2026-07-30 15:52:21 +02:00
Marc-André Bombeck 14d0eeeb46 fix(ui): stop CardHeader drawing a phantom gap without a description
grid-rows-[auto_auto] was unconditional, so a header with a title and
no CardDescription still reserved an empty second row track and drew
the gap-2 for it — 32px instead of the intended 24px between heading
and body on desktop. Make the second row conditional on a description
being present, and widen CardAction to row-span-full so it still
tracks the header's row count when an action lands in a
description-less card.

Also correct two assessment cards that had drifted onto gap-1.5
instead of the sanctioned gap-2 compact tier.
2026-07-30 15:50:14 +02:00
Marc-André Bombeck a4bb2adcb2 fix(e2e): select the measured wrapper by data-slot, not by birth order
The navigation-geometry spec failed on a merge commit whose tree was
byte-identical to the branch head that had passed minutes earlier, so nothing had
regressed. The spec resolved the element it measures with
`main.firstElementChild`, independently in each of its two `page.evaluate` calls.
Any node mounting ahead of the wrapper between the two reads made it measure two
different elements, which is why the failure was 64 against 131.92 rather than a
two-pixel drift. A timing problem gives a small difference or a null; this was a
substitution.

The shell's content wrapper now carries a stable `data-slot`, both reads select
it by that, and the wait gates on the element rather than on scroll state alone.
The same positional pattern in the overscroll spec is fixed with it, so the class
is gone from the measurement paths rather than from one spec.

Re-running the job made it green, which is a workaround. This is the fix.
2026-07-30 14:35:08 +02:00
Marc-André Bombeck a8b61abf6f feat(cycle): drop the standing disclaimer from the cycle-tracking settings card
The card carried a four-sentence non-medical notice at the top of the cycle
on-ramp. It goes, at the maintainer's decision.

The goal-specific caveat on the prediction surface is untouched: a profile set to
avoid pregnancy still gets the stronger "not a contraceptive method" copy, which
is chosen server-side by goal and shared with the native client, so nothing about
that path changes here.

Removes the key from all six locales and the now-unused icon import.
2026-07-30 14:30:18 +02:00
Marc-André Bombeck 1f87e63662 fix(insights): restore the shared card rhythm the two assessment cards lost
The spacing pass on this branch tightened `insight-status-card` to `gap-1.5`
and left `mood-insights-sections` at `gap-2`, so two sibling cards that are
meant to read as one family stopped matching. `assessment-spine-order.test.ts`
exists to hold exactly that contract and went red as soon as the formatter
stopped masking it: `format:check` failed first in the job, so the unit tests
never ran and the divergence shipped unnoticed onto the branch.

Bring the mood card to the same value rather than widen the other one back.
The direction of travel is the tighter rhythm, and both allowed steps are on
the sanctioned spacing scale.

The test's expected string moves with the contract, not around it: it still
asserts that both cards carry the SAME value, which is the whole point of it.
2026-07-30 10:49:06 +02:00
Marc-André Bombeck bc94223423 style(insights): run prettier on the metric stat strip
The formatter joins one JSX line that was left wrapped. No behaviour change.
It is the only thing standing between this branch and a green gate: typecheck,
tests, e2e, integration and both image builds already pass, and `format:check`
fails on this file alone.
2026-07-30 10:33:24 +02:00
Marc-André Bombeck 90b1a4eaee fix(insights): improve narrative contrast and spacing 2026-07-29 21:28:29 +02:00
Marc-André Bombeck ab450bbfd1 fix(dashboard): keep hero headline when score lead is removed 2026-07-29 21:20:16 +02:00
Marc-André Bombeck bdca4f5724 fix(dashboard): compact score-only hero 2026-07-29 17:05:53 +02:00
Marc-André Bombeck 271f0ed28a fix(medications): make import replay results honest 2026-07-29 16:11:45 +02:00
Marc-André Bombeck b0a94b3423 fix(web): stabilize document and example downloads 2026-07-29 15:57:20 +02:00
Marc-André Bombeck 9354c649fe fix(integrations): surface Google sync progress and canonical workouts 2026-07-29 15:55:37 +02:00
Marc-André Bombeck 67eeefd584 fix(i18n): translate final security and import states 2026-07-29 15:07:35 +02:00
Marc-André Bombeck ba6d06be3b feat(01-21): expose Google sync terminal status
- return redacted per-resource outcomes from manual sync
- poll owner-scoped progress and invalidate committed workout views
2026-07-29 15:01:13 +02:00
Marc-André Bombeck f6555d471b fix(workouts): show aggregate pulse in lists 2026-07-29 14:58:25 +02:00
Marc-André Bombeck 89fefce7ac test(workouts): separate pulse aggregates from curves 2026-07-29 14:57:42 +02:00
Marc-André Bombeck 4b64556b38 feat(01-19): expose privacy-safe ECG import outcomes
- return and render bounded ECG result counters with partial-failure wording
- document archive limits, encrypted storage, and exact WHOOP constraints
2026-07-29 14:47:22 +02:00
Marc-André Bombeck 0bf3a3573d fix(01-20): render explicit assessment states
- prioritize safe fallback text over provider metadata
- keep terminal and pending card geometry stable
2026-07-29 14:40:55 +02:00
Marc-André Bombeck b8260474ed fix(01-17): remove duplicate mobile insights spacer
- suppress only the inherited mobile pre-pill top padding
- preserve safe-area clearance focus paint and 44px targets
2026-07-29 14:38:19 +02:00
Marc-André Bombeck 23c738efac fix(01-17): align and pin desktop sidebars
- place both sidebars on the heading row with one main scroll owner
- bound short-viewport sidebar content without horizontal layout drift
2026-07-29 14:36:18 +02:00
Marc-André Bombeck 7396119ec8 feat(01-17): group gated admin navigation
- cluster allowed destinations under semantic settings-style headings
- preserve fail-closed role gating and mobile target order
2026-07-29 14:29:51 +02:00
Marc-André Bombeck 431120d55e fix(01-16): unify records action grammar
- use Save for new and existing lifestyle facts
- show first-entry or populated-list add actions without duplication
2026-07-29 14:29:19 +02:00
Marc-André Bombeck c4ec2d552f fix(01-16): normalize assessment prose presentation
- use the readable foreground at confirmed medication and Mood mounts
- tighten Health Score and medication assessment spacing
2026-07-29 14:28:47 +02:00
Marc-André Bombeck d67b899138 fix(01-16): remove redundant score and summary presentation
- keep useful Today prose without repeating the ring score
- remove stored-summary regeneration and Health Score notice/method/list copy
2026-07-29 14:27:45 +02:00
Marc-André Bombeck 65ca1aae42 test(01-10): add red responsive navigation contracts
- pin grouped and heading-aligned Settings and Admin sidebars
- add mobile Insights geometry and deterministic release evidence captures
2026-07-29 14:22:54 +02:00
Marc-André Bombeck 6f84629482 test(01-13): add red Google sync terminal contracts
- cover authenticated status polling and bounded resource outcomes
- prove committed workouts invalidate server and client cache surfaces
2026-07-29 14:22:49 +02:00
Marc-André Bombeck 18d0facdfe test(01-10): add red assessment and records contracts
- pin honest assessment fallback, exhaustion, foreground, and state envelopes
- require consistent save and empty-state add action grammar
2026-07-29 14:18:35 +02:00
Marc-André Bombeck 8e944dbcd3 test(01-10): add red dashboard and score presentation contracts
- preserve useful Today narrative without duplicate score prose
- remove stored-document regeneration from the detail surface
- retain score identity while rejecting redundant method copy
2026-07-29 14:12:58 +02:00
Marc-André Bombeck 27a3e08436 fix(01-07): make private Nightscout access operator-owned
- remove the user-facing private-host bypass
- render a redacted operator-action state for denied legacy connections
2026-07-29 13:57:48 +02:00
Marc-André Bombeck 7874eea04d feat(01-08): require reauthentication before passkey enrollment
- offer password, TOTP, passkey, and security-key proof choices
- start registration only after the selected existing factor succeeds
2026-07-29 13:53:59 +02:00
Marc-André Bombeck 3f4afb26a4 test(01-03): pin passkey enrollment reauthentication UX
- require password, passkey, TOTP and WebAuthn proof choices
- order fresh-factor completion before challenge creation
- keep successful enrollment in one cookie-bound ceremony
2026-07-29 13:39:55 +02:00
Marc-André Bombeck 5569b8259b test(01-04): pin Nightscout operator-action UI
- Remove the user-facing private-host bypass contract
- Require redacted operator approval guidance for legacy failures
- Preserve usable public and approved-private connected states
2026-07-29 13:39:50 +02:00
Marc-André Bombeck 668e3ca6b6 fix(insights,settings): restore hero-strip column width, gate hero fieldset on hydration 2026-07-28 22:03:39 +02:00
Marc-André Bombeck 7b8fb98e48 feat(analytics): replace health score with reference composite 2026-07-28 21:12:05 +02:00
Marc-André Bombeck 3aff2c16db fix(insights): scope generation guard to prompt fields, clear cache on restore 2026-07-28 21:11:43 +02:00
Marc-André Bombeck c91558f18f feat(analytics): replace health score with reference composite 2026-07-28 21:10:15 +02:00
Marc-André Bombeck 666a85d311 feat(insights): persist correlation patterns 2026-07-28 14:01:11 +02:00
Marc-André Bombeck 8ece6f39cc fix(status): expose background failures 2026-07-28 14:00:44 +02:00
Marc-André Bombeck a689329e79 feat(dashboard): configure Today highlights 2026-07-28 13:22:08 +02:00
Marc-André Bombeck 51dbfefbcb fix(backup): carry the intraday day shape through export and restore
`IntradayCumulativeProfile` shipped classified as backed up and carried by
nothing. The drain folds the per-sample rows into twenty-four running totals
and deletes the originals in the same transaction, so past the grace window
the table is the only copy of a day's curve: a restore rebuilt the account
without it, reported success, and left the same-time baseline reading "still
learning" for a fortnight.

Builder and restore sit together in one file, the way the profile and cycle
pairs do, because the restore route delegates and a grep of the route alone
lies. The section spreads into the payload rather than being copied key by
key. Two curves for the same metric and day, or a curve that is not a whole
day, throw naming the day instead of writing a row every reader drops.

The pair is proven end to end rather than at each end: the round-trip suite
builds a real payload from a populated account, serialises it, parses it back
through the real parser, drives the real restore, and asserts on the rows
written. Remove the read and it goes red.
2026-07-28 00:07:10 +02:00
Marc-André Bombeck 2aeb925e28 merge: s15-stepsdisplay into release/v1.34.0 2026-07-27 23:41:34 +02:00
Marc-André Bombeck b14943ea1b merge: s8-baselines into release/v1.34.0 2026-07-27 23:41:34 +02:00