Commit Graph
350 Commits
Author SHA1 Message Date
Marc-André Bombeck 1b0e81ae42 fix(targets): make the coach launch an icon-only affordance
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.
2026-05-16 01:45:03 +02:00
Marc-André Bombeck d286220bed perf(charts): wire chart-skeleton loading state across dynamic imports 2026-05-16 01:43:37 +02:00
Marc-André Bombeck 4c6d877921 refactor(coach): consolidate launch button to inline + layout-FAB shape
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.
2026-05-16 01:42:12 +02:00
Marc-André Bombeck 155b529def fix(insights): match HealthScore card height to the hero column
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.
2026-05-16 01:41:36 +02:00
Marc-André Bombeck 6f6992c683 refactor(medications): unify medication-list row shape
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.
2026-05-16 01:41:17 +02:00
Marc-André Bombeck 8c8d6dc27f chore(medications): drop the Bestand section from GLP-1 detail
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.
2026-05-16 01:33:50 +02:00
Marc-André Bombeck 8c81af10d6 chore(medications): drop the Dosis-Historie disclosure from GLP-1 detail
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.
2026-05-16 01:27:40 +02:00
Marc-André Bombeck 52edf85f02 chore(insights): retire the InsightAdvisorCard surface
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.
2026-05-16 01:19:14 +02:00
Marc-André Bombeck cad53a68a3 chore(insights): retire the weekly-report surface
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.
2026-05-16 01:14:26 +02:00
Marc-André Bombeck 8e5f71b1b9 chore(dashboard): retire the GLP-1 tile
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.
2026-05-16 01:00:09 +02:00
Marc-André Bombeck 59ef95f284 fix(dashboard): align BD-Zielbereich tile with shared TrendCard primitive
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.
2026-05-16 00:35:28 +02:00
Marc-André Bombeck ac80c099e2 fix(insights): unstick scroll on tab-strip and mother-page navigation
`/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.
2026-05-16 00:31:05 +02:00
Marc-André Bombeck b00be2863d perf(charts): bound health-chart fetches to the active range window
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.
2026-05-16 00:20:27 +02:00
Marc-André Bombeck 538b44f74d fix(api): return 409 on duplicate-timestamp measurement edit
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.
2026-05-16 00:15:47 +02:00
Marc-André Bombeck 82a23069a5 feat(admin): surface the GeoLite2 availability state on the system status page
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.
2026-05-15 22:21:03 +02:00
Marc-André Bombeck 0c6d728f9a fix(ui): lift the NativeSelect primitive to the 40 px tap-target floor
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.
2026-05-15 21:42:08 +02:00
Marc-André Bombeck a71115dfb4 fix(tests): flip the maintainership-banner assertions to the neutralised copy
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.
2026-05-15 21:22:34 +02:00
Marc-André Bombeck bfb1335158 chore(ui): align TrendCard heading weight, account form rhythm, and MoodChart tick margin with siblings
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.
2026-05-15 21:17:54 +02:00
Marc-André Bombeck 267868c013 chore(cleanup): retire orphan exports and the stale legacy insights prompt module
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.
2026-05-15 21:16:35 +02:00
Marc-André Bombeck 0baea139a3 refactor(forms): migrate mood and measurement row-edit dialogs to ResponsiveSheet
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.
2026-05-15 21:11:39 +02:00
Marc-André Bombeck 6704579c25 fix(forms): wire the ResponsiveSheet footer slot across every page-level form mount
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.
2026-05-15 21:05:50 +02:00
Marc-André Bombeck b10ec6261d refactor(settings): migrate the five remaining selects to the NativeSelect primitive
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.
2026-05-15 21:02:05 +02:00
Marc-André Bombeck 7f035958e4 fix(ui): widen the Sheet close-X hit area to match the Dialog primitive
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.
2026-05-15 20:59:08 +02:00
Marc-André Bombeck 3b045d6fba chore(insights): rename the advisor title key to neutral phrasing across six locales
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.
2026-05-15 20:54:54 +02:00
Marc-André Bombeck 0b3f9e3e2c chore(misc): auth padding, daily-briefing links, tab fade, trends min-h, list metadata, passkey badge
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.
2026-05-15 19:23:34 +02:00
Marc-André Bombeck ab4529fcb3 chore(settings,admin): consolidate Withings credentials, SettingsToggle stacking, and feedback tabs
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.
2026-05-15 19:19:14 +02:00
Marc-André Bombeck e1451f840f chore(medications): tighten titration ladder, cadence grid, and phase-config layout on mobile
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).
2026-05-15 19:16:45 +02:00
Marc-André Bombeck 7bfd5beeb2 chore(dashboard): horizontal-scroll the tile strip on narrow viewports and wrap the onboarding header
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.
2026-05-15 19:15:20 +02:00
Marc-André Bombeck 071f26bf0f chore(insights): polish VO2 strip, status show-more, and trend-card secondary row
v1.4.27 MB7 — three related insights surface polish items in one
commit:

- CF-38: the VO2 stat strip extends the 2-column phone grid all the
  way to `<lg` so tablets keep the 2×2 layout, and every chip floors
  at `min-h-[64px]` so the row paints at a consistent height even
  when one stat has no trend arrow.
- CF-39: the insight-status-card body collapses to `line-clamp-3`
  with a "Show more" toggle when the assessment text is long
  enough to actually clip. Short bodies render plain so the user
  never sees a useless toggle.
- CF-41 + CF-67: the trend-card mobile-secondary row retires and the
  all-time aggregate renders inline alongside the `7d` / `30d`
  pair on every viewport. The flex-wrap already handles the overflow
  on narrow tiles, and the standalone comparison-delta block drops
  its `hidden sm:block` gate so the callout reads on every screen.
2026-05-15 19:14:08 +02:00
Marc-André Bombeck 53c5163901 chore(charts): apply responsive aspect-ratio and height var across the chart surface
v1.4.27 MB7 / CF-37 + CF-43 — the scatter correlation chart now uses
an `aspect-square` ratio at `<sm` so the plot area stays a centred
square on phones (matches Apple Health's correlation viz); from `sm:`
upwards a 3:2 ratio recovers the legacy ~250 px shape. The three
HealthChart-family components (`HealthChart`, `MoodChart`,
`MedicationComplianceChart`) drop the hard-coded `h-[240px]` floor
for a `var(--chart-height, 240px)` token plus a `md:` lift to 280 px
so taller viewports get a more legible chart without tweaking each
consumer.
2026-05-15 19:11:39 +02:00
Marc-André Bombeck 07c9d01faf feat(ui): extract a shared NativeSelect primitive and migrate three call sites
v1.4.27 MB7 / CF-52 — three settings/admin surfaces each declared a
local `NATIVE_SELECT_CLASS` constant tracing the same visual contract
as the shadcn `<SelectTrigger>`. The copies had drifted slightly so
`AccountSection`, `TimezonePicker`, and `GeneralSettingsSection`
rendered at subtly different focus-ring weights. The new
`<NativeSelect>` primitive owns the contract; all three call sites
import it and the in-file constants retire. The native picker remains
the right experience for the 400-entry timezone list and the
admin-only locale dropdowns, so the primitive deliberately stays on a
styled `<select>` rather than wrapping the radix popover-driven
`<Select>`.
2026-05-15 19:10:13 +02:00
Marc-André Bombeck 0fb0235d17 fix(admin): move pagination and CSV export out of the table scroll containers on login-overview and app-log-preview
CF-21: the audit-log prev/next controls and summary row used to live
inside the same `overflow-x-auto` wrapper as the table. On narrow
viewports admins had to scroll the table back to its starting offset
before the pagination became visible. Same problem for the app-log
preview summary. Both rows now sit in a sibling block under the
scroll container, while the table itself keeps panning horizontally
on touch. The login-overview CSV export was already outside the
wrapper and stays put. The recent B3 carrier-chip layout is
unchanged.
2026-05-15 19:08:40 +02:00
Marc-André Bombeck 2bd659f66d feat(ui): EmptyState gains a ctaSize prop and lifts to a full-width tap target on mobile
v1.4.27 MB7 / CF-36 — the empty-state CTA used to render at the
button primitive's default density, which on Pixel 5 left a ~32 px
tap target inside a ~280 px column. The new `ctaSize="lg"` lifts
the inner button (or `asChild` link) to the 44 px floor and stretches
the wrapper to `w-full sm:w-auto` so the CTA fills the column on
mobile and centres on tablet+ viewports. Existing call sites that
omit the prop keep the previous compact density.
2026-05-15 19:07:54 +02:00
Marc-André Bombeck 650d4f8e45 feat(coach): re-pin the message-thread scroll when the soft keyboard opens
Subscribe to window.visualViewport.resize so the last bubble stays
visible when the phone's soft keyboard slides in. Without the listener
the scroller's scrollHeight reflects the layout viewport rather than
the visible region, leaving the tail of the thread hidden behind the
keyboard. The handler only re-pins when the user was already at the
bottom, matching the existing auto-scroll contract.
2026-05-15 19:07:42 +02:00
Marc-André Bombeck 40916d3127 feat(coach): swap the sources-rail toggle to a shadcn Checkbox
Replace the raw <input type="checkbox"> in the sources rail with a
shadcn-style <Checkbox> backed by radix-ui. The new primitive ships
a real focus ring, a Space-toggles keyboard contract, and a touch-
friendly hit target so the rail behaves correctly on the right-edge
side-tray surface mobile users open via the chevron button.
2026-05-15 19:07:07 +02:00
Marc-André Bombeck 2fb964ef9d feat(insights): gate the sub-page-shell focus-on-mount
v1.4.27 MB7 / CF-35 — the routed insights sub-pages used to focus the
heading on mount so a screen-reader landing announced the page title.
On mobile that fought the soft-keyboard: navigating between sub-pages
while a form was open dismissed the keyboard mid-typing. The
`focusOnMount` prop defaults to false, so only sub-pages that
explicitly want the screen-reader landing opt in. The `scrollTo({ top: 0 })`
reset stays unconditional because that's a sighted-user affordance,
not an a11y one.
2026-05-15 19:06:10 +02:00
Marc-André Bombeck f9558ce097 feat(insights): widen the Health Score card on tablets and rebalance the hero split
v1.4.27 MB7 / CF-34 — the score card swaps the fixed
`lg:w-[360px] xl:w-[400px]` width for a basis-based column so the hero
strip can split at `md+` and let the score occupy a generous-but-fluid
share of the band. The hero strip's `flex-row` flips activate one
breakpoint earlier (`md:`, alongside the legacy `lg:` modifiers) so
tablets receive the side-by-side layout instead of stacking the score
below the narrative copy.
2026-05-15 19:02:50 +02:00
Marc-André Bombeck 79dbdfbd2b feat(coach): hide the drawer window pill on phone viewports and pin title truncation
The bottom-sheet header already carries the avatar, title, new-chat,
settings, and close buttons on <sm viewports — adding the four-state
window pill there shrunk the title to a single character before
truncation. Wrap the pill in a hidden sm:block container so phone
users reach the same override via the sources-rail picker. Pin
min-w-0 on the SheetTitle so very long conversation titles always
clip cleanly inside the header.
2026-05-15 19:02:35 +02:00
Marc-André Bombeck 117eb87f75 fix(medications): widen the schedule day-of-week grid for narrow viewports and drop the DrugLevelChart dead axis labels
The medication-form schedule editor's day-of-week row packed the
wide "Daily" pill (min-w-24) into the same single-flex row as the
seven weekday pills; at 320 px the row overflowed and the last
weekday clipped off-screen. Stack the Daily pill above a fixed
seven-column grid so every weekday keeps the 44 px tap-target floor
regardless of container width.

Drop two dead axis decorations from the GLP-1 DrugLevelChart: an
empty `<text>` child beneath the XAxis that rendered an invisible
SVG node, and a duplicate Recharts `label={…}` prop on the YAxis
that painted the unit-less caption inside a 1 px-wide axis where it
could never be read. The external `<p>` above the chart remains the
single source of truth for the caption.
2026-05-15 19:02:07 +02:00
Marc-André Bombeck 9b6dbbf6ae feat(charts): pin the compliance heatmap tooltip on tap and float the cell
v1.4.27 MB7 / CF-10 — touch-pin tooltip via pointer events runs in
parallel with the existing mouse hover, so a tap on a cell holds the
day-level breakdown open until the user taps outside or on a different
cell. Pen + mouse keep the hover affordance unchanged. The heatmap now
clamps the stretch-branch cell size to a 14 px floor and wraps the
non-stretch render in overflow-x-auto so narrow viewports horizontal
scroll instead of compressing the calendar below the touch target.
2026-05-15 19:00:54 +02:00
Marc-André Bombeck 246c1def21 feat(coach): mount the drawer at the insights layout with a launch context
Promote the Coach drawer mount from the mother /insights page up to
/insights/layout.tsx so every routed sub-page can reach it. A new
CoachLaunchProvider exposes useCoachLaunch() for the hero strip,
suggested-prompt chips, and the seven sub-pages; the layout renders
the drawer beside the provider so navigation across Insights surfaces
no longer unmounts the conversation.

Each sub-page (blutdruck, gewicht, puls, stimmung, medikamente, bmi,
schlaf) mounts a new CoachLaunchButton that pins to the bottom on
narrow viewports and renders as an inline header action on lg+.
2026-05-15 18:59:55 +02:00
Marc-André Bombeck de0c16338f fix(auth): lift the register submit button and add a branded not-found page
Promote the register page's submit button to the 44 px tap-target
floor (size="lg" + min-h-11 + w-full) so the primary action stays
finger-tap reachable on a narrow viewport.

Add a new branded not-found.tsx page. The previous implicit fallback
was Next.js's bare framework text; this introduces a lighter wrapper
than ErrorDetails (404 is not an application error) — Logo, headline,
explanatory paragraph, and a single "Back to dashboard" link. The
wrapper carries min-h-dvh and safe-area-inset-top so the panel
anchors centred under iOS Safari's animated URL bar and stays clear
of the notch.

Apply the same dvh + safe-area pattern on the root-layout global
error boundary and lift its inline button heights to 44 px. Wrap the
ErrorDetails panel in a min-h-dvh flex column so server-error pages
also centre instead of hugging the notch.
2026-05-15 18:58:35 +02:00
Marc-André Bombeck fc8a58557a fix(auth): unblock the about page for unauthenticated visitors
The /about page (GeoLite2 attribution) was listed in proxy.ts
PUBLIC_PATHS but missed in AuthShell's client-side PUBLIC_PATHS, so
unauthenticated visitors bounced through /auth/login. Add /about to
the AuthShell list and treat it as a standalone public page (same
edge-to-edge render as /privacy).

Apply safe-area-inset-top on the privacy and about sticky headers so
iOS notch / Dynamic Island viewports don't overlap the brand row. Add
a default-closed Contents <details> TOC at the top of the privacy
policy and wrap every HealthKit identifier <code> with break-all so
long camelCase strings (heartRateVariabilitySDNN,
environmentalAudioExposure) wrap inside narrow viewports.
2026-05-15 18:56:49 +02:00
Marc-André Bombeck c77d5252a6 feat(settings): pair the API section tables with a mobile card-list rendering
CF-19 follow-up: the endpoint catalogue, active-token table, and
revoked-token table on `/settings/api` each gain a `md:hidden`
card-list partner so phones never have to horizontal-scroll the
tabular layout. Desktop tables stay verbatim behind `hidden md:block`.
The active-token cards expose a full-width revoke button that clears
the 44pt tap-target floor.
2026-05-15 18:56:08 +02:00
Marc-André Bombeck 4464d2c9b9 chore(charts,errors): apply the tap-target floor across charts and error chrome
Final surface sweep for the v1.4.27 tap-target work: the chart band
overlay, the error-details action row, and the public-page header
links.

- `health-chart.tsx`: band overlay used to inset `right: 18px` while
  the underlying ComposedChart margin is `right: 8`, so the colour
  band drifted left of the plotted line by 10 px on every viewport.
  Pin the overlay to the chart's actual right edge.
- `error-details.tsx`: drop `size="sm"` from the retry, copy, and
  report-issue buttons so they sit at the default 40 px primitive
  height — clears the WCAG 2.5.5 floor on touch.
- `auth/login/page.tsx`: back-to-passkey toggle wraps in `inline-flex
  min-h-11 w-full` so the form-mode swap is reachable on touch.
- `privacy/page.tsx` and `about/page.tsx`: header HealthLog + Sign-in
  links wrap in `inline-flex min-h-11` so the public-page chrome
  clears the floor.
2026-05-15 18:48:07 +02:00
Marc-André Bombeck 17aed374eb chore(dashboard): apply the tap-target floor across the dashboard tile and onboarding chrome
Lift the GLP-1 tile range strip + tab pills, the getting-started
checklist dismiss buttons, and the settings reorder + source-ladder
arrow buttons so every interactive element clears the 44 px WCAG
2.5.5 floor.

- `glp1-tile.tsx`: range-strip radio buttons gain `inline-flex
  min-h-11 items-center justify-center px-3` so each preset sits at
  44 px; the tab pills (level / weight) lift the same way.
- `getting-started-checklist.tsx`: dismiss-all CTA drops `size="sm"`
  so it sits at the default 40 px primitive height; per-row dismiss
  `X` button wraps in `inline-flex h-11 w-11`.
- `dashboard-layout-section.tsx`: reorder up + down arrows `h-5 w-5`
  → `size-11`; column-header spacer widens to `w-11` to keep the
  layout grid aligned.
- `sources-section.tsx`: metric source ladder + device-type ladder
  up/down arrows stack vertically on `<sm` so each button keeps the
  `h-11 w-11` floor without crowding the row; sit side-by-side again
  from `sm:` up to preserve the desktop layout.
2026-05-15 18:45:08 +02:00
Marc-André Bombeck bfb5ba7290 feat(coach): tap-friendly composer hint and accurate evidence aria-expanded
- New `<Popover>` primitive (Radix-backed, Dracula surface tokens)
  mirrors the existing `<Tooltip>` wrapper. Tooltips do not
  tap-toggle reliably on touch; popovers do.
- The Coach composer info-icon swaps off `<Tooltip>` onto
  `<Popover>` so the long-form "Enter to send, Shift+Enter for a new
  line" copy is reachable via a tap on mobile while the keyboard
  hover-preview semantic is preserved on desktop. The `aria-label`
  on the trigger keeps screen-reader users announced.
- The composer textarea now carries `enterKeyHint="send"`,
  `autoCapitalize="sentences"`, and an opt-in `autoFocusOnOpen` prop.
  The drawer flips that prop on when it mounts the fallback
  composer, so the textarea claims the cursor the first time the
  drawer opens; subsequent re-renders of an already-open drawer do
  not steal focus back.
- Coach evidence `<details>` is now controlled by local state. The
  summary carries an accurate `aria-expanded` that flips in
  lock-step with the disclosure; the native Enter / Space toggle
  semantics are preserved.
2026-05-15 18:42:15 +02:00
Marc-André Bombeck bba8921ec9 chore(measurements,medications): apply the tap-target floor across list and form surfaces
Lift the icon-button and pagination controls on the list + form
surfaces under `/measurements` and `/medications` so every touch
target clears the 44 px WCAG 2.5.5 floor.

- `measurement-list.tsx`: mobile-row edit + delete buttons `size-8`
  → `size-11`; pagination chevrons `size="sm"` → `size="icon"
  size-11` with translated `aria-label`; edit-dialog kebab
  `h-9 w-9` → `size-11`; `DeleteButton` default `h-8 w-8` →
  `size-11`. Empty-state CTAs drop `size="sm"` so they sit at the
  default 40 px primitive height.
- `measurement-form.tsx`: reset kebab `h-9 w-9` → `size-11`.
- `intake-history-list.tsx`: desktop + mobile per-row edit + delete
  buttons `size-8` → `size-11`; pagination chevrons → `size-11`
  icon-buttons; edit-dialog kebab → `size-11`; shared `DeleteButton`
  in this file → `size-11`.
- `SideEffectsSection.tsx`: per-entry delete `h-7 w-7` → `size-11`.
- `inventory-section.tsx`: live-pen "mark as in use" / "mark as used
  up" pills drop `size="sm"`/`h-7` so they sit at `min-h-11`; past-pen
  delete `h-6 w-6` → `size-11` icon-button.

Adds `measurements.previousPage` / `measurements.nextPage` and
`medications.previousPage` / `medications.nextPage` translation keys
so the pagination icon-buttons carry accessible labels in every
locale.
2026-05-15 18:41:52 +02:00
Marc-André Bombeck 192170ee5f feat(forms): wire keyboard hints and aria attributes across settings and admin forms
Bring the same `inputMode`, `enterKeyHint`, `autoComplete`, and
`spellCheck` defaults to the settings and admin surfaces:

- Account: profile email gets `autoComplete="email"` +
  `enterKeyHint="next"`, the disabled username carries
  `autoComplete="username"` so the password manager can still bind
  the credential, and the height field hints the decimal keyboard.
- Telegram card: bot token and chat ID both opt out of autofill,
  disable spell-check + autocapitalize, and lead the chained next →
  done keyboard order.
- Integrations: Withings client-id / secret and the moodLog URL +
  API key share the off-spell + no-capitalize secret-input contract
  and chain into the test / save action via `enterKeyHint`.
- Notifications anchors: each card wrapper carries `scroll-mt-28`
  so direct anchor navigation to `#telegram`, `#ntfy`, `#web-push`
  lands the section header below the safe-area sticky header
  instead of under it.
- Thresholds editor: min/max numeric inputs derive `inputMode`
  ("numeric" for integer ACTIVITY_STEPS, "decimal" elsewhere) and
  chain next → done.
- Admin reminders: the late-window and missed-window minute inputs
  pin `inputMode="numeric"` and the next → done keyboard order.
2026-05-15 18:36:33 +02:00
Marc-André Bombeck 48261b6797 refactor(coach): retire the redundant settings-sheet close-X
The primitive `<SheetContent>` close-X is an absolutely positioned
`opacity-70 rounded-xs` button that clashes with the rest of the
sheet's in-header chrome. Hide it via `showCloseButton={false}` and
render a matching ghost-icon `<SheetClose>` inline in the header
so the close affordance shares the same baseline, ghost variant,
and 44 px tap floor as the coach-drawer header pattern. The
settings-sheet test mock picks up a `SheetClose` passthrough so the
existing skeleton + form assertions keep working.
2026-05-15 18:35:02 +02:00