Commit Graph
1563 Commits
Author SHA1 Message Date
Marc-André Bombeck 64186bd8de test(e2e): update settings touch-target assertions to the v1.4.34.5 44 px floor
The v1.4.34.5 iOS-textarea-zoom sweep lifted the mobile input
height floor from 40 px (`h-10`) to 44 px (`h-11`) on `Input`,
`Select` trigger, and the native `<select>` primitives — the
desktop tier still resolves to 40 px via `sm:h-10`. The
settings-mobile-consistency spec still pinned every form input,
the Compare-to trigger, and every `/settings/ai` native select to
40 px, so it went red on the v1.4.36 tag while the implementation
was correct.

Bump the three height assertions to 44 px and refresh the header
docblock so a future maintainer reads the contract as "44 on
mobile, 40 on >=sm" instead of stumbling on a stale baseline.
2026-05-17 14:52:48 +02:00
Marc-André Bombeck 1883e1e8c1 test(integration): isolate apns-dispatch + integration-status from shared mock state
The integration suite used to share a single module graph across
files (`pool: forks`, `fileParallelism: false`, `isolate: false`).
That kept the container boot amortised but meant a `vi.mock()`
declaration in one file lost to whichever earlier file had already
pulled the real module into the graph. `apns-dispatch.test.ts` and
`integration-status.test.ts` both fall through to
`@/lib/notifications/dispatcher` (and its `@parse/node-apn` /
telegram sender deps); when a sibling test mounted the real
dispatcher first, the mock factories registered later silently
no-op'd and the in-suite run reported the mock as called zero
times. Each file still passed in isolation, which is the
fingerprint the v1.4.35.1 closure already flagged as pre-existing.

Flip the integration config to `isolate: true`. Per-file module
isolation costs ~10 s of import rebuild across the 56-file run;
the Postgres testcontainer + migrations live in `globalSetup`, so
the slow part (container boot, ~5-15 s) is unaffected. The two
`withings-*-sync.test.ts` files were previously dependent on a
sibling setting `process.env.ENCRYPTION_KEY` before them — pin the
deterministic test key in each file so they remain self-contained
under per-file isolation. Locally `pnpm test:integration` now
shows the four target failures green; the remaining two
bp-in-target flakes are pre-existing local-only data drift that
passed on CI under the old isolation flag and continue to be
ordering-independent.
2026-05-17 14:52:39 +02:00
Marc-André Bombeck 6ad5eb25e9 test(insights): raise timeout on the 5 MB payload guard test
The features-payload-too-large guard constructs a ~5.6 MB rollup
payload and runs the whole serialiser pipeline to confirm the
ceiling fires. Local hardware completes the assertion well inside
the default 5 s budget, but the GitHub-Actions Ubuntu runners
crossed it and timed out on the v1.4.36 push. Pin a 30 s budget on
this single case (Vitest 4's two-arg `it(name, opts, fn)` form,
the deprecated three-arg form was removed) and leave the rest of
the suite on the default; the runtime is dominated by the giant
allocation, so the wider window only matters for the slowest
runners.
2026-05-17 14:52:22 +02:00
Marc-André Bombeck e33bb41c6d docs(planning): W6 wave report for the v1.4.37 settings + misc fixes
Per-commit list, tests delta, code-review pass, brief-back —
documents the W6 outcome (timezone-override removal, BMI status
timeout-sentinel + skeleton, cf-connecting-ip branch, geo-backfill
schedule). Notes the three-way race-condition with adjacent waves
that landed W6 file content under neighbouring commit messages.
2026-05-17 14:45:20 +02:00
Marc-André Bombeck 0280369952 refactor(dashboard): self-review polish on the medication-intake quick-add
Two findings from the post-implementation self-review:

  - Empty-state CTA: swap the plain anchor for next/link so the
    /medications jump stays inside the SPA router instead of triggering
    a full page reload.
  - JSDoc accuracy: the original module-level comment claimed a
    non-default dose 'surfaces as a sonner toast hint' — neither the
    toast nor a notes-style append actually exists. Rewrite the dose
    paragraph to honestly describe the contract: the dose input is
    pre-filled for visual confirmation, editable, but the POST body
    never carries the dose override. The intake event always
    references the medication's catalogue dose; a half/double dose
    still routes through the medication edit on /medications.
2026-05-17 14:44:03 +02:00
Marc-André Bombeck a6bfe57571 test(dashboard): pin medication-intake quick-add submit contract
Add the SSR-level suite for the new quick-add: empty-state markup, the
populated form's field testids plus 44 px touch floors, the pure
default-selection heuristic across the four ranked branches
(empty / single-active / due-now / alphabetical fallback), and the
no-raw-key locale check for English and German.

Extend the existing quick-add-labels guard so it covers the third
DropdownMenu row alongside the original two — the icon is decorative
and the trigger label sits above the menu, so the three visible
labels must stay distinct from each other and from the trigger.
2026-05-17 14:42:48 +02:00
Marc-André Bombeck c42a62d03f feat(i18n): medication-intake quick-add labels across six locales
Add the menu-item label (dashboard.quickAddMedicationIntake) and the
dashboard.medicationIntakeQuickAdd block (sheet title, sheet
description, field labels, hint, error message, and empty-state copy)
to all six locales. The German bundle ships Marc-Voice copy with full
UTF-8 umlauts; the other locales mirror the English fallback already
in use for the other two quick-add rows until the localisation pass.
2026-05-17 14:42:40 +02:00
Marc-André Bombeck 64a7408e7d feat(dashboard): wire medication-intake action into the Hinzufuegen menu
Add the third row to the dashboard quick-add DropdownMenu so a
medication-intake event can be logged without leaving the page. The new
ResponsiveSheet shares the same footer-portal contract as the existing
Measurement and Mood sheets so the Save and Cancel buttons sticky-pin
above the mobile soft keyboard, and the menu label is a self-contained
verb-phrase ('Medikamenteneinnahme erfassen' / 'Log medication intake')
that doesn't collide with the trigger or the sibling rows.
2026-05-17 14:42:33 +02:00
Marc-André Bombeck f9dab34fed feat(dashboard): medication-intake quick-add overlay
Mirror the ResponsiveSheet primitive used by the Measurement and Mood
quick-adds for medication intake. The sheet auto-selects the active
medication whose schedule window is currently open (or the only active
medication when the user has just one), pre-fills the dose from the
catalogue entry, and defaults the time taken to now. On submit the
form invalidates medicationDependentKeys plus the inline per-medication
compliance chart cache so the detail tile, the dashboard compliance
chart, the today list, and the achievements card all refresh in lockstep
with the API. An empty-state branch suppresses the form and surfaces a
CTA to /medications when the user hasn't yet created a medication.
2026-05-17 14:42:25 +02:00
Marc-André Bombeck d3c9eaaf48 chore(queue): schedule geo-backfill via pg-boss on the :40 hourly slot
The `runGeoBackfill` helper has shipped since v1.4.27 but was never
wired into the worker boot — it only ever fired through a test path,
so the admin sign-in overview's "Standort" column kept rendering
"—" on every historical row where the fire-and-forget IP resolver
missed at write time. Marc's report against v1.4.36 made the
problem visible: even after the v1.4.36 fallback fix, the backfill
was missing entirely.

Wire the helper into the existing pg-boss schedule list at :40
every hour. The queue name + cron expression live in the helper
module so the worker boot pulls them by import; a co-located unit
test pins both shapes (queue name + cron slot) plus a source-text
probe that confirms the worker actually imports and registers the
runner. The helper itself is unchanged — it stays idempotent, capped
at 5 000 rows per pass, and scoped to the trailing 30-day window
that the operational triage cares about.

The :40 slot was chosen to dodge the existing :00 (Withings activity
+ measure crons), :15 (Withings sleep), :30 (moodlog sync), and
:25 (medication compliance status) crowd at the top of the hour;
the helper's per-call cap keeps the audit-log write budget under
control even if a future ingest path enqueues a hot run.
2026-05-17 14:42:09 +02:00
Marc-André Bombeck c824488895 docs(planning): W7c step-consolidation wave report 2026-05-17 14:39:47 +02:00
Marc-André Bombeck bca5298049 docs(planning): W4b wave report for the v1.4.37 medication-card symmetry fix
Per-commit summary, file ownership, tests delta, self-review findings
and applied resolutions, visual diff between Ramipril and Mounjaro
post-fix, and the GLP-1-specific rotation-hint prominence judgement.
2026-05-17 14:38:51 +02:00
Marc-André Bombeck c7480e90e3 fix(measurements): tighten W7c grouped-view pagination and timezone comment
Self-review polish on top of the v1.4.37 W7c step-consolidation
landing:

- The collapsed daily view returns up to 5000 synthesised rows in
  one shot, so the legacy 25-row pagination would have re-fired the
  same scan with offset=0 returning the same slice. Disable
  pagination on the day-grouped path; per-sample lists keep their
  PAGE_SIZE=25 behaviour.
- Bump the per-page row scan from 1250 to the server max of 5000 so
  even chatty pre-drain Apple-Watch accounts (~200 chunks/day on
  busy days) get a useful number of daily rows on the first paint.
  Post-drain the scan is trivially short (each day reduces to one
  stats: row).
- Reset offset=0 explicitly on the grouped path so a sort-direction
  flip never paginates into an empty server slice.
- Clarify the day-boundary comment in the dayKey drill-down branch
  — the ±12h around canonical noon shift works for every IANA
  offset, including sub-half-hour zones (Asia/Kathmandu +5:45,
  Pacific/Chatham +12:45).
2026-05-17 14:38:44 +02:00
Marc-André Bombeck 1a61d25bdf style(medications): blank line between imports and JSDoc on glp1 card
Minor formatting cleanup left over from the v1.4.37 W4b symmetry work.
2026-05-17 14:37:53 +02:00
Marc-André Bombeck a43f704690 feat(audit): cf-connecting-ip branch in getClientIp behind TRUST_CF_CONNECTING_IP
The Coolify-fronted HealthLog stack sits behind Cloudflare, which
strips and re-sets `cf-connecting-ip` on every request that lands
on its edge. Without consulting this header, `getClientIp` reads
the Caddy loopback IP off the `X-Forwarded-For` chain and the
admin sign-in geo lookup ends up with a null IP for every visitor
— Marc's "Standort" column rendered a literal "—" on every row of
the sign-in overview even after v1.4.36 closed the missing-ASN
fallback.

The new branch prefers `cf-connecting-ip` ahead of the XFF / x-real-ip
chain, but only when the operator opts in via `TRUST_CF_CONNECTING_IP=1`.
A self-hosted deployment without Cloudflare in front MUST leave the
flag off — an attacker can set the header directly on the public
internet, and the downstream geo resolver would happily report a
forged location.

The trust-violation accounting in `getClientIpOrTrustWarning` skips
when the CF header satisfies the lookup because Cloudflare re-sets
the value on every edge request — there is no chain to violate. The
existing XFF + x-real-ip semantics are unchanged for every
deployment that leaves the flag unset.

`.env.example` documents the flag in a dedicated "Reverse-proxy
trust" block right above the process-split section so the operator
finds it next to the existing infra-trust controls.
2026-05-17 14:37:48 +02:00
Marc-André Bombeck 00e2515849 test(medications): pin Ramipril / Mounjaro detail-card symmetry
Side-by-side contract suite for the v1.4.37 W4b symmetry fixes from
Marc's UX audit (item 11). Asserts the load-bearing structural seams
both medication-card variants must share:

* History + Pencil header buttons render on both cards.
* No kebab on either card when no overflow prop is wired (Mounjaro
  matches Ramipril in the common case).
* Kebab only mounts on Mounjaro when `onLogSideEffect` is supplied;
  no equivalent surface on Ramipril.
* Category-label badge uses the shared category-map lookup on both
  cards (Mounjaro no longer hard-codes the treatment-class label).
* Take-now coloured pill renders on both cards when inside the
  current schedule window.
* Primary-actions row carries the canonical two-button shape — the
  GLP-1-specific Stethoscope no longer leaks into this row.
* Purple `text-purple-400` accent on the upcoming schedule dose
  surfaces uniformly across both card kinds.
2026-05-17 14:35:42 +02:00
Marc-André Bombeck bc95e8fcba docs(planning): W2 wave report for the v1.4.37 analytics rollup lift 2026-05-17 14:35:18 +02:00
Marc-André Bombeck 7032cb1b76 refactor(analytics): rename ninetyFiveDaysAgo + document tz day-key 2026-05-17 14:33:58 +02:00
Marc-André Bombeck aed890c9bb test(measurements): pin groupBy=day, dayKey, and drain cutoff contracts
Four new test files cover the v1.4.37 W7c surface:

* group-by-day.test.ts (route) — collapses per-sample step rows into
  one row per user-TZ day with sampleCount; gates on
  CUMULATIVE_HK_TYPES so non-cumulative types keep the legacy
  findMany behaviour; resolves the dayKey drill-down window via
  measuredAt: { gte, lt }; rejects malformed dayKey at the
  validation layer.

* measurement-list-step-grouping.test.tsx (UI) — renders the
  collapsed daily total with the localised sample-count caption;
  exposes a chevron-expand button with aria-expanded; suppresses
  the edit/delete actions on the synthesised daily row.

* drain-per-sample-cumulative.test.ts (helper) — passes a
  measuredAt cutoff into the per-sample scan when cutoffHours is
  set; falls back to the all-rows behaviour for CLI/admin callers
  that omit the option.

* drain-cumulative-queue.test.ts (scheduler) — pins the queue name,
  cron cadence (03:45 Europe/Berlin), 36 h cutoff constant, and
  pg-boss handler wiring.
2026-05-17 14:33:11 +02:00
Marc-André Bombeck 2987b8c99d perf(analytics): tighten correlations window to 28d + best-effort sentinel
Self-review polish on top of the W2 fast-path landings:

- Rename the misleading `ninetyFiveDaysAgo` local in
  bp-in-target-fast-path to `readSince` so the 396-day BP rollup
  read window does not read like a 95-day shortcut.
- Document the rollup-bucket-vs-display-tz day-key shift in
  correlations-fast-path. The DAY bucket anchors at UTC midnight; for
  the Berlin-centric production tenant that lands on the same local
  calendar day, so the rollup-fast-path's per-day pair shape matches
  the live path within the n >= 20 surface gate. A v1.5 follow-up can
  mint per-user-tz buckets if a non-Berlin account needs strict
  parity.
2026-05-17 14:33:00 +02:00
Marc-André Bombeck 675c084baa ui(medications): move GLP-1 side-effect button into the card header actions overflow
The GLP-1 card carried a third Stethoscope button in the primary
actions row (Eingenommen / Übersprungen / Nebenwirkung), which broke
the two-button symmetry the generic medication card establishes.
Marc reported the asymmetry during the v1.4.37 UX audit (audit
item 11): "ich möchte natürlich, dass das genau gleich ist".

Move the side-effect quick-log into a kebab-style overflow in the
header `actions` slot. The kebab only mounts when the parent wires
`onLogSideEffect`, so Mounjaro and Ramipril cards stay byte-equivalent
in the common case (history + edit, no kebab). When the side-effect
hand-off is wired, Mounjaro gains the kebab while the primary actions
row stays the canonical two-button shape.

The localised label re-uses the existing `common.moreOptions` key for
the trigger aria-label and the existing `medications.glp1LogSideEffect`
key for the menu item, so no new i18n strings are required.
2026-05-17 14:32:40 +02:00
Marc-André Bombeck 2327332206 test(coach): invariant — every Coach-bearing surface respects the global flag
Marc directive: when the operator turns the global Coach flag off,
every Coach affordance on every surface must vanish — no grey-out,
no error, no inert button, no DOM trace. The recent v1.4.37 W5
hotfixes closed three holes the UX audit caught (HeroStrip action
button, SuggestedPrompts chip strip, /targets CoachDrawer mount); a
follow-up audit item asked for an automated invariant so the gate
contract can't regress.

Walk every Coach-bearing web surface (HeroStrip action row,
SuggestedPrompts chips, CoachLaunchButton inline pill, LayoutCoachFab
mobile sticky, LayoutCoachMount drawer subtree) under a QueryClient
pre-seeded with `coach: false` and assert each one paints nothing
measurable in the SSR output. The fixture maintains a known surface
list; a trailing count check forces contributors to revisit the
fixture when a new Coach-bearing component lands.
2026-05-17 14:30:34 +02:00
Marc-André Bombeck 511d29ab29 fix(medications): purple dose accent shared across all medication kinds
The generic medication card surfaces the upcoming schedule dose with
a `font-medium text-purple-400` accent so titration overrides on the
schedule visually pop out of the muted next-intake line. The GLP-1
card omitted the accent entirely.

Mirror the same accent on the GLP-1 card so the only difference
between Mounjaro and Ramipril detail surfaces is the data being
shown, not the styling treatment. The schedule.dose still rides
inside the existing next-injection paragraph (hidden below sm: like
the generic card so the narrow-viewport row stays one line).
2026-05-17 14:29:45 +02:00
Marc-André Bombeck a1f61bb29b chore(queue): schedule nightly drainPerSampleCumulative with 36h cutoff
Registers a new drain-per-sample-cumulative pg-boss queue + cron
(03:45 Europe/Berlin, slotting between the audit-log cleanup at
03:15 and the feedback aggregator at 04:00). The handler invokes
drainPerSampleCumulative across every user with cutoffHours=36 so
today + the trailing watch-sync window stay per-sample while
completed-and-stable days collapse into one stats: row per
calendar day per cumulative HK type.

Replaces the manual CLI + admin-endpoint pattern from v1.4.30 for
the recurring case (the operator-driven paths remain for one-shot
backfills) and avoids the prod-image tsx + Prisma symlink hack
documented in feedback_prod_image_tsx_prisma_gap.md.
2026-05-17 14:29:23 +02:00
Marc-André Bombeck 9a08ad66d1 feat(measurements): cutoffHours grace window for drainPerSampleCumulative
The drain previously collapsed every per-sample APPLE_HEALTH cumulative
row it could see, including today's still-in-flight watch syncs. When
called from the scheduled nightly job we want to leave a grace window
around now so the list view keeps surfacing late watch reconciliations
in real time before the day stabilises.

cutoffHours adds an optional window: rows newer than now() -
cutoffHours are excluded from the scan. CLI + admin-endpoint callers
keep the previous all-rows behaviour by omitting the option.
2026-05-17 14:29:15 +02:00
Marc-André Bombeck 4e336f93df feat(measurements): collapsed list view with day drill-down chevron
When the type filter is a cumulative HK type (steps, active energy,
distance, flights, daylight) the measurements list now opts into the
route's groupBy=day mode and renders one row per user-TZ day with
the day's total + a sampleCount caption. A chevron in the row
action column lazy-fetches the day's per-sample chunks via a second
query against ?dayKey=YYYY-MM-DD and inlines them beneath the row
so power users can still answer wann wurden Schritte gemeldet
without scrolling past 200 step rows on the default view.

Mirrors the universal Apple-Health / Garmin / Withings pattern
captured in the v1.4.37 research brief: one number per day as the
primary surface, drill-down to the chunks behind it.
2026-05-17 14:29:06 +02:00
Marc-André Bombeck f6008e762b feat(measurements): collapsed list and per-day drill-down for cumulative types
GET /api/measurements gains two new branches:

* groupBy=day + cumulative type → returns one synthesised row per
  user-TZ day with value=SUM, sampleCount, and a dayKey anchor so
  the list view paints one row per day instead of hundreds of
  per-sample chunks from Apple Health.

* dayKey=YYYY-MM-DD + cumulative type → returns the per-sample rows
  for that single calendar day in the user's IANA timezone, so the
  expand chevron in the row can lazy-fetch the chunks that summed
  into the daily total.

Both branches gate on CUMULATIVE_HK_TYPES so the iOS contract for
spot metrics (BP, weight, pulse, BG) stays untouched. The collapsed
row's measuredAt anchors to local noon to match the
canonicalDailyTimestamp convention used by the drain helper.
2026-05-17 14:28:41 +02:00
Marc-André Bombeck 2e929d0afb fix(medications): take-now / overdue pill on the GLP-1 card too
The generic medication card painted a coloured take-now / overdue /
very-overdue pill at the top of the detail surface; the GLP-1 card
silently omitted it. Marc reported the asymmetry during the v1.4.37
UX audit (audit item 11): Mounjaro and Ramipril cards felt "different"
even though both should read as the same row shape.

Wire the shared `reduceCurrentWindowStatus` reducer into the GLP-1
card, fetch the same reminder thresholds as the generic card, and
re-render once a minute so the pill tracks wall-clock progress without
a route reload. The pill renders only when a schedule is currently
in/past its window and the user has not yet covered every overdue
schedule with today's intake events — same suppression rules as the
generic card.
2026-05-17 14:27:46 +02:00
Marc-André Bombeck 6fc9c99009 test(insights): pin HealthScoreCard full-height contract
Add a render assertion that the inner column ships with grid +
flex-1 + grid-rows-[auto_auto_auto_auto_auto_1fr_auto] so a future
refactor can't silently revert to a flex stack and re-introduce
the v1.4.36 "Karte hört bei der Trennlinie auf" report. The
mt-auto + h-full + flex-col + flex assertions stay in place
alongside the new grid contract — they describe complementary
load-bearing classes.
2026-05-17 14:27:13 +02:00
Marc-André Bombeck 85590ad579 test(targets): pin coach CTA disappears when global flag is off
Mock useFeatureFlags so the suite can drive both the flag-on (CTA
mounts) and flag-off (CTA hides) branches alongside the existing
aiEnabled gate test. Both branches share the all-on default unless
the case overrides it with mockReturnValueOnce, so the rest of the
suite continues to render with the default-mock flag set.
2026-05-17 14:27:03 +02:00
Marc-André Bombeck f88780a06c fix(coach): gate SuggestedPrompts chips on the global Coach flag
Every chip in <SuggestedPrompts> is a Coach affordance — clicking one
seeds the Coach drawer's composer with the localised prompt and opens
the drawer. The strip rendered unconditionally until now, so a
deployment with the operator's global Coach flag off still painted
five inert chips below the hero band.

Read flags.coach inside the component and short-circuit when the flag
is off. The HeroStrip caller already wraps the mount in the same
guard (W5 commit prior); this is defence-in-depth so a future caller
that mounts <SuggestedPrompts> outside the hero band can never leak a
Coach surface.
2026-05-17 14:27:00 +02:00
Marc-André Bombeck 4f7f8e7a83 fix(targets): gate TargetCard coach CTA on global Coach flag
Read useFeatureFlags() inside TargetCard and pair the existing
aiEnabled guard with flags.coach so the per-card Coach CTA hides
entirely when the operator disables the Coach feature globally.
Pre-fix the CTA mounted whenever the user-level provider chain was
configured, even when the operator-level kill-switch was off; the
disable cascade is supposed to make every Coach surface disappear,
not just the hero-strip button. Sister sites (hero-strip,
suggested-prompts, /targets CoachDrawer) belong to W5.
2026-05-17 14:26:57 +02:00
Marc-André Bombeck 002426170d test(export): pin Arztbericht hero render + CTA contract
Adds an SSR-only contract suite for `<ArztberichtHeroCard>` covering
the hero slot, value statement, CTA touch-target floor, the
`aria-describedby` link from the CTA to the value statement, the i18n
key resolution path (DE + EN), the hero-gradient + glow-purple visual
treatment, and the closed-state pass that keeps the dialog markup out
of the SSR output until the CTA opens it.

Updates the existing `<ExportSection>` smoke suite for the v1.4.37 W7a
shape: four secondary cards in the grid (not five), the hero mounted
above the grid, and the "Other export options" sub-heading.
2026-05-17 14:26:50 +02:00
Marc-André Bombeck 0af77230c8 feat(export): promote Arztbericht hero card and shell on settings export
The doctor-report PDF is HealthLog's flagship "data out" path. The
v1.4.16 phase B7 layout treated it as one of five equal cards in the
grid; this change lifts it into a hero card at the top of
`/settings/export`. The remaining four export cards (Measurements CSV,
Medications CSV, Mood CSV, Full JSON backup) move under a dedicated
"Weitere Export-Optionen" / "Other export options" sub-heading.

The hero reuses the `hero-gradient + glow-purple` visual treatment from
the Insights hero strip, so the surface feels consistent across the
app. The CTA routes through the existing `<DoctorReportDialog>` +
`/api/doctor-report` flow — only the framing changed. Mobile-first: at
< sm the CTA falls onto its own row and clears the 44 px WCAG touch
floor (`h-11 sm:h-10`); the button carries an `aria-describedby` link
to the value statement so screen readers announce the framing
alongside the action label.
2026-05-17 14:26:24 +02:00
Marc-André Bombeck 5a6f921928 fix(targets): gate CoachDrawer on /targets behind the Coach flag
/targets carries two Coach affordances — the per-card 'Ask Coach' CTA
on every TargetCard and the page-level CoachDrawer mount. The CTAs
were gated through `aiEnabled` (provider chain status), but the
drawer mount always rendered regardless of the operator's global
Coach flag. When the operator turned Coach off in Settings the drawer
still loaded its SSE machinery + Sheet portal subtree even though no
on-page CTA could open it.

Read flags.coach via useFeatureFlags and fold it into the existing
`aiEnabled` gate so the per-card CTAs vanish when either layer is
off. Wrap the page-level <CoachDrawer> mount in the same gate as
defence-in-depth, since the CTAs already hide when the flag is off.

Tests: add two new cases to targets-coach-mount.test.tsx covering the
Coach-flag-off branch (CTAs hide + drawer mount disappears). The
sibling targets-* test files mock @tanstack/react-query without
exposing QueryClientContext; add a use-feature-flags mock that
short-circuits to the all-on default so the existing assertions keep
running against the visible page shape.
2026-05-17 14:25:57 +02:00
Marc-André Bombeck b643baed82 fix(medications): category label uses the category-map lookup uniformly
The generic medication card mapped `medication.category` to one of the
localised category labels via an inline lookup table, while the GLP-1
card hard-coded `medications.treatmentClassGlp1` into the same slot.
The asymmetry made Mounjaro and Ramipril cards visually diverge on the
medications list even though both should read the same row shape.

Lift the lookup into `src/lib/medications/category-label.ts` and use it
from both card variants. The GLP-1 nature of a medication is already
implied by the rotation hint + injection-metadata rows below the
header, so the category badge now carries the real classification
(BLOOD_PRESSURE, HORMONE, OTHER, ...) on both cards.

Tests pinned the old contract; they now assert the new shared label
shape for both English and German locales.
2026-05-17 14:25:49 +02:00
Marc-André Bombeck d0ba800d9c ui(dashboard): centre-align Hinzufuegen button on mobile
Switch the title-row flex from items-start to items-center
sm:items-start. The dashboard.title plus welcomeText sub-line
wraps to two lines on < 380 px viewports and the button used to
hug the top of that 2-line block without a baseline anchor.
items-center on mobile centres the button against the title; sm+
returns to the original top-aligned posture where the title is
one line.
2026-05-17 14:25:45 +02:00
Marc-André Bombeck 3645fe151c fix(insights): Mood mini chart wrapper height matches HealthChart
Collapse the Card's inter-row gap to 0 and drop the CardHeader
inner gap to 0 so the only space between the title and the chart
strip is the CardHeader's own pb-1 (4 px). HealthChart mini paints
a plain div with mb-1 under its title; Mood's pre-fix gap-1
(4 px) on top of the header pb-1 stacked an extra 4 px below the
title, leaving Mood ~12-16 px taller than the BP/weight siblings
inside the trends row.
2026-05-17 14:24:42 +02:00
Marc-André Bombeck 49cac49e4d feat(export): promote Arztbericht to hero card on settings export page
The doctor-report PDF is HealthLog's flagship "data out" path — the
artefact a user actually carries into a doctor's appointment. The
v1.4.16 phase B7 layout treated it as one of five equal cards in the
grid, which undersold the feature.

This change extracts the doctor-report flow into a new
`<ArztberichtHeroCard>` component and renders it at the top of
`/settings/export` with the same `hero-gradient + glow-purple` visual
treatment the Insights hero strip uses, so the surface feels
consistent with the rest of the app. The remaining four export cards
(Measurements CSV, Medications CSV, Mood CSV, Full JSON backup) move
under a dedicated "Weitere Export-Optionen" / "Other export options"
sub-heading.

The CTA routes through the existing `<DoctorReportDialog>` +
`/api/doctor-report` flow — only the framing changed. Mobile-first:
the hero stacks at < sm with the CTA falling onto its own row; the
button clears the 44 px WCAG touch floor (`h-11 sm:h-10`) and carries
an `aria-describedby` link to the value statement so screen readers
announce both alongside the action.
2026-05-17 14:24:34 +02:00
Marc-André Bombeck d2d0cbdc3b feat(export): i18n keys for Arztbericht hero across six locales
Adds a new `settings.sections.export.hero.{eyebrow,valueStatement,cta,
formatHint}` bag plus a `settings.sections.export.otherOptionsHeading`
key across DE, EN, FR, ES, IT, PL so the doctor-report hero card and
the "Other export options" sub-heading resolve everywhere. Retires the
now-unreferenced `settings.sections.export.cards.doctorReport.action`
key — the hero owns the CTA copy from this point on.

DE + EN ship fully translated; the four AI-initial locales (FR, ES, IT,
PL) receive native translations for the hero copy in line with the rest
of their export bag.
2026-05-17 14:24:13 +02:00
Marc-André Bombeck 232c2011e6 refactor(medications): lift currentWindowStatus to shared helper
Both the generic medication card and the GLP-1 variant need to paint
the same coloured take-now / overdue / very-overdue pill, but the
schedule-window math lived inlined inside medication-card.tsx only.
Move parseTimeToMinutes, toBerlinDate, getWindowStatus,
isLastIntakeInCurrentWindow, and the reducer that picks the
most-actionable schedule into src/lib/medications/window-status.ts so
both cards can consume the same code path.

No behaviour change for the generic card. The extracted reducer
mirrors the previous inline implementation, including the
overnight-window adjustment, the late / very-late thresholds, and the
"don't show in-window if the last intake already covered the window
today" guard. The helper accepts any schedule shape that exposes
windowStart, windowEnd, and daysOfWeek, which both card variants
already satisfy.
2026-05-17 14:23:59 +02:00
Marc-André Bombeck 758f13ec98 fix(targets): tighten card gap to eliminate dead space below the header
Override the Card primitive's default gap-4 md:gap-6 with gap-3
md:gap-4 and drop the CardHeader's pb-3 to pb-0. The default
spacing stacked on top of the CardHeader padding rendered a ~36 px
dead gap between the metric label and the headline number on md+,
so the card visually split into two disconnected blocks. The
tighter rhythm pulls the value back under the label so the card
reads as one cohesive unit.
2026-05-17 14:23:45 +02:00
Marc-André Bombeck b5a605cf31 fix(insights): HealthScoreCard fills the hero-row full height
Swap the inner column from a flex stack to a 7-row grid with a 1fr
slack row on the provenance accordion. The hero strip's row already
stretches the card via items-stretch, the card already declares
h-full, and the disclaimer keeps mt-auto for the no-grid render
path. The remaining work was teaching the inner column to actually
distribute the recovered vertical slack — flex with mt-auto only
on the disclaimer parked the recovered space under the disclaimer
instead of between the body and the footer, so the visible card
ended a divider-line above the left column's bottom edge.
2026-05-17 14:23:24 +02:00
Marc-André Bombeck cc37e4bdcd perf(analytics): probe-gated healthScore on rollup-fast-path
The Personal Health Score builder used to read 37 days of raw weight
rows from the measurements table on every dashboard mount. When DAY
rollup coverage exists for WEIGHT, the new helper derives the weight
series from per-day MEAN values in measurement_rollups and only
issues a narrow 2-column projection against measurements for the
source-attribution pill (the rollup table does not carry the source
enum). Partial-coverage and brand-new accounts keep the legacy 37-day
raw read so the score stays correct on first cold-mount.

Mood, medications, and intake events still read live regardless of
coverage — none has a rollup equivalent today. A path:"rollup"|
"live" annotate on meta.healthScore.path makes the branch selection
provable in prod logs, matching the bp_in_target and summaries-slice
patterns.

Body relocated from route.ts to lib/analytics/health-score-fast-path.ts
so the dispatcher can be unit-tested against mocked Prisma without a
container. Unit test pins both branches (probe → rollup, probe →
live), the empty-component-set early-out, and the implicit-probe
shortcut for callers that omit the coverage map.
2026-05-17 14:22:59 +02:00
Marc-André Bombeck 5688a47f34 fix(insights): gate HeroStrip coach button on the global Coach flag
The hero band carries three Coach affordances — the action-row 'Ask the
coach' button, the SuggestedPrompts chip strip, and (via prop drilling)
the HealthScoreCard's onAskCoach handler. All three open the same Coach
drawer that the layout already gates on the operator's global Coach
flag. Until now they painted unconditionally; users on a Coach-disabled
deployment would click the chips or button and the drawer mount would
refuse to render, leaving the controls inert.

Read flags.coach in the hero and short-circuit each Coach surface when
the flag is off. The action row + chip strip are conditionally
rendered; the HSC's onAskCoach prop drills through as undefined so a
future re-addition of an inline HSC Coach button stays gated too.
2026-05-17 14:21:48 +02:00
Marc-André Bombeck 5f86d6d4fa ui(dropdown): keep menu items single-line + widen container to w-60
Add whitespace-nowrap to the DropdownMenuItem primitive so every
dropdown row stays on a single line app-wide, and bump the sidebar
3-dot overflow menu from w-56 to w-60. The Benachrichtigungs-Center
label (23 chars) plus its Bell icon and the default text-sm cap sat
right at the 14-rem ceiling and wrapped at the hyphen on sub-pixel
rounding.
2026-05-17 14:21:25 +02:00
Marc-André Bombeck 96e9842963 ui(select): chevron right-margin parity with date-picker icon
Split the trigger's horizontal padding (pl-3 pr-2.5) and push the
chevron 4 px further right via [&_svg:last-child]:mr-1 so the
trailing glyph sits at the same visual gutter as the browser-native
date-input calendar icon. The old uniform px-3 parked the chevron
12 px from the edge, which read visibly tighter than the date-picker
glyph when both controls shared a form row.
2026-05-17 14:20:54 +02:00
Marc-André Bombeck 89c835bc92 docs(planning): W3 wave report for the v1.4.37 intake-history fix 2026-05-17 14:20:10 +02:00
Marc-André Bombeck f33c70b3a2 style(medications): prettier sweep on the W3 intake-history fix
Run prettier --write on the four files the v1.4.37 W3 commits
touched (the schema, the route, the V2 component, and the two test
files). Drive-by also collapses two pre-existing whitespace drifts
in the InventoryItem type aliases and the glp1PostBodySchema refine
call so the file matches the formatter contract end-to-end. No
behavioural change.
2026-05-17 14:15:41 +02:00
Marc-André Bombeck 04aa2fc8b9 docs(diagrams): add five Dracula-styled architecture diagrams
Five SVG diagrams anchor the architecture story across README, the
public doc site and the landing page:

- 01-data-flow: source → ingest → store → read → surface
- 02-coach-pipeline: question → snapshot → prompt → provider chain → cited reply
- 03-self-hosting-topology: reverse proxy → app + worker → Postgres + image registry
- 04-source-priority: per-metric ladder with audit-trail kept/dropped lanes
- 05-security-model: three concentric layers plus rate-limit / audit / SSRF rails

Hand-authored SVG, no external dependencies, mirrored into the
healthlog-docs and healthlog-landing repos so each surface ships its own
copy.
2026-05-17 14:14:41 +02:00