MedicationIntakeQuickAdd's useQuery shared the queryKey with the
onboarding-checklist subscriber but ran with default staleTime=0, so
every sheet open triggered a fresh `/api/medications` GET even when
the parent dashboard cache already held current data. Match the
dashboard's 60s staleTime and disable refetch-on-mount /
refetch-on-window-focus so opening the sheet inside the TTL reads
straight from the React-Query cache.
P3-2: pair the Arztbericht hero CTA's explicit h-11 with a min-h-11
floor (and sm:min-h-9 on desktop) so a future className override can
not accidentally re-lift the mobile minimum below the touch-target
contract.
P3-3: drop the dashboard onboarding checklist's sm:min-h-10 (a no-op
against the Button default of h-10) down to sm:min-h-9 to match the
Hinzufuegen menu trigger pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add CircleCheck / AlertCircle / AlertTriangle icons next to the
take-now / overdue / very-overdue pill on both the generic medication
card and the GLP-1 card so colour-blind users can disambiguate the
three states without relying on the red-green hue. WCAG 1.4.1 (Use of
Color). Symmetry kept across both surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the aria-live=polite the loading skeleton already carries onto
the success card so screen readers announce the assessment when it
swaps into the same slot the skeleton occupied. Closes the load to
loaded transition signal flagged in W10 P2-2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user has no active medications, render the Medikament-anlegen
link in the sheet footer beside Cancel rather than as an in-body
button. This balances the empty branch with the populated form (whose
Save button also lives in the footer) and keeps the footer-slot
primary-action promise across both states.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop pr-2.5 + [&_svg:last-child]:mr-1 down to a flat pr-2 so the
chevron parks at the same 8px gutter that Safari and Chromium legacy
themes paint for the native date-input glyph. Chromium Material still
reads aligned (16px button + 8px gutter = 24px). Removes the ~4px
parity miss flagged in the W10 select-vs-date-input pairing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thread aria-controls from the cumulative-type expand/collapse chevron
to the per-day drill-down container so screen readers can jump from
the disclosure button to the disclosed content. Uses dayKey-derived
stable ids on both desktop (TableRow) and mobile (wrapper div)
layouts; falls back to row id when ungrouped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dose field on the medication-intake quick-add sheet was rendered
editable but the POST body has no `dose` slot — any user edit was
silently dropped, so a user typing "5 mg" for a half-pill day saw the
catalogue dose recorded instead. The previous hint copy did not warn
about the constraint, leaving the user's mental model out of sync
with the persisted data.
Render the field read-only with a muted background, drop the tab
order on the input, and rewrite the hint copy across all six locales
so the user knows that variable doses are coming in a later version
and the standard dose is recorded for now. The setDoseOverride state
holder stays so the reset-on-medication-switch keeps working when the
v1.4.38 doseOverride schema lands.
W4a item 2 promoted DropdownMenuItem to whitespace-nowrap globally and
bumped the sidebar user menu to w-60 so the German "Benachrichtigungs-
Center" label stayed on a single line. The top-bar user menu kept its
w-48 (192 px) width — the same label plus a 16 px icon + 8 px gap + 16
px padding measures ~220 px and overflowed under the global nowrap +
overflow-x-hidden, clipping without an ellipsis.
Bump the top-bar user menu to w-60 so it matches the sidebar's symmetry.
W-CI raised the onboarding checklist toggle and the intake-history
pager to the 44 px mobile touch floor (WCAG 2.5.5). The new
medication-intake quick-add sheet sibling shipped with an empty-state
CTA at h-8 (size="sm" default) and footer Save/Cancel buttons at h-10,
both below the project-wide floor and visibly inconsistent with the
sibling MoodForm / MeasurementForm + the rest of the v1.4.37 surfaces.
Apply the established min-h-11 sm:min-h-9 override to the empty-state
CTA, the footer Save/Cancel, and the empty-state Close button. Extend
the SSR contract test to count three min-h-11 floors instead of two so
a future regression that drops one of the buttons back below 44 px
trips before the release.
The v1.4.34.5 tap-target sweep covered the dashboard tile and
per-row chevron but missed the two CTAs the
`<GettingStartedChecklist>` header renders on the onboarded-user
dashboard: the expand/collapse toggle (32 px tall) and the
dismiss-all CTA (40 px via shadcn's default Button, which has not
yet learned the `h-11 sm:h-10` mobile lift the input primitives
follow). Both flagged in the `mobile-viewport` touch-target probe.
Pin `min-h-11 sm:min-h-10` on each so the desktop rhythm is
unchanged.
While here, two e2e specs needed updates to match the same wave:
- `mobile-viewport.spec.ts` enforced `width < 44 || height < 44`,
which flagged the chart-range pill row (`7T` / `30T` / `90T`,
each 44 px tall but 30-40 px wide) as a WCAG violation even
though 2.5.5 honours adjacent-target spacing for horizontal
groups. Drop the width branch; the height floor is the hard
contract every solo CTA must clear.
- `onboarding-flicker.spec.ts` route-stubbed `**/api/analytics`,
which is exact-match against the path — the IW1 slim-slice
variant `/api/analytics?slice=summaries` (which the checklist
hook now uses) bypassed the stub and hit the real route, so the
onboarded-user fixture flipped to `measurementCount = 0` and
the card rendered. Swap both stubs to a regex that matches the
base path AND any query-string variant.
Two findings from the post-implementation self-review:
- Empty-state CTA: swap the plain anchor for next/link so the
/medications jump stays inside the SPA router instead of triggering
a full page reload.
- JSDoc accuracy: the original module-level comment claimed a
non-default dose 'surfaces as a sonner toast hint' — neither the
toast nor a notes-style append actually exists. Rewrite the dose
paragraph to honestly describe the contract: the dose input is
pre-filled for visual confirmation, editable, but the POST body
never carries the dose override. The intake event always
references the medication's catalogue dose; a half/double dose
still routes through the medication edit on /medications.
Add the SSR-level suite for the new quick-add: empty-state markup, the
populated form's field testids plus 44 px touch floors, the pure
default-selection heuristic across the four ranked branches
(empty / single-active / due-now / alphabetical fallback), and the
no-raw-key locale check for English and German.
Extend the existing quick-add-labels guard so it covers the third
DropdownMenu row alongside the original two — the icon is decorative
and the trigger label sits above the menu, so the three visible
labels must stay distinct from each other and from the trigger.
Mirror the ResponsiveSheet primitive used by the Measurement and Mood
quick-adds for medication intake. The sheet auto-selects the active
medication whose schedule window is currently open (or the only active
medication when the user has just one), pre-fills the dose from the
catalogue entry, and defaults the time taken to now. On submit the
form invalidates medicationDependentKeys plus the inline per-medication
compliance chart cache so the detail tile, the dashboard compliance
chart, the today list, and the achievements card all refresh in lockstep
with the API. An empty-state branch suppresses the form and surfaces a
CTA to /medications when the user hasn't yet created a medication.
Self-review polish on top of the v1.4.37 W7c step-consolidation
landing:
- The collapsed daily view returns up to 5000 synthesised rows in
one shot, so the legacy 25-row pagination would have re-fired the
same scan with offset=0 returning the same slice. Disable
pagination on the day-grouped path; per-sample lists keep their
PAGE_SIZE=25 behaviour.
- Bump the per-page row scan from 1250 to the server max of 5000 so
even chatty pre-drain Apple-Watch accounts (~200 chunks/day on
busy days) get a useful number of daily rows on the first paint.
Post-drain the scan is trivially short (each day reduces to one
stats: row).
- Reset offset=0 explicitly on the grouped path so a sort-direction
flip never paginates into an empty server slice.
- Clarify the day-boundary comment in the dayKey drill-down branch
— the ±12h around canonical noon shift works for every IANA
offset, including sub-half-hour zones (Asia/Kathmandu +5:45,
Pacific/Chatham +12:45).
Side-by-side contract suite for the v1.4.37 W4b symmetry fixes from
Marc's UX audit (item 11). Asserts the load-bearing structural seams
both medication-card variants must share:
* History + Pencil header buttons render on both cards.
* No kebab on either card when no overflow prop is wired (Mounjaro
matches Ramipril in the common case).
* Kebab only mounts on Mounjaro when `onLogSideEffect` is supplied;
no equivalent surface on Ramipril.
* Category-label badge uses the shared category-map lookup on both
cards (Mounjaro no longer hard-codes the treatment-class label).
* Take-now coloured pill renders on both cards when inside the
current schedule window.
* Primary-actions row carries the canonical two-button shape — the
GLP-1-specific Stethoscope no longer leaks into this row.
* Purple `text-purple-400` accent on the upcoming schedule dose
surfaces uniformly across both card kinds.
Four new test files cover the v1.4.37 W7c surface:
* group-by-day.test.ts (route) — collapses per-sample step rows into
one row per user-TZ day with sampleCount; gates on
CUMULATIVE_HK_TYPES so non-cumulative types keep the legacy
findMany behaviour; resolves the dayKey drill-down window via
measuredAt: { gte, lt }; rejects malformed dayKey at the
validation layer.
* measurement-list-step-grouping.test.tsx (UI) — renders the
collapsed daily total with the localised sample-count caption;
exposes a chevron-expand button with aria-expanded; suppresses
the edit/delete actions on the synthesised daily row.
* drain-per-sample-cumulative.test.ts (helper) — passes a
measuredAt cutoff into the per-sample scan when cutoffHours is
set; falls back to the all-rows behaviour for CLI/admin callers
that omit the option.
* drain-cumulative-queue.test.ts (scheduler) — pins the queue name,
cron cadence (03:45 Europe/Berlin), 36 h cutoff constant, and
pg-boss handler wiring.
Self-review polish on top of the W2 fast-path landings:
- Rename the misleading `ninetyFiveDaysAgo` local in
bp-in-target-fast-path to `readSince` so the 396-day BP rollup
read window does not read like a 95-day shortcut.
- Document the rollup-bucket-vs-display-tz day-key shift in
correlations-fast-path. The DAY bucket anchors at UTC midnight; for
the Berlin-centric production tenant that lands on the same local
calendar day, so the rollup-fast-path's per-day pair shape matches
the live path within the n >= 20 surface gate. A v1.5 follow-up can
mint per-user-tz buckets if a non-Berlin account needs strict
parity.
Marc directive: when the operator turns the global Coach flag off,
every Coach affordance on every surface must vanish — no grey-out,
no error, no inert button, no DOM trace. The recent v1.4.37 W5
hotfixes closed three holes the UX audit caught (HeroStrip action
button, SuggestedPrompts chip strip, /targets CoachDrawer mount); a
follow-up audit item asked for an automated invariant so the gate
contract can't regress.
Walk every Coach-bearing web surface (HeroStrip action row,
SuggestedPrompts chips, CoachLaunchButton inline pill, LayoutCoachFab
mobile sticky, LayoutCoachMount drawer subtree) under a QueryClient
pre-seeded with `coach: false` and assert each one paints nothing
measurable in the SSR output. The fixture maintains a known surface
list; a trailing count check forces contributors to revisit the
fixture when a new Coach-bearing component lands.
The generic medication card surfaces the upcoming schedule dose with
a `font-medium text-purple-400` accent so titration overrides on the
schedule visually pop out of the muted next-intake line. The GLP-1
card omitted the accent entirely.
Mirror the same accent on the GLP-1 card so the only difference
between Mounjaro and Ramipril detail surfaces is the data being
shown, not the styling treatment. The schedule.dose still rides
inside the existing next-injection paragraph (hidden below sm: like
the generic card so the narrow-viewport row stays one line).
When the type filter is a cumulative HK type (steps, active energy,
distance, flights, daylight) the measurements list now opts into the
route's groupBy=day mode and renders one row per user-TZ day with
the day's total + a sampleCount caption. A chevron in the row
action column lazy-fetches the day's per-sample chunks via a second
query against ?dayKey=YYYY-MM-DD and inlines them beneath the row
so power users can still answer wann wurden Schritte gemeldet
without scrolling past 200 step rows on the default view.
Mirrors the universal Apple-Health / Garmin / Withings pattern
captured in the v1.4.37 research brief: one number per day as the
primary surface, drill-down to the chunks behind it.
The generic medication card painted a coloured take-now / overdue /
very-overdue pill at the top of the detail surface; the GLP-1 card
silently omitted it. Marc reported the asymmetry during the v1.4.37
UX audit (audit item 11): Mounjaro and Ramipril cards felt "different"
even though both should read as the same row shape.
Wire the shared `reduceCurrentWindowStatus` reducer into the GLP-1
card, fetch the same reminder thresholds as the generic card, and
re-render once a minute so the pill tracks wall-clock progress without
a route reload. The pill renders only when a schedule is currently
in/past its window and the user has not yet covered every overdue
schedule with today's intake events — same suppression rules as the
generic card.
Add a render assertion that the inner column ships with grid +
flex-1 + grid-rows-[auto_auto_auto_auto_auto_1fr_auto] so a future
refactor can't silently revert to a flex stack and re-introduce
the v1.4.36 "Karte hört bei der Trennlinie auf" report. The
mt-auto + h-full + flex-col + flex assertions stay in place
alongside the new grid contract — they describe complementary
load-bearing classes.
Mock useFeatureFlags so the suite can drive both the flag-on (CTA
mounts) and flag-off (CTA hides) branches alongside the existing
aiEnabled gate test. Both branches share the all-on default unless
the case overrides it with mockReturnValueOnce, so the rest of the
suite continues to render with the default-mock flag set.
Every chip in <SuggestedPrompts> is a Coach affordance — clicking one
seeds the Coach drawer's composer with the localised prompt and opens
the drawer. The strip rendered unconditionally until now, so a
deployment with the operator's global Coach flag off still painted
five inert chips below the hero band.
Read flags.coach inside the component and short-circuit when the flag
is off. The HeroStrip caller already wraps the mount in the same
guard (W5 commit prior); this is defence-in-depth so a future caller
that mounts <SuggestedPrompts> outside the hero band can never leak a
Coach surface.
Read useFeatureFlags() inside TargetCard and pair the existing
aiEnabled guard with flags.coach so the per-card Coach CTA hides
entirely when the operator disables the Coach feature globally.
Pre-fix the CTA mounted whenever the user-level provider chain was
configured, even when the operator-level kill-switch was off; the
disable cascade is supposed to make every Coach surface disappear,
not just the hero-strip button. Sister sites (hero-strip,
suggested-prompts, /targets CoachDrawer) belong to W5.
Adds an SSR-only contract suite for `<ArztberichtHeroCard>` covering
the hero slot, value statement, CTA touch-target floor, the
`aria-describedby` link from the CTA to the value statement, the i18n
key resolution path (DE + EN), the hero-gradient + glow-purple visual
treatment, and the closed-state pass that keeps the dialog markup out
of the SSR output until the CTA opens it.
Updates the existing `<ExportSection>` smoke suite for the v1.4.37 W7a
shape: four secondary cards in the grid (not five), the hero mounted
above the grid, and the "Other export options" sub-heading.
The doctor-report PDF is HealthLog's flagship "data out" path. The
v1.4.16 phase B7 layout treated it as one of five equal cards in the
grid; this change lifts it into a hero card at the top of
`/settings/export`. The remaining four export cards (Measurements CSV,
Medications CSV, Mood CSV, Full JSON backup) move under a dedicated
"Weitere Export-Optionen" / "Other export options" sub-heading.
The hero reuses the `hero-gradient + glow-purple` visual treatment from
the Insights hero strip, so the surface feels consistent across the
app. The CTA routes through the existing `<DoctorReportDialog>` +
`/api/doctor-report` flow — only the framing changed. Mobile-first: at
< sm the CTA falls onto its own row and clears the 44 px WCAG touch
floor (`h-11 sm:h-10`); the button carries an `aria-describedby` link
to the value statement so screen readers announce the framing
alongside the action label.
The generic medication card mapped `medication.category` to one of the
localised category labels via an inline lookup table, while the GLP-1
card hard-coded `medications.treatmentClassGlp1` into the same slot.
The asymmetry made Mounjaro and Ramipril cards visually diverge on the
medications list even though both should read the same row shape.
Lift the lookup into `src/lib/medications/category-label.ts` and use it
from both card variants. The GLP-1 nature of a medication is already
implied by the rotation hint + injection-metadata rows below the
header, so the category badge now carries the real classification
(BLOOD_PRESSURE, HORMONE, OTHER, ...) on both cards.
Tests pinned the old contract; they now assert the new shared label
shape for both English and German locales.
Collapse the Card's inter-row gap to 0 and drop the CardHeader
inner gap to 0 so the only space between the title and the chart
strip is the CardHeader's own pb-1 (4 px). HealthChart mini paints
a plain div with mb-1 under its title; Mood's pre-fix gap-1
(4 px) on top of the header pb-1 stacked an extra 4 px below the
title, leaving Mood ~12-16 px taller than the BP/weight siblings
inside the trends row.
Both the generic medication card and the GLP-1 variant need to paint
the same coloured take-now / overdue / very-overdue pill, but the
schedule-window math lived inlined inside medication-card.tsx only.
Move parseTimeToMinutes, toBerlinDate, getWindowStatus,
isLastIntakeInCurrentWindow, and the reducer that picks the
most-actionable schedule into src/lib/medications/window-status.ts so
both cards can consume the same code path.
No behaviour change for the generic card. The extracted reducer
mirrors the previous inline implementation, including the
overnight-window adjustment, the late / very-late thresholds, and the
"don't show in-window if the last intake already covered the window
today" guard. The helper accepts any schedule shape that exposes
windowStart, windowEnd, and daysOfWeek, which both card variants
already satisfy.
Override the Card primitive's default gap-4 md:gap-6 with gap-3
md:gap-4 and drop the CardHeader's pb-3 to pb-0. The default
spacing stacked on top of the CardHeader padding rendered a ~36 px
dead gap between the metric label and the headline number on md+,
so the card visually split into two disconnected blocks. The
tighter rhythm pulls the value back under the label so the card
reads as one cohesive unit.
Swap the inner column from a flex stack to a 7-row grid with a 1fr
slack row on the provenance accordion. The hero strip's row already
stretches the card via items-stretch, the card already declares
h-full, and the disclaimer keeps mt-auto for the no-grid render
path. The remaining work was teaching the inner column to actually
distribute the recovered vertical slack — flex with mt-auto only
on the disclaimer parked the recovered space under the disclaimer
instead of between the body and the footer, so the visible card
ended a divider-line above the left column's bottom edge.
The hero band carries three Coach affordances — the action-row 'Ask the
coach' button, the SuggestedPrompts chip strip, and (via prop drilling)
the HealthScoreCard's onAskCoach handler. All three open the same Coach
drawer that the layout already gates on the operator's global Coach
flag. Until now they painted unconditionally; users on a Coach-disabled
deployment would click the chips or button and the drawer mount would
refuse to render, leaving the controls inert.
Read flags.coach in the hero and short-circuit each Coach surface when
the flag is off. The action row + chip strip are conditionally
rendered; the HSC's onAskCoach prop drills through as undefined so a
future re-addition of an inline HSC Coach button stays gated too.
Add whitespace-nowrap to the DropdownMenuItem primitive so every
dropdown row stays on a single line app-wide, and bump the sidebar
3-dot overflow menu from w-56 to w-60. The Benachrichtigungs-Center
label (23 chars) plus its Bell icon and the default text-sm cap sat
right at the 14-rem ceiling and wrapped at the hyphen on sub-pixel
rounding.
Split the trigger's horizontal padding (pl-3 pr-2.5) and push the
chevron 4 px further right via [&_svg:last-child]:mr-1 so the
trailing glyph sits at the same visual gutter as the browser-native
date-input calendar icon. The old uniform px-3 parked the chevron
12 px from the edge, which read visibly tighter than the date-picker
glyph when both controls shared a form row.
Run prettier --write on the four files the v1.4.37 W3 commits
touched (the schema, the route, the V2 component, and the two test
files). Drive-by also collapses two pre-existing whitespace drifts
in the InventoryItem type aliases and the glp1PostBodySchema refine
call so the file matches the formatter contract end-to-end. No
behavioural change.
Add two SSR fixtures to lock the v1.4.37 W3 fix in place: a mixed
taken+skipped seed asserts the distinct lucide icon + green styling
for taken rows and the muted outline styling for skipped rows, and a
German-locale skipped-only seed proves the table body never leaks the
"Eingenommen" label onto a skipped row. The seed helper threads the
new `status:"completed"` cache-key shard so the component reads its
own seed without firing a network call.
Marc reported that rows with `takenAt:null AND skipped:false` rendered
as "Eingenommen" with an empty leading column on the medication
detail page — the binary `event.skipped ? "Übersprungen" : "Eingenommen"`
mapping never accounted for the "missed / never confirmed" branch the
v1 component used to surface as "verpasst". The list now pins
`?status=completed` so those ambiguous rows stay off the detail-page
table (they remain visible on the calendar / today surfaces), and the
status cell branches explicitly: green secondary chip with a check
icon for taken rows, muted outline chip with a skip-forward icon for
skipped rows. Derived from `!skipped && takenAt` rather than `skipped`
alone so a malformed row cannot sneak past as "Eingenommen" again.
The build-time row read its label from `t("builtAt", { time: "" }).trim()`
with a fallback to the gitSha key. The fallback was effectively dead
(the trim never produced an empty string) and the pattern silently
risked rendering two side-by-side "Build" columns when an
implementer dropped the trailing `{time}` placeholder. Replace with
a dedicated `settings.about.builtAtLabel` key across all six locales.
Extract the newer-version badge into an exported UpdateBadge
component. Renders as a 44 px-square anchor (or role=img span
fallback) with text-primary colour, focus-visible:ring-* on the
anchor branch, and aria-label/title carrying the latest tag so
keyboard + screen-reader users get the same signal sighted users
get from the icon. Drops the hardcoded text-dracula-purple in
favour of the semantic primary token, which already meets WCAG AA
on light + dark.
shadcn Button size="sm" renders at 32 px — below the project's
44 px touch floor adopted in v1.4.34.4. Use the default size with
an explicit min-h-11 sm:min-h-9 override so the pagination row
stays compact on desktop but clears WCAG 2.5.5 on touch.
Sort headers relied on hover:underline only and dropped below the
44 px touch target floor on mobile. Add focus-visible:ring-* and
min-h-11 sm:min-h-9 so keyboard + touch users can hit the column
header reliably.
The carrier chip on the admin login-overview audit table was empty
whenever the offline GeoLite2-ASN MMDB had no entry for the IP. In
practice this is every IP behind a non-DACH or cloud-NAT range — the
free-tier MMDB carries DACH carriers densely but thins out
elsewhere — and the chip went blank even though the city + country
were already populated from the parallel location lookup.
When `entry.carrier` is null but `entry.location` is non-null,
render the chip with the city/country string and a "carrier
unavailable" qualifier. Private/loopback IPs still hide the chip
entirely (both columns null).
The dedicated "Updates" card carried a "Check for updates" button,
a last-checked timestamp, and three result paragraphs covering
up-to-date / newer-available / unknown. All of it surfaced an answer
to one question — "is there a newer release?" — and the answer was
overwhelmingly "no, you're current". The card was three full rem of
chrome for that signal.
The 24 h auto-check still runs on mount. When it returns
`newer_available`, an ArrowUpCircle badge appears next to the
version line; hover/tap exposes the latest tag via the title
attribute. The link points at the release HTML URL when present.
No bouncing, no badge counter, no manual recheck button.
The settings/about route now reads as a tight identity card +
external links list, matching how often the surface actually
gets used.
The About surface (version + license + source links + update check)
used to live as its own Settings section and the user-card dropdown
exposed it from both desktop and mobile. The three cards collectively
get opened once or twice a year, mostly by the operator confirming
the running version against the latest release.
Move the surface to `/admin/about` and drop the dropdown link from
both the desktop sidebar and the mobile top-bar. The public-facing
landing at `/about` (credits + MaxMind attribution) stays untouched
since it's reachable without a session. The legacy `/settings/about`
route still resolves so external bookmarks don't break, but is no
longer surfaced in any nav.
The existing `<AboutSection>` is reused as-is — no markup changes,
the component owns its own heading so the admin renderer mounts it
without the SectionFrame wrapper.
The Insights trends row fires three parallel
GET /api/measurements?type=…&aggregate=daily&limit=5000 requests on
mount (BP_SYS / BP_DIA / WEIGHT). The legacy date_trunc path scanned
the measurements table each time — ~3 s per call on Marc's account.
Add a `source=rollup` opt-in on the listMeasurements schema. When set
alongside `aggregate=daily` the route reads from measurement_rollups
DAY buckets instead, swapping a full-table scan for a small indexed
read against the ~5 k-row rollup table. Cumulative HK metrics get
mean * count to reconstruct the daily SUM the chart expects.
The chart-data client opts in for every windowDays > 7 fetch so the
trends row + every sub-page chart benefit. When the rollup returns
zero buckets for the requested window the route falls through to the
live aggregate so brand-new accounts still render their first chart
correctly.