Commit Graph
1190 Commits
Author SHA1 Message Date
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 67fddb98e2 docs(planning): append v1.4.28 backlog with R4 reviewer deferrals
Append every deferred item from the nine R4 reviewer reports to
`.planning/v1428-backlog.md` so the v1.4.28 handoff lands on a single
canonical surface. Each entry carries the source reviewer + finding
ID, severity, summary, recommended fix, effort estimate, and the
why-deferred rationale from the source report.

Sections added:

- 14 mobile-fix-plan deferrals (CF-77 through CF-90) from the R3c →
  R3d consolidation. CF-77 (six admin tables card-list parity) flagged
  as the largest dedicated bucket.
- 9 simplifier follow-ups (F-H1 + F-M1 through F-M7 + F-L1 through
  F-L9 grouped) for the v1.4.28 simplification micro-bucket.
- 8 senior-dev follow-ups (HIGH-1 footer slot wiring + HIGH-2
  viewport rotation + MED-1..4 + Dialog drift cleanup + RHF deps).
- 12 design-tier polish items (M1 through M12) — GLP-1 padding, tab
  strip contrast, viewport coverage, SubPageShell description prop
  sweep, settings sidebar height, `<Button asChild>` adoption.
- 6 UI-conformity sweeps — 11 admin section heading divergence, 21
  SectionCard primitive candidates, 18 spinner palette variations,
  two tab-strip implementations, Sleep InsightStatusCard miss, admin
  feedback-inbox card list parity.
- Vendor-label exemption pointer to `docs/audit/v1427-summary.md`.
- Wider 1,664-row partial-translation residue with the namespace
  prioritisation, plus three drift-guard tightening proposals.
- 14 orphan exports + stale Vitest mock + 2 cosmetic comments from
  the dead-code scan.
2026-05-15 21:09:09 +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 9266e405ca fix(i18n): translate the GLP-1 dashboard tile copy across FR/ES/IT/PL
`dashboard.glp1.lastInjection`, `nextInjection`, `weightDelta`, and
`inDays` were translated to DE only in the v1.4.27 dashboard rebuild
— the FR/ES/IT/PL bundles still held the EN strings verbatim and
rendered "Last injection" / "Next injection" / "since start" / "in
{count} days" on the live tile. Translate all four keys into native
phrasings:

- FR: `Dernière injection` / `Prochaine injection` / `depuis le début` /
  `dans {count} jours`
- ES: `Última inyección` / `Próxima inyección` / `desde el inicio` /
  `en {count} días`
- IT: `Ultima iniezione` / `Prossima iniezione` / `dall'inizio` /
  `tra {count} giorni`
- PL: `Ostatnia iniekcja` / `Następna iniekcja` / `od początku` /
  `za {count} dni`

The other seven keys in the `dashboard.glp1.*` namespace already ship
correctly across every locale, so this closes a copy-paste residue
rather than a missing namespace.
2026-05-15 21:04:34 +02:00
Marc-André Bombeck b1b0b1b9b1 feat(insights): add a Coach launch fallback to every empty-state sub-page and lift the CTA size
Every Insights sub-page's empty branch surfaced a single "Add
measurement" CTA and stopped there — a user with zero data had no
path to the assistant who can explain why the missing reading
matters. Mount CoachLaunchButton below the primary CTA with a
metric-specific prefill so the secondary fallback is one tap away,
and pair the change with the ctaSize="lg" prop that EmptyState picked
up in MB7 (CF-36) but never had a consumer wire — both findings hit
the same files so they fold cleanly into one change.

Covers blutdruck, gewicht, puls, stimmung, medikamente, bmi (both
the no-weight and no-height branches), and schlaf.
2026-05-15 21:04:13 +02:00
Marc-André Bombeck 815f31a321 fix(i18n): restore interpolation spacing on pagination and counter keys across FR/ES/IT/PL
The earlier translation pass dropped the surrounding spaces around the
connector word on ten counter / pagination keys — the result was
`Page 2 de10` / `Page 2 z10` rendering on every FR/ES/IT/PL
`pageInfo`-shaped surface plus the admin pagination, onboarding tour
step pill, getting-started progress chip, target consistency chips,
and target weekly summary headline. Restore the spaces and translate
the surrounding nouns natively at the same pass:

- pageInfo (measurements / mood / medications), `admin.section.auditLog.pageOf`
  → FR `Page {page} sur {total}`, ES `Página {page} de {total}`,
  IT `Pagina {page} di {total}`, PL `Strona {page} z {total}`.
- `admin.showingEntries`, `onboarding.tour.stepOf`, `gettingStarted.progress`,
  `targets.consistency.daysInRange`, `targets.consistency.daysLogged`,
  `targets.summary.weekTitle` — translated in full with native verb forms
  and balanced placeholder whitespace.

The drift-guard test stays green; a follow-up tightening to assert
`[A-Za-zÀ-ÿ]\{|\}[A-Za-zÀ-ÿ]` returns no match across the bundle would
have caught the original drift and is queued for the next i18n round.
2026-05-15 21:03:32 +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 374b410f46 docs(about): document the deliberate no-TOC call for the short-form page
The R4 design review flagged /about as missing the TOC the /privacy
page carries. /about is two sections (Project + Credits) and fits the
fold on every viewport, so a collapsible TOC costs a tap to expand
for negligible payoff. Pin the decision in the file header so a
future reader does not re-open the question on a future polish
sweep.
2026-05-15 21:00:30 +02:00
Marc-André Bombeck 25e63fcf02 fix(ui): widen scroll-mt on about and privacy anchors so the sticky header does not occlude the section start
Both pages anchored their sections at scroll-mt-20 (80 px). The sticky
header pads safe-area-inset-top (~47 px on a notch device) plus py-3
plus an inner min-h-11 link, landing around 115 px — past the 80 px
offset. Lifting to scroll-mt-28 (112 px) clears the header on every
viewport we ship to without over-padding the desktop case.
2026-05-15 20:59:52 +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 21a297adf8 fix(ui): seed useIsMobile from an SSR-safe snapshot to avoid the first-paint desktop flash
The previous useEffect-driven flavour returned false on the server and
on the first client paint, then flipped on the effect tick.
ResponsiveSheet and CoachDrawer branch their render tree off the value,
so a phone-class viewport painted the desktop branch for one frame
before swapping — Coach drawer side flipped from right to bottom
between paints and sheet contents re-mounted through the wrong layout.

Switch to useSyncExternalStore with a matchMedia client snapshot. The
server snapshot stays false (no window present) so SSR markup remains
deterministic, but the very first client render reads the live media
query state synchronously — by the time either consumer opens its
tree the hook already reflects the runtime viewport.
2026-05-15 20:58:28 +02:00
Marc-André Bombeck 7850976b99 chore(i18n): replace remaining assistant-product references with neutral terms across six locales
Rewrite two strings that still leaked the forbidden assistant-product
vocabulary across every locale bundle:

- `account.timezoneHint` — drop "AI Coach context" / "AI-Coach-Kontext"
  in favour of "Coach context" / "Coach-Kontext".
- `i18n.maintainershipBanner.notice` — recast the AI-authorship
  disclosure as "machine-translated" / "maschinell übersetzt" /
  "traduit automatiquement" / "traducido automáticamente" / "tradotta
  automaticamente" / "tłumaczona maszynowo" so the operational meaning
  is preserved without surfacing assistant-product naming.

Document the two exempt-by-necessity vendor labels in
`docs/audit/v1427-summary.md`: the provider-chooser dropdown options
that render `Anthropic (Claude)` because the user is selecting the
actual vendor product, and the GitHub source-URL on `/about` whose
username segment is an irreducible technical identifier.
2026-05-15 20:57:47 +02:00
Marc-André Bombeck 7dc51a2177 fix(workouts): correct pagination under canonical dedup
Reading rows with skip/take and then deduping inside the window
double-counted cluster boundaries: a cluster split across two pages
would be dropped on page N-1 and surface again on page N, while
meta.total reflected the per-window canonical count rather than the
user's true canonical total.

Pull the full filtered set, dedupe once, then slice. Workout volume
for a single user stays well within budget for an unbounded read; if
the workload ever grows past that, switch to a (startedAt, id) cursor
with a deterministic over-fetch window. A new test case exercises a
window with eight twin clusters split across two pages to pin the
contract.
2026-05-15 20:57:13 +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 47d2071938 fix(ci): wire MAXMIND_LICENSE_KEY into the build workflow and fail fast on missing secret
Add a `Fetch GeoLite2 databases` step ahead of the buildx build-push so
the offline GeoLite2-City + GeoLite2-ASN MMDB files land in
`assets/geolite2/` before the Dockerfile COPY at line 78 reads the
directory. The step exports `MAXMIND_LICENSE_KEY` from the repo secrets
and fails the workflow with a clear error when the secret is unset,
matching the Coolify auto-deploy gate shape from v1.4.26 P6-3 so the
missing-secret state can no longer ship as a silent no-op offline tier.
2026-05-15 20:53:23 +02:00
Marc-André Bombeck 7b342030b7 docs(planning): R4 dead-code scan report for v1.4.27 2026-05-15 20:47:00 +02:00
Marc-André Bombeck 5256ecdbac docs(planning): R4 UI-conformity sweep report for v1.4.27 2026-05-15 20:46:53 +02:00
Marc-André Bombeck f96c998062 docs(planning): R4 design review report for v1.4.27 2026-05-15 20:46:22 +02:00
Marc-André Bombeck 3deb76cf71 docs(planning): R4 product-lead review report for v1.4.27 2026-05-15 20:44:10 +02:00
Marc-André Bombeck 9fce43837f docs(planning): R4 senior-dev review report for v1.4.27 2026-05-15 20:43:49 +02:00
Marc-André Bombeck dc23d40446 docs(planning): R4 simplifier review report for v1.4.27 2026-05-15 20:42:46 +02:00
Marc-André Bombeck b0b1e65b61 docs(planning): R4 security review report for v1.4.27 2026-05-15 20:42:15 +02:00
Marc-André Bombeck 018a97c9a9 docs(planning): R4 code review report for v1.4.27 2026-05-15 20:41:53 +02:00
Marc-André Bombeck 4f60428ce6 docs(planning): R4 i18n runtime probe report for v1.4.27 2026-05-15 20:40:37 +02:00
Marc-André Bombeck 617d451864 fix(tests): flip the insights-coach mount assertions to the new layout owner
MB4 promoted the Coach drawer mount from the mother page up to
`/insights/layout.tsx` so navigating into a routed sub-page no longer
unmounts the drawer. The launch lifecycle now lives in the
`<CoachLaunchProvider>` context that the layout supplies; the page
consumes it via `useCoachLaunch`.

The three insights-polish assertions still pinned the old contract
(import + mount in the page, local `setCoachOpen` / `setCoachPrefill`
state). Re-pointed them at the new owner:

- layout imports the launch context, wraps children in
  `<CoachLaunchProvider>`, and mounts `<LayoutCoachMount>`
- page's hero strip still wires `onAskCoach` / `onPickPrompt` but now
  delegates to `coachLaunch.askCoach(prefill)`
- the local `setCoachOpen` / `setCoachPrefill` pair is gone
2026-05-15 19:28:04 +02:00
Marc-André Bombeck 4d35f6c46c docs(planning): R3d MB5 tables-cardlist bucket report 2026-05-15 19:26:20 +02:00
Marc-André Bombeck 5848533500 docs(planning): R3d MB7 surface-specific polish residue bucket report
11 commits across ~25 files; every Critical / High CF in MB7 plus
every Medium ≤ M plus every same-file Low landed. Report enumerates
each CF-ID's commit, the file-touch list, and the docs collision
matrix follow-through.
2026-05-15 19:25:07 +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 f8b407da56 docs(planning): R3d MB6 regression and auth bucket report 2026-05-15 19:10:54 +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 e52b64bf34 fix(insights): point the empty-state CTAs at the existing measurements route
The B4 insights empty-state CTAs (blutdruck, gewicht, puls, bmi)
linked to `/measurements/new`, a route that does not exist; a tap
landed on a Next.js 404. Re-route via `/measurements?add=<TYPE>`,
the parameter the measurements page now consumes. Each CTA picks a
sensible default: BLOOD_PRESSURE for the BP page, WEIGHT for the
weight and BMI pages, PULSE for the pulse page. The mood, sleep,
and medication empty-states already pointed at their own dedicated
routes and need no change.

Add a `?add=<TYPE>` consumer to the measurements page. The consumer
opens the add-measurement dialog with the right `defaultType` on
first render, then `router.replace` strips the param so the back
button returns to a clean `/measurements`. The allow-list mirrors
MEASUREMENT_TYPES inside `measurement-form.tsx` so a stale link
cannot trap the user on an unknown form. The render-driven set-state
pattern follows account-section.tsx and satisfies the
`react-hooks/set-state-in-effect` rule.
2026-05-15 19:09:34 +02:00
Marc-André Bombeck 7ad894b535 docs(planning): R3d MB4 coach reachability bucket report 2026-05-15 19:08:48 +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