Commit Graph
25 Commits
Author SHA1 Message Date
Marc-André Bombeck a238ae48d1 fix(design): pin acknowledgment dialog footer + i18n back-button + drop GoalsChipPicker storage block
Three small surface fixes consolidated because each is a one-touch
change on a separate file:

- `ResearchModeAcknowledgmentDialog`: the entire `DialogContent`
  scrolled at `max-h-[90vh] overflow-y-auto`, which pushed the
  Acknowledge / Cancel CTAs below the fold on phone-class viewports.
  Move scrolling onto the body section only so the header and footer
  stay pinned.
- Medication history page back-button rendered the literal "Zurück"
  instead of going through `t()`. Wire the new `medications.back`
  key across all six locale bundles.
- `GoalsChipPicker` shipped a localStorage persistence block scoped
  by user id, but no consumer reads the same key. Drop the storage
  layer (and the unused `goalsStorageKey` /
  `ONBOARDING_GOALS_STORAGE_PREFIX` exports) until v1.4.26 lands the
  `User.onboardingGoals` column the picker is meant to feed.
2026-05-14 22:30:58 +02:00
Marc-André Bombeck fddb975432 chore(i18n): drop 380 dead translation keys flagged in W10 runtime probe
W10's i18n-runtime-gaps probe inventoried 405 unreferenced catalog keys
across both locale files. A second-pass verification against the source
tree (literal `"key"` / `'key'` / backtick exact-match) confirmed 380 of
them as truly orphan and 25 as live references the static analyser had
missed — typically built through template literals or wrapped in
non-string contexts.

This commit removes the 380 verified-dead keys from all six locale
files (en/de/fr/es/it/pl) and prunes the now-empty
`classifications.{bp,pulse,bodyFat}.*` placeholder allow-list entries
in `i18n-locale-integrity.test.ts` so the parity test passes against
the smaller surface.

Top namespaces retired:
  settings.*            (114 keys) — old setup screens, audit-log
                                     leaf labels, ai providerChain
                                     scaffolding
  classifications.*     (69 keys)  — alerts.* group + legacy per-metric
                                     leaf labels; live classification
                                     surfaces use targets.* now
  admin.*               (38 keys)  — superseded by admin/[section] shell
  onboarding.*          (21 keys)  — v1.4.22 onboarding rewrite leftovers
  medications.*         (18 keys)  — v1.4.18 redesign orphans
  dashboard.*           (15 keys)  — v1.4.20 insights redesign leftovers
  bugreport.*           (12 keys)  — simplified bug-report screen
  common.*              (12 keys)  — generic strings no longer referenced
  charts.*              (15 keys)  — v1.4.20 chart-label slugs
  auth.*                (10 keys)  — auth UI now uses inline copy
  notifications.*       (10 keys)  — toast variants no longer fired
  targets.*             (9 keys)   — replaced by targets.{label,status}.*
  measurements.*        (7 keys)
  comparison.*          (6 keys)
  mood.*                (5 keys)
  thresholds.*          (4 keys)
  achievements.*        (4 keys)
  format.*              (3 keys)
  gettingStarted.*      (2 keys)
  nav.*                 (1 key)

Verification:
  pnpm typecheck                  → clean
  pnpm lint                       → clean
  pnpm vitest run                 → 3731 passed, 1 skipped
  i18n-locale-integrity           → 26 / 26 (locales still parity-clean)
2026-05-14 21:40:13 +02:00
Marc-André Bombeck 86dbff0e6e feat(medications): EMA titration-ladder display on GLP-1 detail page
Adds the read-only TitrationSection to the medication detail page,
mounted between SchedulingSection (W19e) and IntakeHistoryList.
Same chrome as the W19d / W19e panels so the Wave-4b stack reads
as one visual group.

Layout:
  - Header with drug INN
  - Vertical step list on mobile, horizontal track from sm; each
    step shows dose, typical-weeks badge, and a current/past/upcoming
    state attribute the design system can hook (CSS-only, no JS state)
  - Current step highlighted with a primary-color ring + 'You are
    here' caption
  - Optional dwell hint + next-step caption — observational copy
    only (no 'should/must step up' anywhere; verified by test)
  - Ceiling message when the user is on the top step
  - Non-standard dose caption when the latest dose doesn't snap to
    a step inside the ±10 % tolerance
  - Disclaimer + external EMA-source link on every render

i18n (medications.titration.*) added in all six locales after the
medications.scheduling block per the W19e handoff for stable JSON
ordering. EN + DE hand-curated Marc-Voice; FR / ES / IT / PL drafted
from EN. Parity test (i18n-locale-integrity) passes.

10 component tests cover surface render, step-card count, single
'you are here', next-step caption, ceiling message, non-standard
dose, disclaimer + EMA link, empty ladder, observational-copy
discipline, and DE locale switch.

The escalationDueHint copy is strictly reference (MDR boundary,
parallel to W19c safety ground rules); the test pins the negative
assertion — no 'should step up', no 'must step up', no
'recommend step up'.
2026-05-14 21:27:29 +02:00
Marc-André Bombeck 71cc05acd6 feat(medications): GLP-1 cadence visualisation + compliance chips on detail page 2026-05-14 21:15:24 +02:00
Marc-André Bombeck 7dfc212634 feat(medications): side-effect logging on GLP-1 detail pages
Adds the /api/medications/[id]/side-effects collection + per-row
endpoints (GET / POST + DELETE), the SideEffectsSection picker UI,
and i18n across the six locales. Mounted on the medication detail
page between the drug-level chart and the intake history; only the
GLP-1 treatmentClass renders the section.

The picker is category → entry chip → 5-Likert severity with an
optional 280-char note. The server derives the category from the
entry server-side — a client-supplied mismatch returns 422, so a
NAUSEA row tagged as INJECTION_SITE never lands in the DB.

POST is rate-limited 30/min/user (matching the inventory route);
DELETE is allowed at any time because the rows are user-owned and
non-clinical, with the audit trail preserving entry + severity +
occurredAt so a forensic reconstruction stays possible.

DE + EN copy hand-curated; FR / ES / IT / PL drafted from EN with
EMA-EPAR-§4.8 terminology. 39 new tests cover the route contract
(auth, ownership, validation, category mismatch, rate-limit) and
the section's static-render shape (empty state, category badge,
severity ladder, entry label, locale switch).
2026-05-14 21:01:58 +02:00
Marc-André Bombeck 20a72d2e32 feat(settings): research-mode toggle with version re-prompt banner
Adds the Research Mode card to Settings → Advanced alongside the
existing danger zone. Toggle ON opens the acknowledgment dialog (the
dialog owns the POST so the user reads the disclaimer first); OFF
fires DELETE directly. An amber re-prompt banner appears above the
toggle whenever the server's currentDisclaimerVersion drifts ahead of
the acknowledgedVersion, surfacing a Re-acknowledge CTA that re-opens
the dialog without flipping the toggle. The Advanced section
description is updated across all six locales to reflect that the
section now hosts both controls.
2026-05-14 20:30:51 +02:00
Marc-André Bombeck 85ba382009 feat(medications): estimated drug-level chart on GLP-1 detail pages
Recharts AreaChart over a 21-day window, anchored at now. Wires the
W19c-Backend pure PK module: drug id resolved from the medication
brand via the catalog, doses derived from non-skipped intake events
matched against the dose-change history. Y-axis is unit-less per
research §2.3 — no tick labels, the 'Estimated level (relative)'
caption sits above the frame. The chart gates on enabled +
acknowledgedVersion === currentDisclaimerVersion (defence-in-depth
alongside the server 400) and renders distinct placeholders for
each failure mode. Mounted on /medications/[id]/history when
treatmentClass === GLP1.
2026-05-14 20:26:21 +02:00
Marc-André Bombeck 3502f57694 feat(medications): research-mode MDR acknowledgment dialog
Opt-in modal that gates the GLP-1 drug-level chart behind an MDR-aware
disclaimer. Reads the live disclaimer version from the server and posts
it back on acknowledge so a server-side bump forces re-acknowledgment.
Sections cover what the chart is, what it isn't, why population PK is
an estimate, the EU 2017/745 + MDCG 2021-24 boundary, and EMA + ASCPT
psp4.13099 citations. Stale-version 400 surfaces a localised error
toast and keeps the dialog open.
2026-05-14 20:19:25 +02:00
Marc-André Bombeck b896622d22 feat(onboarding): baseline + done steps + welcome-back banner + entry-point swap 2026-05-14 20:08:19 +02:00
Marc-André Bombeck 888efd6cf4 feat(onboarding): source 4-card grid with Apple Health coming-soon 2026-05-14 20:03:17 +02:00
Marc-André Bombeck c7b1fdb022 feat(onboarding): goals chip-picker on step 1 2026-05-14 20:00:19 +02:00
Marc-André Bombeck 983c2ff26a feat(onboarding): value-prop carousel on welcome step 2026-05-14 19:56:00 +02:00
Marc-André Bombeck 489d0dacd2 i18n(onboarding): add wizard shell key surface across six locales
New keys (under the existing `onboarding` namespace):
  shell.{back,skip,next,step1of4..step4of4,welcomeTitle,welcomeBody}
  goals.{title,body,placeholder}
  source.{title,body,placeholder}
  baseline.{title,body,placeholder}
  done.{title,body,returnCta}

EN is the production reference; FR / ES / IT / PL mirror the EN
intent. DE strings are neutral hand-off placeholders pending Marc's
hand-curated copy in a later pass — every value is real text so the
no-empty-values and no-TODO locale-integrity guards keep passing.
2026-05-14 19:44:13 +02:00
Marc-André Bombeck e51a4f53bb feat(insights): PR badge on metric trend tiles when achieved in last 30 days
Adds a small text-only PR pill that consumers can compose next to
a metric label. The badge pulls /api/personal-records?metricType=
and renders only when the most recent record landed in the last
30 days — older PRs are still visible on the dedicated /insights
PR list but the dashboard surface keeps a fresh-only filter so the
pill is a noticeable signal rather than a permanent fixture.

i18n keys insights.personalRecord.badge and insights.personalRecord.tooltip
ship in all six locales. The badge component is locale-aware
through the standard useTranslations / useFormatters hooks and
exposes a withTooltip prop so standalone uses (doctor report,
correlation cards) can skip the Tooltip provider.
2026-05-14 18:59:14 +02:00
Marc-André Bombeck 15a72bb329 feat(notifications): personal-records push opt-in toggle (default OFF)
Adds the PERSONAL_RECORD event type to the dispatcher vocabulary
and flips its default policy to opt-in. Without an explicit
preference row the dispatcher now suppresses PR pushes — so a
multi-year Apple Health backfill writes the records but does not
saturate the lock-screen on first sync. Other events keep the
opt-out default they always had.

The /notifications matrix renders the new event automatically
because it iterates EVENT_TYPES; the per-event default helper
keeps the toggle in the right initial state across all six
locales (EN, DE, FR, ES, IT, PL).
2026-05-14 18:51:05 +02:00
Marc-André Bombeck 9b30e116ee feat(i18n): MaintainershipBanner acknowledges AI-drafted safety-critical content
Updates the banner notice across all six locales to be explicit that
the AI-initial locales (FR / ES / IT / PL) are AI-drafted including
the Coach's safety-critical instructions, and that automated
validation guards them in CI. The CTA reframes from "help out" to
"report on GitHub" so users with a regression know the
contribution path is bug reporting, not editorial rewrites.

Pins the new wording in the banner test so a future copy regression
cannot quietly soften the disclosure. Banner gating (renders only on
non-maintained locales, SSR-safe) and the GitHub-issue CTA target
URL remain unchanged from W9e.
2026-05-14 18:30:36 +02:00
Marc-André Bombeck da73e0607e feat(i18n): GLP-1 drug knowledge labels across 6 locales
Add medications.glp1.drug.* namespace with localized INN + drug-class
labels for tirzepatide, semaglutide, liraglutide, dulaglutide, and
exenatide, plus medications.glp1.source.{ema,psp4} citation strings.

Each locale uses the regional spelling convention (DE Tirzepatid,
ES Tirzepatida, PL Tyrzepatyd, …) — scientific names are written
naturally for each market rather than transliterated. Drug-class
labels follow the local pharmacology vocabulary.

All 6 locale files validate; i18n-locale-integrity green.
2026-05-14 18:08:25 +02:00
Marc-André Bombeck 0e51007974 feat(insights): VO2 max chart-row card on Insights cardio surface
W8d shipped the opt-in VO2 max trend tile on the dashboard; this is
the deeper Insights surface. Renders one HealthChart for the trend
plus a compact stat strip (latest / min / max / 30-day average with
the comparison delta vs. the prior period) on /insights/puls because
VO2 max sits in Apple Health's Heart category — cardio fitness, not
its own root concept.

Implementation notes:

  - vo2Max joins CHART_OVERLAY_KEYS so the per-chart cog persists its
    overlay-prefs independently from the pulse chart sitting directly
    above it on the same page.
  - The card stays mounted at zero samples so a brand-new account sees
    the "no data yet" hint rather than a missing surface.
  - Compare-delta caption only renders when the user has a comparison
    baseline active AND the prior-period bucket has data; otherwise the
    slot is suppressed.
  - HealthChart is dynamic-imported with the same skeleton placeholder
    the trends row uses so Recharts stays out of the Insights bundle.

Translations land in all six locales; FR / ES / IT / PL carry English
placeholders matching the v1.4.25 convention. RTL test exercises the
stat-strip slots, the empty-state branch, the compare-delta gates, and
the German label rendering.
2026-05-14 17:50:30 +02:00
Marc-André Bombeck 8f3a95463d fix(design): Dracula tokens + Romance BP abbreviations + therapy-timeline polish
- GLP-1 medication card streak flame: `text-orange-400` →
  `text-dracula-orange`. The hardcoded Tailwind shade was skewing
  warm against the rest of the card.
- /insights/medikamente compliance counters: `text-green-500` /
  `text-orange-500` / `text-red-500` → `text-dracula-green` /
  `-orange` / `-red`. Same drift in three sibling spans on one
  surface; replace as a set.
- Side-effect affordance: `Smile` icon read as a positive
  affordance for a *negative* logging surface. Switch to
  `Stethoscope` so the icon matches the clinical context.
- Health Score componentBp: FR `BP` → `TA`, IT `BP` → `PA`,
  ES `BP` → `PA`, PL `BP` → `RR` — the native compact forms
  used in clinical practice. DE "Blutdruck" + EN "BP" unchanged.
- Therapy-timeline `<li>` key: array index → composite
  (`kind-date-medicationName`). Prevents DOM-reuse bugs when a
  dose change is logged mid-window and reorders the list.
- Therapy-timeline inline icons: `aria-hidden="true"` on the
  `<Pill>` injection glyph and the dose-arrow `<ArrowUp>` /
  `<ArrowDown>` so screen readers stop announcing "image"
  mid-sentence.
- Medication-form Dialog tap targets: the schedule-row dropdown
  trigger (`h-7 w-7`) and the New-schedule button + the four
  schedule inputs (`h-8`) promoted to `h-11` so the dense
  schedule grid honours the 44px floor. The labels stay compact;
  the controls grow.
2026-05-14 16:57:56 +02:00
Marc-André Bombeck 72fcdc656f fix(a11y): WCAG touch-target floor + ARIA pairing across W4–W9 surfaces
Extends the W8.3 44px touch-target rule and the W8e accordion ARIA
contract to surfaces that shipped without them:

- Insights tab-strip pills: pills are primary navigation; promote to
  `min-h-11` so a thumb tap on "Stimmung" cannot bleed into the
  neighbour two pixels below.
- Injection-site picker: raise the invisible hit-target radius from
  11 to 12 SVG units so the rendered diameter (\~25.6 CSS px)
  clears the WCAG 2.5.8 Level AA 24×24 floor. The abdomen left/right
  pair at \Δx=24 caps the upper bound — larger circles would
  collide.
- Source-priority reorder arrows (per-metric + device-type axes):
  promote four `h-7 w-7` icon buttons to `h-11 w-11`.
- Sub-page heading: replace the bare `focus-visible:outline-none`
  with the same ring vocabulary used by insights pills + the Coach
  drawer so sighted-keyboard users get a visible indicator on
  programmatic focus.
- Source-priority per-metric + device-type accordions: pair
  `aria-expanded` with `aria-controls` + matching `id` on the
  panel, mirroring the W8e Provenance contract.
- Maintainership banner dismiss button: 22×22 hit zone replaced by a
  44×44 inline-flex button that keeps the same icon size.
- Injection-site picker group: `aria-label` was leaking the
  cadence-preset string; introduce
  `medications.injectionSitePickerAriaLabel` and translate it
  across all six locales.
- Body-outline SVG: hardcoded English "Body outline" replaced by
  `medications.injectionSiteBodyOutlineAriaLabel` with native
  translations in DE/FR/IT/ES/PL.
2026-05-14 16:55:10 +02:00
Marc-André Bombeck e14466e238 fix(i18n): sleep headline suffix translates across all six locales
The 'per night' caption beneath the sleep headline was branching on
`locale === "de"` and falling through to English for FR/IT/ES/PL.
Move the string into `insights.sleep.headlineCaptionSuffix` with
native translations (de: pro Nacht, fr: par nuit, it: per notte,
es: por noche, pl: na noc) and resolve it through `t()`.
2026-05-14 16:47:06 +02:00
Marc-André Bombeck b48d05a8be feat(health-score): inline provenance accordion with source attribution
Adds a tap-to-expand 'Driven by' panel under the four component
sub-bars inside <HealthScoreCard>. Each row pairs the existing
value bar with a second weight-share bar tinted in the Coach
provenance accent plus a source pill (manual / withings /
appleHealth / mixed / none). Rows sort by effective weight
descending so the biggest contributor leads; null components sink
to the bottom and dim to 50 % opacity.

A 'Provisional' header badge fires when fewer than half of the
configured inputs carry data, and a role=status 'mixed sources'
banner surfaces above the rows whenever at least one component
blends multiple ingest paths.

i18n keys land under insights.healthScore.provenance.* across all
six locales (en / de / fr / es / it / pl with formal register).
The locale-integrity drift-guard auto-discovers the new keys.
14 new RTL-style SSR tests pin the contract: aria-expanded toggle,
aria-controls/id pairing, sort order, source-pill localisation,
mixed banner gating, empty-state dimming, provisional threshold,
asOf tooltip forwarding, and footnote rendering.
2026-05-14 16:21:32 +02:00
Marc-André Bombeck b324e6c41d feat(dashboard): VO2 max trend tile (opt-in, secondary-metric pattern)
Adds the VO2 max strip tile to /, matching the existing
secondary-metric convention (sleep / steps / glucose / oxygen
saturation are all default-invisible + opt-in via Settings →
Dashboard).

Wire-up:
  - src/lib/dashboard-layout.ts: vo2Max appended to
    DASHBOARD_WIDGET_IDS and DEFAULT_DASHBOARD_LAYOUT.widgets with
    visible=false / tileVisible=false. The /api/dashboard/widgets
    Zod enum is derived from the same array so the toggle is wired
    automatically.
  - src/app/page.tsx: vo2Summary from data?.summaries?.VO2_MAX
    (auto-populated by /api/analytics; the route iterates the full
    measurementTypeEnum.options list, no backend change needed),
    hasVo2 data-floor gate, showVo2Tile combined gate, and the
    TrendCard push next to showStepsTile. directionSentiment is
    up-good (higher VO2 max is better) and the unit string runs
    through the t() helper so the iOS app session can localise it.
  - src/components/settings/dashboard-layout-section.tsx: vo2Max
    label key added to WIDGET_LABEL_KEYS so the Settings → Dashboard
    toggle row renders the localized label.
  - messages/{en,de,fr,es,it,pl}.json: dashboard.vo2Max + vo2MaxUnit
    keys. "VO₂ max" is universal across all six locales (scientific
    abbreviation, no translation needed); the unit string is
    consistent SI notation.

No chart-row card — that's an explicit v1.5 deferral per the W8d
outline §6.4. The tile lights up the moment the user accumulates any
VO2 sample (Withings ingest or the upcoming iOS Health passthrough).
2026-05-14 16:04:46 +02:00
Marc-André Bombeck 6a7da4b2cb fix(i18n): backfill audio-exposure + time-in-daylight + comparison hint keys
W8d.1 follow-up — the first commit landed the schema + code surface but
the messages/*.json edits did not persist through the editor sync. This
commit completes the locale parity:

  - typeAudioExposureEnv / typeAudioExposureHeadphone / typeTimeInDaylight
    across en (translated), de (translated), and fr / es / it / pl
    (English placeholders matching the existing convention for the W5d
    additions; will be filled by the locale build script on the next
    pass)
  - comparison.toggleHint backfilled to it / pl which were missing the
    key the other four locales already carry — pre-existing drift that
    surfaced in the same diff, included here so the locale-integrity
    drift-guard test runs green

No code change. The drift-guard tests
(measurement-list-meta + i18n-locale-integrity) now pass.
2026-05-14 15:52:44 +02:00
Marc-André Bombeck 3c4bbdef4f feat(i18n): MaintainershipBanner notice + foundation for 4 new locales
Add the four AI-initial locale bundles (FR / ES / IT / PL) and a
small dismissible MaintainershipBanner strip that paints at the top
of the auth shell whenever the active locale falls outside the
maintainer-curated set (currently DE + EN). The banner names the
AI-initial provenance and links to a GitHub translation-feedback
template so users can contribute fixes back. Dismissal persists
per-locale in localStorage so dismissing the FR banner does not
hide the IT banner once the user switches language.

The bundles are generated by scripts/i18n/build-locale.py — a layered
walk of messages/en.json that applies a vocabulary lookup keyed on
the lowercase EN value, preserves every {interpolation} placeholder
and <tag> marker verbatim, and falls through to the English value
for long-tail prose the script can't translate. The banner advertises
this status so users know what they are looking at. All four locale
files pass the i18n-locale-integrity test verbatim — full key
parity to en.json, no empty values, no TODO/FIXME placeholders,
no value-equals-last-segment matches.

Foundation work bundled with the banner so the build never lands in
a half-wired state:
- locales array extended to ["de","en","fr","es","it","pl"] with
  matching localeLabels + MAINTAINED_LOCALES set + isMaintainedLocale
  helper in src/lib/i18n/config.ts.
- INTL_LOCALE_MAP and parseLocaleFromAcceptLanguage in
  format-locale.ts gained the four new tags so SSR locale resolution
  and Intl number/date formatting work end-to-end.
- i18n/context.tsx and i18n/server-translator.ts import the four new
  JSON bundles alongside DE + EN.
- Coach (src/lib/ai/coach/system-prompt.ts) and the strict insights
  generator (src/lib/ai/prompts/insight-generator.ts) ship the EN
  system prompt with a one-line REPLY LANGUAGE footer for the four
  AI-initial locales. The DE branch keeps its hand-curated body
  verbatim. Routing through the EN system prompt preserves every
  safety contract (zero hallucinations, no dose-prescription,
  evidence-block sentinel format) without re-validating four new
  language bodies.
- Five locale-parameter signatures (no-key-fallbacks, glp1-plateau,
  time-window-format, target-prompts, medication-form) widened
  from "de" | "en" to the canonical Locale type. Helpers that
  ship DE+EN copy route non-DE locales to the EN body, mirroring
  the same fallback chain the JSON bundles use.
- OpenGraph alternateLocale on the root layout lists the four new
  BCP-47 tags so social-link previews advertise the broader
  language coverage.

Tests:
- 4 new SSR tests pin the locale gate (EN/DE hidden, FR/ES/IT/PL
  visible by default, dismissed flag honoured) and the re-exported
  isMaintainedLocale helper.
- parseLocaleFromAcceptLanguage gains a FR/ES/IT/PL coverage block
  alongside the existing DE / EN / unknown branches.
- The existing i18n-locale-integrity suite auto-discovers each new
  messages/<locale>.json and runs the 25-sub-test parity + value-
  quality battery against it — all six locales pass.

LocalePicker is data-driven (settings and onboarding both iterate
the locales array) so promoting the array to 6 entries grows both
dropdowns without per-call-site changes. The PDF doctor-report
renderer is t()-driven for every label, so the four new JSON
bundles give that renderer FR/ES/IT/PL output without touching
doctor-report-pdf-core.ts.
2026-05-14 15:40:34 +02:00