The 7-day window on the dashboard chart still pulls raw rows from
`/api/measurements` (the server-side `aggregate=daily` path kicks in
only for windows over a week). The client-side daily aggregator then
reduced every per-day bucket with `sum / count`, which averages step
counts across the per-sample rows Apple Health emits instead of
summing them. Steps on a 7-day view rendered as the per-sample
average (hundreds), not the daily total.
The reduction now branches on `CUMULATIVE_HK_TYPES` (the same set
that drives the server-side SQL aggregator in v1.4.29): cumulative
types reduce with `sum`, spot metrics keep the `sum / count`
average.
Each widget row carried a vertical pair of 44-px arrow buttons —
116-px row, repeated across 15 widgets, ~1 740 px of scroll just
for the reorder list. Three times the height of every neighbour
on the Settings page.
Swap the vertical arrow stack for a horizontal pair on the
trailing edge. `size-11 sm:size-9` preserves the 44-px mobile
tap target and shrinks to 36-px on desktop where pointer accuracy
is high. Row container drops `p-3 gap-3` for `px-3 py-2 gap-2`
with `min-h-12` as the height floor — 48 px, 2.4× tighter.
Label gains `truncate` + `title` so long widget names stay on a
single line.
Column-header spacer widens to reserve the new trailing arrow
column so the Tile / Chart switches stay column-aligned.
The existing `dashboard-layout-section.test.tsx` reads `data-slot`
attributes (unchanged), so the four-test suite stays green.
The dashboard tile strip enforces equal heights at `sm:` and above
via `grid auto-rows-fr` but lets each tile take its content height
at `<sm`. Sibling drift of 16-24 px appears whenever the comparison
callout or the sub-row pair paints on one tile and not on its
neighbour (the glucose tiles ship without `trend7Delta` /
`compareDelta`, making this the visible case).
Pin a 140-px contract at `<sm` and let it release back to `auto`
from `sm:` upwards:
- Strip wrapper carries `[--tile-h:140px] sm:[--tile-h:auto]`.
- Each tile cell consumes `h-[var(--tile-h)] sm:h-auto`.
- Inside `<TrendCard>`, the comparison-callout slot reserves
`min-h-[18px]` even when empty and clamps to `line-clamp-1`
at `<sm`.
- Sub-row pair switches to `flex-nowrap overflow-hidden` at `<sm`
(clipping over wrapping) and releases to `flex-wrap` from `sm:`.
A new snapshot test pins the contract: callout slot reserves the
minimum height, callout clamps to a single line, sub-row pair
renders with `flex-nowrap` + `overflow-hidden`.
Recharts ignores the `interval` prop on `<XAxis type="number" />`.
The pulse + mood charts switched to numeric axes in v1.4.25 to
support multi-band rendering, which silently degraded the
day-aware density policy to "every tick" on those charts. On a
365-row pulse view that meant ~365 overlapping labels.
Add `computeTickPositions(chartData, viewportWidth)` — translates
the existing `chooseTickInterval` skip count into explicit indices
the numeric axis consumes via the `ticks` prop. Tick count clamps
to a 3-12 visible-label range so dense series don't overcrowd and
sparse series don't collapse to a single label.
The category-axis charts (`compliance-line-chart`,
`medication-compliance-chart`) stay on `interval` — Recharts
honours it there.
The chart fetcher always pulled raw rows from `/api/measurements` —
up to ~5 000 pulse samples per range-tab change on a continuous-
monitoring account. The server's `aggregate=daily` SQL path lands
in v1.4.29 C2; this commit teaches `<HealthChart>` to ask for it
on every window over 7 days. Short windows (7-day overview) keep
raw fetching so hour-by-hour detail stays visible.
Effect on a 30-day pulse view: payload drops from up to ~5 000
rows to one row per day. The client-side `dailyAggregates` Map
becomes a no-op on already-bucketed rows.
Range test gains two assertions: the default 30-day window asks
for `aggregate=daily`; the `windowOverride="last7days"` mini-chart
omits the parameter.
Sweep across the v1.4.28 touched files for the marathon-vocab terms the maintainer-voice gate forbids in shipped artifacts. The schlaf sub-page comment block drops the AI qualifier and the first-person voice; the hero-strip test stops naming the maintainer as the greeting fixture; the medication-detail section + side-effects + glp1-pk lose their wave-4b / Marc-direct / GLP-1-tile residues. Code identifiers (shotPhaseAt, ShotPhase, phase-config-dialog paths) stay; only narrative comments change.
Two consumers (trends-row.tsx + vo2-max-chart-row.tsx) still hand-rolled the same dynamic() block plus a degenerate local ChartSkeleton shadow that bypassed the canonical primitive. Both files now import HealthChartDynamic; the local ChartSkeleton drops from trends-row and vo2-max-chart-row in favour of the shared chart-skeleton module.
The per-target Coach button rode the MessageCircle glyph; hero strip, inline pill, layout FAB and daily-briefing CTA all paint Sparkles. The lone outlier swaps to Sparkles so the Coach-launch glyph reads as one icon vocabulary across the app. Separately, the insights/medikamente per-medication card painted its own header recipe (text-sm title with a Pill glyph and dose on line 2); routing the same row through MedicationCardHeader matches the canonical FB-G1 two-line shape used on the medications page. The streak chip rides the stateBadges slot.
The drug-level chart shipped a hand-rolled section wrapper (rounded-xl border p-4) that diverged from the three sibling sections on /medications/[id]/history. The chart now mounts inside the shared MedicationDetailSection primitive so the page reads on one chrome recipe. The compact prop + windowHoursBefore override retired with the dashboard tile that consumed them; the chart now ships one window contract and one body height. ~95 LOC removed.
Two narrow-viewport regressions surface on 320 px phones. The medication-list row's category badge and state badges share line 2 via flex flex-wrap; any state badge pushes the row to three lines and breaks the FB-G1 two-line contract for ~20 % of configured drugs. State badges now ride their own row below the category badge so the canonical row stays two lines. The side-effects timeline pinned its date column at 88 px; the longest short-date variant fits inside 56 px with slack, so the column narrows and the category + entry + severity left slot recovers 32 px of wrap headroom.
The MoodChart mini wrapper rode the shadcn Card primitive which paints rounded-xl by default; the BP and weight tiles next to it paint rounded-md via the HealthChart mini shell. The trends row therefore showed two corner radii. Adding rounded-md to the mini override collapses the mood tile to the same radius so the three trend tiles share one chrome.
The delta-explainer trigger now ships aria-expanded + aria-controls and the surrounding delta text gets a parent-owned aria-describedby thread to the explainer body. The mobile path drops the <span onClick onKeyDown> wrapper around the button so the a11y tree shows one interactive element where it used to show two; the button handles Enter/Space natively. The HealthScoreCard threads a useId pair across the delta line and the explainer so the connection is structural, not visual.
The per-card Coach button on the targets page rode the shadcn icon variant (size-10, 40 px) and the HealthScore delta explainer trigger collapsed to a 16 px chip. Both sit under the project's 44 px floor (WCAG 2.5.5). The targets Coach button gains min-h-11 min-w-11 on the icon variant; the delta explainer keeps a 12 px visible glyph but inflates the hit surface to 44 px and pulls the row stride back via negative margin so the optical chip stays small.
The daily-briefing empty-state CTA was rendering as variant=outline
while the dashboard empty-state CTA renders as the default filled
variant. Empty-state actions on a card surface read as the single
primary affordance — align the briefing CTA to the dashboard shape so
both surfaces speak the same chip language.
Closes BK-M2.
Lift the section-header chrome on /medications/[id] to one canonical
shape so the page reads as one document. Every section header now uses
text-base font-semibold leading-6 tracking-tight via the shared
<MedicationDetailSection> primitive (which keeps the surrounding
border-border/60 + px-3 py-2.5 + border-t contract). DrugLevelChart's
standalone header migrates to <h2> with the same classes.
Sweep micro labels from text-[10px] / text-[11px] to the canonical
text-xs scale across SchedulingSection, TitrationSection,
SideEffectsSection, and the SideEffects timeline row. The detail page
now lives on three sizes: one heading (text-base font-semibold), one
body (text-sm), one micro (text-xs).
Closes FB-F3 and FB-F4. Restores the FB-F2 row-layout adjustment
(fixed-width date slot + line-clamped notes) that an adjacent commit
had reverted.
The "vs last week" delta line on the HealthScore card has been
shipping a digit without explaining what the comparison covers, why
the digit moved, or what the user can do next. FB-I1 — Marc asked
for a tappable `?` glyph next to the delta line that opens a short
read explaining the three things in one shot.
The new `<HealthScoreDeltaExplainer>` mounts a single icon-only
`HelpCircle` button next to the existing delta caption. Tapping the
glyph opens a popover on `md+` and a bottom-sheet on phone-class
viewports — the existing `<ResponsiveSheet>` primitive handles the
branch, mirroring the same pattern landed in v1.4.27 MB1.
Body copy is a three-sentence Marc-Voice read: which components
contributed, what window the comparison covers, what concrete next
step the user can take. Six locales ship native translations — no
English fallback strings on the new keys.
The explainer only mounts when the parent surfaces a numeric delta;
the "no history yet" branch keeps the existing caption untouched
because the explainer would have nothing to compare against.
Tests pin the trigger button's `data-slot` and locale-grounded
accessible label across EN/DE, lock in the "closed by default" SSR
snapshot, and verify the body copy ships in every locale via the
existing `t()` payload. The interactive open behaviour is exercised
on the live page; the SSR contract is enough to catch a future
silent locale drop.
Shorten the add-CTA to a single verb (Log / Erfassen / Consigner /
Registrar / Registra / Dodaj) so the chip stops overflowing the
side-effects card on narrow viewports. The section title carries the
context; the qualifier was redundant. Tests updated to pin the new
contract.
Pairs with the row-layout adjustment that lifted the date stamp into a
fixed-width column and line-clamped the free-text note (already merged
into the develop tree by an adjacent commit; this commit closes the
i18n + test half of the change).
The Coach drawer's phone branch capped at 95 dvh, leaving only a 5 %
slice of the underlying /insights page visible — read as a takeover
instead of a sheet. Every other mobile sheet in the app routes
through <ResponsiveSheet>, whose phone branch caps at 90 dvh so 10 %
of the underlying page stays visible. Match the convention here so
the Coach drawer feels like the same kind of surface as the rest of
the app on phone-class viewports.
The per-card Coach CTA was rendering a labelled ghost pill (chat
glyph + 'Coach fragen' text). On a card already carrying a status
pill, range bar, consistency strip, headline number, edit cog and a
source link, the labelled pill dominated the optical hierarchy. Drop
the visible label, keep the chat-bubble glyph, and surface the
affordance via aria-label + title so screen readers and tooltips
still announce 'Ask Coach' / 'Coach fragen'. Collapse the footer
flex container to a single horizontal row at every breakpoint now
that the button is icon-sized.
Lift the mobile FAB out of <CoachLaunchButton> into a new
<LayoutCoachFab> mounted once next to <CoachLaunchProvider> in the
Insights layout. Each sub-page previously mounted <CoachLaunchButton>
which rendered BOTH the FAB and the inline pill, so a phone user
ended up with 2-3 duplicate FAB nodes in the a11y tree (collapsed
visually by fixed positioning). The inline pill stays on the per-page
action rows where copy + position matter; the FAB now paints exactly
once per Insights surface.
The HealthScore card paints 75-110 px shorter than the action+prompts
column to its left at md+ — the bottom edge of the card lands a full
chip-row above the suggested-prompts strip's last entry. The two
columns sit in a flex row but the row was anchoring children with
`items-start`, which lets the right column collapse to its content
height even though the left column's chip-strip wraps to two or three
rows on desktop.
The fix is two-sided:
- The hero row flips from `items-start` to `items-stretch` whenever the
HealthScore panel mounts. The class only applies on the conditional
branch so the no-score layout keeps the same vertical rhythm it had.
- The card wrapper opts into the stretched shell with `flex h-full
flex-col`; the existing inner content block becomes `flex-1 flex
flex-col gap-3` and the disclaimer footer gets `mt-auto`. The score
number and sub-bars stay anchored at the top, the disclaimer pins to
the bottom, and the recovered vertical space sits quietly between
the provenance accordion and the disclaimer footer rather than
spilling under the disclaimer line.
FB-H3 — the "Wochenbericht erstellen" button and the WeeklyReportBanner
were both retired in the R3b weekly-report scope-reduction pass; the
hero action row is already Coach-only and nothing in this commit
revisits that surface.
Tests pin the row's `md:items-stretch` / `lg:items-stretch` classes,
the wrapper's `h-full flex flex-col` opt-in, and the disclaimer's
`mt-auto` anchor — DOM-class assertions, no brittle pixel-height
checks. Tests on the no-score branch keep the older `items-start`
absence pinned so a future refactor can't blanket-apply the stretch
contract on layouts that have nothing to stretch toward.
Carve a shared <MedicationCardHeader> primitive consumed by both the
generic <MedicationCard> and the <Glp1MedicationCard>. Both rows now
render the canonical two-line shape: line 1 = {name} {dose} (bold), line
2 = class label + optional state badges. The GLP-1 row drops its syringe
glyph and middle-dot separator and reads as the same row shape every
other medication does. Tests updated to pin the new contract.
Retire the entire inventory surface from the GLP-1 medication card:
the inline "pens left · weeks of supply" summary line, the disclosure
that wrapped the per-pen list, and the InventorySection component
file (no other consumer). The orphan i18n keys under
medications.inventory.* and the glp1Inventory* labels retire alongside
the help-text revision for glp1DosesPerUnitHelp. The iOS-consumed
Glp1InventoryDTO slot on /api/medications/[id]/glp1 stays byte-stable
in the response shape, and the server-side state-machine helpers
remain for that endpoint's computed weeksOfSupply.
Retire the inline dose-history `<details>` block on the GLP-1
medication card and the IntakeHistoryList mount on the standalone
medication detail page. The IntakeHistoryList component had no other
consumer, so the file retires together with its orphan i18n keys.
The iOS-consumed GET /api/medications/[id]/intake endpoint, its Zod
schema and the doseChanges wire field on /api/medications/[id]/glp1
all stay byte-stable.
Drop the InsightAdvisorCard component, its four integration tests
under src/components/insights/__tests__, the page-level mount on
/insights, the orphan i18n keys (advisorTitle, aiOverviewTitle), the
e2e advisor-card spec, the dashboard.spec.ts advisor assertion and
every related comment. The InsightsTabStrip regenerate icon stays
untouched as the sole power-user refresh affordance. The iOS-consumed
POST /api/insights/generate endpoint and its request/response shape
stay byte-stable.
Drop the entire weekly-report path: the /insights/report/[week] route,
the WeeklyReportView component, the hero strip's banner mount and
"Wochenbericht erstellen" action button, the ISO-week helpers, the
schema slot, the AI prompt rule + OUTPUT FORMAT block, the openapi
field, the i18n keys and every associated test. Coach now owns the
sole hero-row action. iOS contract is unaffected — no native consumer
reads any weekly-report endpoint or schema slot per the v1.4.28 iOS
contract inventory.
Drop the standalone GLP-1 dashboard tile and its data-fetch route.
The /api/dashboard/glp1 endpoint has no consumer outside the retired
tile (no native client reads it), so the route, the tile component,
its test, the e2e spec, the locale keys, the query-key entry and the
drift-guard assertion all retire together.
The medication detail page and its bidirectional contracts stay
untouched. An opt-in dashboard widget that surfaces the same level
chart will land later in this cycle, anchored on the existing
dashboard-layout toggle list.
The BD-Zielbereich tile rendered "1.1." instead of the expected
percent. Root cause: the dashboard mount synthesised a fake
`slope30 = bpTrendDelta / 30` so the trend arrow could move. The
result was a small float (≈ 1.1 for a 33-point delta) that the
TrendCard's downstream formatter pipeline rendered as a
date-shaped artefact — the maintainer's screenshot regression
across v1.4.22 / v1.4.25 / v1.4.26 patches that kept treating the
symptom instead of the divergence.
Rewrite the tile against the shared `<TrendCard>` primitive with
the same prop contract every other tile uses (`latest`, `avg7`,
`avg30`, `slope30=null`, `trend7Delta`, `compareBaseline`,
`compareDelta`). Drop the synthetic-slope block, drop the
`avgAllTime` prop family from `<TrendCard>` (the BD tile was the
only consumer), drop the orphan `charts.avgAllTimeShort` i18n key
across six locales. The all-time aggregate stays visible on the
`/targets` Blutdruck card with full context.
New regression spec at `src/components/charts/__tests__/bd-tile.test.tsx`
locks the BD tile's headline to a numeric percent string, asserts
DOM-shape parity with the Weight tile, and pins the absence of the
retired `trend-card-all-time` slot.
iOS contract: none. `/api/analytics` still emits the
`bpInTargetPct*` family unchanged (the `bpInTargetPctAllTime`
field stays for the /targets page consumer); iOS does not consume
the dashboard tile.
`/insights` landed at the wrong vertical position when the user
returned from a sub-page, and vertical swipes that began on the
sticky `<InsightsTabStrip>` were eaten by the strip's
`overflow-x-auto` scroll container so the page "felt stuck"
mid-gesture. Two roots, two fixes:
1. Move the horizontal pill scroll from the outer `<nav>` onto the
inner pill-row `<div>` (the inner row already declared
`overflow-x-auto`; the outer one is redundant once it's gone).
Add `touch-action: pan-y` to the outer `<nav>` so vertical
swipes pass through.
2. Defer the `<SubPageShell>` scroll-reset to the next animation
frame so the sub-page's first paint settles before the scroll
lands. Mirror the same deferred reset on the `/insights` mother
page so a back-navigation lands cleanly at the top instead of
inheriting the sub-page's vertical position through cached
scroll state.
A Playwright spec at `e2e/insights-scroll-restoration.spec.ts`
locks both regressions: (a) /insights → /insights/<sub-page> → back
lands within 50 px of the top, (b) the outer `<nav>` declares
`touch-action: pan-y` and no longer owns the horizontal scroll.
iOS contract: none — both surfaces are web-only.
The chart-data query in health-chart.tsx walked the full measurement
history with a `while (true)` paginated loop on every visit. For
Apple-Health-rich accounts (pulse every minute, months of backfill)
this meant tens of thousands of rows per type per page navigation,
which is the root cause behind FB-D2's "pulse insights loads forever"
report.
Derive `from` / `to` from the active range tab (plus the
comparison-baseline shift when overlay is on), pass both to
/api/measurements, and drop the unbounded paginator. The query key
re-keys on the window so a range-tab change refetches the right
slice. staleTime 60s + gcTime 5min hold the bounded payload across
tab navigation between insights sub-pages.
New `aggregate` query param + range-aggregation helper let the GET
handler downsample server-side for ranges over 90 d (one row per
day) and over 365 d (one row per ISO week). The per-request `limit`
ceiling lifts from 500 to 5000 — still bounded, but enough headroom
for an explicit from/to window without re-introducing pagination.
iOS contract — GET /api/measurements: additive. The `from`, `to`,
`aggregate` params are all optional; callers that omit them (the
iOS client, the existing measurements page) see the unchanged
response shape and the historical 500-row default cap. The
aggregated branch is only entered when both from + to are present
AND the window is wide enough for the threshold ladder to engage.
The PUT /api/measurements/[id] handler dispatched the Prisma
update without a try/catch, so re-pointing measuredAt onto an
existing (userId, type, measuredAt, source, sleepStage) tuple
bubbled the P2002 violation as a 500 and the row-edit Sheet
rendered the generic save-error toast.
Mirror the POST handler's P2002 catch: return a clean 409 with a
stable `meta.errorCode = "measurement.duplicate_timestamp"` and a
new measurements.duplicateTimestamp i18n key across the six
locales. The mutation in measurement-list.tsx reads the error
code from the response envelope and surfaces the localised
banner instead of the upstream English fallback.
iOS contract — PATCH /api/measurements/{id}: additive. The 200
happy-path response shape (MeasurementWireDTO), the 422
validation path and the updateMeasurementSchema are untouched.
The new 409 path is strictly clearer than the 500 it replaces.
Both the `/admin` overview snapshot and the full `/admin/system-status`
page now read `offlineGeoEnabled` from `/api/version` and render a
status row with a green chip when the offline databases are loaded or
a yellow chip with the configuration hint when the runtime is on the
ipwho.is fallback. The row only renders when the field is present so
legacy `/api/version` responses do not produce a placeholder slot.
Adds six locale keys per bundle (`admin.offlineGeoLabel`,
`offlineGeoEnabled`, `offlineGeoFallback` plus the three
`admin.overview.snapshotOfflineGeo*` siblings) across all six
locales.
MB7 extracted three drifted local `NATIVE_SELECT_CLASS` constants
into the shared primitive at `h-9` (36 px) — the pre-v1.4.27 floor.
MB2 had already lifted Input + Select + SelectTrigger to `h-10`
(40 px) for the WCAG 2.5.5 tap-target sweep; the NativeSelect
extraction missed that contract bump and the three settings + admin
surfaces consuming it (account-section gender + language + timezone,
timezone-picker, general-settings-section default-language) now
render one step shorter than the Input rows they share a grid with.
The settings-mobile-consistency e2e spec already asserts a uniform
40 px floor across every `section input + section select` on
/settings/account and a 40 px floor across every `section select`
on /settings/ai; the primitive bump aligns the visual rhythm with
both the rest of the form and the test contract.
RC1 rewrote the FR/ES/IT/PL banner notice from the previous
"AI-drafted" disclosure to "machine-translated" wording as part of
the convention-compliance pass. The locale pattern tests still
checked the retired vocabulary and failed. Re-pinned to the new
shape so a future regression cannot quietly soften the disclosure
or re-introduce the retired wording.
Plus the orphan RC1 reconcile report at
.planning/round-4-reconcile-rc1-ci-i18n-convention-report.md.
Three single-class polish swaps to retire P0 / P1 drifts the UI-
conformity review caught:
- TrendCard headline number was the only dashboard tile using
font-bold for its hero value; every sibling renders the same
numeric role at font-semibold. Align so the trend strip reads
uniformly.
- account-section password form was the only form in the codebase
on space-y-3 outer rhythm; every other form uses space-y-4. Lift
to match.
- mood-chart YAxis tickMargin sat at 6 while every other line chart
passes 10 from the same canonical recipe. Match so mood labels
sit at the same distance from the axis as siblings.
Drop fourteen exported helpers and constants with zero call sites:
countMessages, buildGeneratePrompts, _resetSafetyContractsCacheForTests,
forEachLocaleAndRule, getPulseRange, trendLinePoints,
CHART_MINI_HEIGHT_PX, formatDateWithWeekday,
resolveGeneralStatusLocale, isSubPageSlug, recordHeartbeat,
EVENT_TYPE_LABELS, cleanupExpiredRateLimits, and sleepStageEnum.
Also retire the v1.4.20 legacy insights prompt scaffold —
getInsightsSystemPrompt, InsightsOutput, and the two dead
INSIGHTS_SYSTEM_PROMPT_EN/DE constants — plus the matching vi.mock
key in the generate route test that no longer maps to a live export.
Drop the empty __testables block in glp1-snapshot.ts, the legacy
helper reference in the insight-generator JSDoc, and the two stale
v1.4.16 InsightsPageHero comments in hero-strip + its test file.
Consume the MedicationComplianceChart compareBaseline prop via an
explicit void so the destructure documents the intentional accept-and-
defer pattern instead of looking like a missed wire-up.
Every symbol verified zero-caller via grep across src/, scripts/, and
__tests__/ before drop.
The mood-list and measurement-list edit dialogs were raw <Dialog> mounts, so
on phones the user faced a centred dialog with the keyboard pushing the form
off-screen instead of the bottom-sheet branch the rest of the app uses.
Both row-edit surfaces now mount <ResponsiveSheet>, with the Save / Cancel
row portalled into the footer slot so the bottom-sheet branch can sticky-pin
the action row. The submit button keeps its <form> association via the HTML
form attribute so submit-on-Enter and native validation continue to work.
Until now every <ResponsiveSheet> consumer (except export-section) inlined the
form footer inside the body slot, so the primitive's sticky-pinned bottom rail
on the Sheet branch only painted empty. The Save / Cancel row therefore
scrolled with the body and slid out from under the soft keyboard on mobile.
This rewires the action-row through the footer prop on all seven page-level
mounts (dashboard quick-entry measurement + mood, /measurements, /mood,
/medications, side-effects sheet, inventory add sheet, intake-history edit +
create sheets, medication create/edit sheet). Each form receives an optional
footerSlot prop; when set, the kebab + Cancel + Save row is portalled into
the ResponsiveSheet footer slot so the bottom-sheet branch can sticky-pin it.
The Save button keeps its <form> association via the HTML form attribute so
submit-on-Enter and native validation continue to work end-to-end.
MB7's CF-52 sweep retired three drift-prone NATIVE_SELECT_CLASS clones
behind the shared NativeSelect primitive. The AI provider section
carried five more raw <select> declarations with their own copy of the
border + bg + height contract — exactly the drift the primitive was
introduced to cure. Swap them over so the AI provider, OpenAI model,
Anthropic model, local model, and Add-provider pickers all render
through the same styled wrapper as the rest of Settings.
The Sheet close-X button rendered at the 16 px size of its SVG icon —
no min-h / min-w floor — even though the bottom-sheet branch of
ResponsiveSheet is the primary dismiss path on phone viewports for
quick-add sheets, intake-history trays, and the Coach side trays. The
Dialog primitive lifted its close-X to a 36 px floor in MB2 per the
WCAG 2.5.8 minimum target rationale; align the Sheet primitive on the
same shape so the highest-traffic dismiss control no longer trails
the rest of the v1.4.27 tap-target sweep.
Replace `insights.aiAnalysisTitle` with `insights.advisorTitle` across
all six locale bundles and the four call sites in
`insight-advisor-card.tsx`. EN renders "Personal health advisor", DE
"Persönlicher Berater" matching the card subtitle vocabulary, and
FR/ES/IT/PL pick up native phrasings instead of the English fallback
that previously leaked verbatim on those locales. The card now reads
in the same voice as the rest of the Coach and advisor surfaces.
v1.4.27 MB7 — residual surface polish bundled into a single
commit:
- CF-61: the auth login + register cards drop `p-8` to `p-6 sm:p-8`
so Galaxy Fold / Pixel 5 keep the card breathable instead of
surrendering half the visible viewport to padding.
- CF-68: each daily-briefing key-finding row wraps in a `<Link>` to
the matching routed sub-page so the whole card is tappable on
mobile. Metrics without a dedicated sub-page (HRV, resting HR,
steps, …) render the legacy static row.
- CF-70: the sleep-stage window-toggle row bumps from `gap-1` to
`gap-1.5` so the active pill border doesn't fuse with the
inactive neighbour on Pixel 5.
- CF-71: the trends-row card `min-h-[300px]` floor moves behind a
`md:` gate so mobile single-column doesn't eat dead space below
a short chart.
- CF-72: the insights tab-strip paints a right-edge fade gradient on
`<sm` so the user reads "there's more to scroll" when the pill
row overflows.
- CF-46: the measurement-list filter row stacks vertically on `<sm`
so the `w-48` SelectTrigger doesn't crowd the count caption to a
2-line wrap.
- CF-76: the measurement-list mobile-row badges bump from
`text-[10px]` to `text-[11px]` so the metadata holds the
legibility floor.
- CF-75: the passkey mobile card promotes the device-type from
plain text to an outline Badge so the chip row reads consistent
with the desktop table column.
- MA2-F8: the medikamente page card header wraps the title in
`min-w-0` + `truncate` so long medication names don't push the
streak badge off the card on Galaxy Fold.
v1.4.27 MB7 — four settings/admin polish items in one commit:
- CF-53: the Withings credentials grid drops from a 3-column row
(client-id / secret / save) to a 2-column row of inputs with the
Save button lifted into its own row below. The previous invisible-
label hack collided on Galaxy Fold; the new row places the Save
button at `justify-end` on every viewport and fills the column
on `<sm`.
- CF-55: the feedback-inbox TabsList wraps in an `overflow-x-auto`
strip so the 4 status pills + count badges scroll horizontally on
Pixel 5 instead of squashing below the legibility floor.
- CF-56: the shared `SettingsToggle` row stacks vertically on `<sm`
so the German description copy doesn't compete with the switch for
horizontal space. The inline duplicate in
`general-settings-section.tsx` (default-language row) picks up
the same contract.
- CF-57: the integrations action row floors each button to a
`min-w-[10rem]` on `<sm` so the four AlertDialog + test-connection
triggers stack two per row at most and fill their column cleanly.
v1.4.27 MB7 — four medications-surface polish items in one commit:
- CF-48: the titration ladder keeps its vertical (`flex-col`) layout
until `md:` instead of breaking horizontal at `sm:`. The German
dose + typical-weeks labels wrapped to 3 lines per step on Pixel 5,
which read denser than the vertical layout below.
- CF-49: the 30-day cadence grid tightens from `gap-1` to `gap-0.5`
on `<sm` so more cells fit per row without sacrificing the 44 px
tap target per cell. The original 4 px gap returns at `sm:`.
- CF-50: the PhaseConfig dialog rows stack on `<sm` so the input +
mode toggle + suffix caption fit inside the 280-300 px dialog
inner width on Galaxy Fold. The suffix wraps below at `<sm` via
`basis-full`; `sm:` keeps the single-line layout.
- CF-66: the DrugLevelChart wrapper drops the `md:p-6` lift so its
padding matches the surrounding trend cards (`p-4` everywhere).
v1.4.27 MB7 / CF-42 + CF-44 — the dashboard tile strip switches to a
`flex overflow-x-auto` row at `<sm` so the tiles scroll horizontally
instead of wrapping to 3-4 rows on Pixel 5 / Galaxy Fold. From `sm:`
upwards the strip falls back to the canonical grid `auto-fit +
minmax(9rem, 1fr)` layout the maintainer pinned in v1.4.4. The
getting-started checklist header wraps onto a second row when the
title + dismiss button overflow, and `min-w-0` + `truncate` keep
the title from pushing the chevron off-screen on Galaxy Fold.