The Insights overview panned sideways at every phone width. SectionHeading
pinned its trailing action slot with shrink-0, which sizes a flex item to its
max-content width. That slot was designed for a compact control, but every
caller passes prose today — the provenance method caption, the device-score
subtitle, the sleep-quality subtitle. A sentence at max-content measures
1229 px in English and 1464 px in German, and since the row sits inside the
AuthShell <main> — whose overflow-y:auto makes overflow-x compute to auto —
that width turned the page into a horizontal scroller. min-w-0 lifts the flex
min-width:auto floor so the caption wraps to the row instead; the parent's
flex-wrap still moves the slot to its own line before anything is squeezed.
The existing width guards could not see this. Both measured
documentElement.scrollWidth, which never budges when <main> absorbs the
overflow into its own scroll area, and the insights case ran against an empty
mocked payload that fits any viewport by construction.
- Add e2e/utils/horizontal-overflow.ts: asserts the document AND the <main>
scroll container, and names the widest offending element on failure.
- Add e2e/mobile-horizontal-overflow.spec.ts: sweeps /insights, /, /measurements
and /medications at 360/390/393/402/430 px against populated fixtures, plus a
canary proving the measurement can see through <main>, and a check that the
insights fixture really populates the page.
- Rewrite the cycle spec's empty payload as a populated one and sweep every
phone width; its /insights case moves to the new spec.
- Point mobile-density.spec.ts at the shared assertion — it carried the same
document-only blind spot.
The Today digest's dose-window rail item linked to the bare /medications
list even though the overdue medication's id was already resolved
server-side alongside its name — a tap landed the user on a generic list
they then had to scan themselves.
buildMedsTodayBlock now carries nextDueMedicationId; the digest's
"Log dose" action links to /medications?highlight=<id> when the id is
known, falling back to the bare list for an older cached block. The
medications page reads the highlight param, scrolls the matching card
into view, and rings it briefly (mirrors the vault's duplicate-upload
highlight) in both the card grid and the table view. Regenerates
docs/api/openapi.yaml for the new medsToday field.
The digest-driven Today hero is the single promoted hero at the top of
the dashboard, so the old opt-in hero band is redundant — a user who had
opted in and has Insights on saw two heroes. Remove the legacy hero and
its heroVisible layout flag.
- Drop the DashboardHero / DashboardHeroSkeleton mount from the
dashboard page; the header now renders the greeting unconditionally
(its showGreeting gate is gone with the hero that owned it).
- Retire the heroVisible field from the dashboard layout (type,
defaults, resolver, serializer) and the widgets PUT schema. The
resolver is a whitelist constructor, so a stored heroVisible is
dropped on read and never re-persisted — no stored layout breaks.
- Delete the now-orphaned hero components (band, skeleton, ring
carousel, briefing spotlight) and their tests; drop the retired
dashboard.hero copy across all locales.
- Remove the hero-visibility toggle from Settings → Dashboard. The
score-ring picker no longer hangs off that flag and stays available,
since the rings it configures still ride the server-computed
dashboard snapshot.
- Repoint the phone-width density guard at the Today hero and mock the
daily digest so it renders.
- Repoint the preserve-when-absent doc analogy off the removed field.
The dynamic /i18n/[locale] route handler answered with Next's default
Cache-Control (private, max-age=0, must-revalidate) and no validator —
its immutable-cache header was overridden, so a non-PWA browser would
re-download the full catalog on every hard load. Static public/ files
keep an ETag (repeat loads revalidate to a 304) and the service
worker's cache-first rule covers the offline relaunch; the ?v= build
key the layout appends busts both per deploy.
scripts/generate-i18n-boot.mjs writes public/i18n/<locale>.js for all
six locales as a prebuild/predev step (the sw-version.js pattern);
output is gitignored. The route handler is removed.
Measured on a production build, authenticated DE session: dashboard
document 522 KB raw / 139 KB gz -> 170 KB raw / 24.6 KB gz; the DE
catalog moves into a one-time 119 KB gz cached asset.
H2
The correlation-card header's right slot kept its icon-era shrink-0
after v1.22 replaced the info glyph with an inline caption — the
unshrinkable badge+caption row escaped the card's right edge by
14–39 px at 390/360 across all five cards and squeezed the title to a
2-line 36–42 % sliver. The caption now stacks under the title below sm
(only the compact strength badge stays pinned right); on >=sm the
inline layout is unchanged.
Same surface: the discovered-relations stat caption ('{n} gepaarte
Tage · r · q') stacks below its finding sentence on phone widths
instead of wrapping to a 4-line sliver at ~25 % of the row, and the
subsection caveat caption stacks under the subheading instead of
wrapping 9 lines beside it.
The phone-width density guard now sweeps /insights/mood too, with
mocked correlation + discovery payloads pinning the populated worst
case (five cards, three mood pairs) so the guard cannot pass vacuously.
H6
New density guard at 390x844 and 360x780 on the dashboard and
/insights: page scrollWidth never exceeds the viewport, and no
descendant's visible box crosses its card / hero / list-row edge
(clip-aware below the tile; content cut off at the tile's own edge
counts as an escape). The snapshot mock gains hero fixtures — layout
heroVisible, three score rings, and a briefing whose signal rows pair
long wrapping headlines with wide delta strings — so the guard
exercises the exact shape that regressed, and asserts the spotlight
rows actually render rather than passing vacuously.
Move the BP/pulse/body-fat/weight band + target derivation out of the
dashboard client component and into the snapshot DTO. The page recomputed
every band from the user profile on each render via the analytics band
helpers; the snapshot builder now resolves the same numeric structures
once server-side under a new targetBands block, calling the identical
helpers. Emit only the i18n-free numbers — the label-bearing BP target
zones stay client-assembled from the server numbers.
Add a parity test asserting buildTargetBands matches the prior client
derivation for a male, a female, and a no-profile user.
The snapshot's fast phase gains a medsToday block — active count,
today's scheduled/taken/skipped tally after the idempotent projection,
and the earliest server-computed next due across active medications —
and the warm phase derives a compact health score from the coverage
map and graded blood-pressure windows the extras build already holds.
A pure verdict resolver ranks the day's single highlighted item over
nine rungs from an overdue dose down to a quiet-day fallback, with
fixed crisis floors for the blood-pressure rung and timezone-correct
day checks. The layout grows a heroVisible flag riding the stored
widgets JSON, the iOS summary route adopts the shared dose-state
builder with an unchanged wire shape, and the new sub-blocks report
their build time alongside the existing snapshot timings.
The two-o'clock status crons generated for every account, ignoring both
the per-user coach opt-out and the operator kill switch; they now share
one gated discovery that also skips users the pregenerate pass will
rewarm anyway, and the mood card finally gets the nightly run the other
six always had. Deleted measurements no longer feed any prompt. Locales
without an assistant language fall back to English consistently instead
of German texts with English fallbacks. A rate-limited regenerate stops
claiming success, the dashboard briefing state distinguishes a missing
provider from a pending generation and serves a stale briefing with its
timestamp instead of hiding it, narrative warming actually processes
oldest first, and worker failures reach the error tracker through one
shared reporter while the insight queues gain real retries.
Add the data-value features to the insights category pages on top of the
density pass:
- Stat strip: lead each metric page with min / max / median / mean,
read from the already-fetched analytics summaries slice (no new
network). Median is now computed in both summary producers — exact in
the live path, a windowed PERCENTILE_CONT in the rollup-tier slim
slice. The thin HealthKit pages mount it too for parity; no AI
assessment is added there.
- Range band: shade an Apple-Health-style min-max area around the daily
mean line where the data supports it (non-cumulative metrics). The
daily-aggregate read now emits per-day min / max from the rollup
buckets; the band rides the existing target-range toggle.
- All readings: a dedicated per-metric subpage at /insights/values/<type>
reusing the measurement list pinned to one type (pagination + inline
edit/delete). A clear entry button mounts at the foot of each
category page.
- Diversity nudge: a gentle hint when a metric's readings cluster on one
weekday or time of day, computed from a bounded, cached timestamp
window — non-blocking, self-gating.
All new reads stay cache-friendly and off the navigation path.
The v1.7.2 default-ON snapshot flag makes the dashboard read every tile
from GET /api/dashboard/snapshot; the e2e build sets no opt-out var, so
the specs that only mocked the legacy cells hit the real route, got
empty summaries for the zero-measurement seed user, and timed out the
tile/chart assertions.
Add a shared, correctly-typed mock helper and wire it into the five
tile-asserting specs alongside the existing legacy mocks (kept to
protect the reversible path).