Commit Graph
970 Commits
Author SHA1 Message Date
Marc-André Bombeck bfd129fc7f feat(medications): 90-day research-mode acknowledgment staleness gate 2026-05-14 20:45:00 +02:00
Marc-André Bombeck 8502e7b11f test(coach): adversarial refusal probes for drug-level questions 2026-05-14 20:43:26 +02:00
Marc-André Bombeck 79f8234104 feat(coach): GROUND RULE 15 — refuse drug-level estimates with MDR cite 2026-05-14 20:41:52 +02:00
Marc-André Bombeck 32c1a41c2f docs(planning): W19c-Frontend dialog + chart + settings phase report 2026-05-14 20:32:28 +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 6d748de389 docs(planning): W14b-Content step body + entry-point phase report 2026-05-14 20:09:46 +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 e2fd11ed9c docs(planning): W19c-Backend research-mode + PK phase report
Records the three commits, scope delivered, and the contracts
W19c-Frontend needs:

  - RESEARCH_MODE_DISCLAIMER_VERSION import path,
  - computeOneCompartment / shotPhaseAt signatures,
  - drug-id enum + look-up helper,
  - GET/POST/DELETE API contract for the Settings toggle,
  - chart-wiring rule that hides the curve when persisted
    acknowledgment version drifts behind the server-side constant.

Also notes the two-compartment math deferred to v1.5 and the Ka
textbook-approximation fallback used for non-tirzepatide drugs.
2026-05-14 19:56:36 +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 cf27df49c6 feat(api): research-mode acknowledgment endpoint (GET/POST/DELETE)
Three handlers at /api/auth/me/research-mode gate the opt-in GLP-1
PK research-view chart behind an MDR-disclaimer acknowledgment.

  GET     — returns the persisted flag/version + the live
            currentDisclaimerVersion so the Settings UI can detect
            copy drift and re-prompt.
  POST    — { acknowledged: true, version } turns the flag on.
            Rejects 400 when the submitted version does not match
            the server-side RESEARCH_MODE_DISCLAIMER_VERSION; rejects
            422 on malformed body. Per-user rate-limit 5/min — this
            is a one-tap dialog, not a hot path.
  DELETE  — idempotent: clears all three columns and writes an
            audit row even when the flag is already off.

Prompt-injection guard — the body shape accepts the version string
only (not free text); the value is matched against a server-side
constant before any DB write and never reaches the LLM. Hand-rolled
validation keeps the surface visibly tiny in code review.

11 route tests cover unauth/enabled/disabled GET; happy-path POST
with audit; stale-version POST; malformed POST; rate-limited POST;
unauth/successful/idempotent DELETE with audit.
2026-05-14 19:55:30 +02:00
Marc-André Bombeck 36d147e4f4 feat(medications): one-compartment GLP-1 PK helpers (qualitative use)
Pure module — no I/O, no Date.now(), reads only the static catalog
in glp1-knowledge.ts. Implements Bateman first-order absorption +
first-order elimination per research §2.2, exposes:

  computeOneCompartment — array of {tHours, concentration} samples
                          across a configurable window for the
                          opt-in research-view AreaChart.
  shotPhaseAt           — qualitative rising/peak/fading/none chip
                          per research §2.4.

Two-compartment fidelity is deliberately out of scope. The
journal-of-record (Schneck & Urva 2024, psp4.13099) confirms a
two-compartment structure as the high-fidelity model, but importing
it would invite numeric concentration readouts that cross the EU MDR
Class I threshold (research §11, §12.4). The one-compartment closed
form is sufficient for the qualitative chip and the unit-less
research chart; the rigorous variant waits for v1.5 / R8 alongside
the medical-device review.

RESEARCH_MODE_DISCLAIMER_VERSION (2026-05-14.1) is exported from
this module so the API endpoint records exactly which copy version
the user has acknowledged, and the Settings UI re-prompts on
version drift.

15 tests cover: per-drug smoke, zero-before-dose, empty input,
linear superposition, window/step overrides, sawtooth shape, phase
classification at fresh/mid/tail timepoints.
2026-05-14 19:53:19 +02:00
Marc-André Bombeck 5eeedd7202 feat(db): add user.researchMode acknowledgment fields for GLP-1 PK chart
Three columns gate the opt-in GLP-1 PK research-view chart behind an
undismissable MDR-disclaimer dialog (research §2.3, §11):

  research_mode_enabled — master switch, default false.
  research_mode_acknowledged_at — timestamp of the latest ack.
  research_mode_acknowledged_version — copy version the user read.

Versioning the acknowledgment lets the Settings UI re-prompt when
the disclaimer copy drifts ahead of the persisted value, so a copy
update cannot quietly keep the chart visible to a user who has not
read the new wording.

Migration 0058 uses ADD COLUMN IF NOT EXISTS to mirror 0057's
idempotent posture.
2026-05-14 19:50:35 +02:00
Marc-André Bombeck 64089e56da docs(planning): W14b-Foundation phase report
Five commits landed for the onboarding rebuild scaffold: schema
migration, OnboardingShell primitive, nested step routes, progress
API, and 132 new i18n entries across six locales. Documents the
deviation from the flat-file locale layout the spec assumed, the
left-over content work for the W14b-Content agent, and the test
counts (45 directly-touched tests, full suite green at 3460 passed).
2026-05-14 19:46:26 +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 204dd98e60 docs(planning): Fix-I e2e brittleness hot-fix report
Capture root-cause analysis and fix narrative for the two e2e specs
that were failing on PR #168's latest CI run — Recharts 3.x DOM shift
hitting charts-mobile.spec.ts, and a brittle literal AI-prose
assertion in dashboard.spec.ts replaced with the structural insight-
summary slot.
2026-05-14 19:40:49 +02:00
Marc-André Bombeck 03bf21e544 fix(e2e): assert dashboard insight card container, not literal AI text
The /insights mother page no longer fetches the `/api/insights/general-status`
endpoint — InsightAdvisorCard reads `/api/insights/generate` (a POST
that doubles as cache-aware read), gated by `/api/insights/comprehensive`
returning a non-null payload. The old test mocked a dead endpoint, hit
the EmptyState CTA path, and asserted on a literal AI prose substring
that the renderer never produces verbatim — twice brittle.

Swap the mocks to the real endpoints with a minimal-but-valid
`InsightResult` payload, then assert on the populated card's stable
structural hook `[data-slot="insight-summary"]`. The hook is set by
`InsightAdvisorCard` on the populated-body branch only, so the
assertion still proves "the card mounts its summary when the payload
arrives" — the actual contract under test — without coupling to AI
prose that varies per provider and cache state by design.
2026-05-14 19:39:47 +02:00
Marc-André Bombeck 4849ac7634 feat(api): add POST /api/onboarding/step for wizard progression
Persists per-step progress with strict order enforcement: submitted
step must equal current+1, step 4 flips onboardingCompletedAt and
clears the hl_onboarding proxy cookie. Re-reads the fresh User row to
defend against tab-race conditions, audit-logs every transition, and
caps writes at 30 per 10 min per user to defang stuck-retry loops.

409 on out-of-order, 409 on already-completed, 422 on payload
validation failure, 429 on rate limit.
2026-05-14 19:39:42 +02:00
Marc-André Bombeck f2ec1e2c6f feat(onboarding): scaffold /onboarding/[step] nested routes
Adds the dynamic step segment (steps 0-4) plus a pass-through layout
that leaves the legacy /onboarding root page untouched. Each step page
renders the OnboardingShell with a placeholder body the Content agent
will replace.

Server-side gating enforced:
  - unauthenticated → /auth/login
  - onboardingCompletedAt != null → /
  - out-of-order forward jumps → user's current onboardingStep
Backwards navigation stays allowed so the shell's Back button works.
2026-05-14 19:38:04 +02:00
Marc-André Bombeck d22fc21276 fix(e2e): make charts-mobile xAxis-tick wait class-independent
Recharts 3.x relocated x-axis tick labels out of `.recharts-xAxis` into
a sibling ZIndex container `.recharts-xAxis-tick-labels`. The old
selector `.recharts-xAxis .recharts-cartesian-axis-tick text` matched
nothing under the new DOM, so the wait timed out and the spec failed
on chromium-mobile. Switch both the readiness wait and the per-wrapper
count to the new container, scope counting to `.recharts-wrapper` so
each chart contributes once, and drop empty axes from the tally.
2026-05-14 19:37:25 +02:00
Marc-André Bombeck 9a9827e5fe feat(onboarding): add OnboardingShell wizard chrome primitive
Server component that paints the consistent step frame (logo, four-dot
progress strip, back/skip/next CTA row, safe-area-bottom padding) for
the rebuilt /onboarding/[step] flow. Step pages render their step-
specific body into the children slot; the shell owns every surrounding
pixel.

Translator falls back to the raw i18n key when keys are missing, so the
shell renders cleanly before the full onboarding.* key surface lands.
2026-05-14 19:36:14 +02:00
Marc-André Bombeck ff3314ddb7 feat(db): add user.onboardingStep for wizard resume-state
Persists the highest step a user reached in the rebuilt /onboarding flow
so closing the tab mid-flow no longer wipes progress. Nullable Int
default 0; encoding mirrors the new wizard:
  0 welcome / 1 goals / 2 source / 3 baseline / 4 done

Migration is idempotent (ADD COLUMN IF NOT EXISTS) so it tolerates
hand-applied dev-DB state without diverging from the migration history.
2026-05-14 19:34:23 +02:00
Marc-André Bombeck d2fd9511cd docs(planning): preserve Session-2 phase reports and research files
Bundle 51 .planning/ artifacts written by the Round-3 wave agents and the
W10 reviewer pass that were never staged: phase reports for W3/W5b/W6/W7d/W7e/W8/W8b/W8c/W8e/W11/W11a/W11b/W14a/W14c/W16a-c/W17bc/W19a/W19b/W20a + Fix-G/H,
plus research backgrounders for Apple Health, GLP-1, Withings, Health-Score
provenance, source-priority two-axis, W10 reviewer outputs, and others.

Historical record only - no production-code or schema changes.
2026-05-14 19:28:52 +02:00
Marc-André Bombeck 1c18aeb8b1 docs(planning): v1.4.25 Session 2 handoff for continuation in Session 3
Session 2 hit ~78% context across orchestrator + both W14b-Foundation
and W19c sub-agents. Both paused before any destructive work. This
handoff captures everything that landed, what's in-flight, what's
remaining, and the recommended dispatch order for Session 3.

PR #168 stays as Draft. develop accumulates further commits in Session
3 until Marc's UAT signoff at W23.
2026-05-14 19:21:08 +02:00
Marc-André Bombeck a6f94cfd95 fix(ci): lowercase GHCR image ref for the multi-arch publish workflow
The v1.4.25 W11a refactor moved the workflow off `docker/metadata-action`
as the single source of the image-ref string and started passing
`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}` directly into the
`outputs:` of `docker/build-push-action`. `IMAGE_NAME` comes from
`github.repository` (`MBombeck/HealthLog`), and GHCR rejects pushes
with any uppercase character in the repo segment, so both per-platform
builds failed with `invalid reference format`. Resolve the lowercase
form once at the top of each job via `${VAR,,}` bash expansion and
reuse it across `metadata-action`, `build-push-action`, and the
`buildx imagetools` calls in the merge stage.
2026-05-14 19:18:07 +02:00
Marc-André Bombeck d75d47b4fa fix(test): spread batch-delete fixture rows across distinct measuredAt
The W17b/c sleep-stage migration widened the measurement unique
index to (user_id, type, measured_at, source, sleep_stage) with
NULLs treated as not-distinct, so three same-instant APPLE_HEALTH
weight rows now collide on insert. Derive a stable per-externalId
minute offset and stagger the seeded `measuredAt` accordingly so
the deletion-by-externalId contract is exercised without piggy-
backing on dedup behaviour the route doesn't care about.
2026-05-14 19:12:19 +02:00
Marc-André Bombeck afe8634453 fix(test): align coach-prefs round-trip with W5 defaultWindow default
W5 added `defaultWindow` to the coach-prefs schema with `"allTime"`
as its default, so the PUT handler now returns the canonical
defaulted shape even when the caller omits the field. Update the
integration round-trip to expect the defaulted form on both the
response envelope and the persisted row.
2026-05-14 19:11:51 +02:00
Marc-André Bombeck 82c138fb8e fix(test): anchor coach-snapshot tz test to a sliding now-2d window
The hard-coded "2026-05-14T13:00:00.000Z" reading was due to fall out
of the snapshot's 14-day `recent` window two weeks after the suite
landed, deterministically breaking the assertion. Anchor the instant
to "today minus 2 days at 13:00 UTC" so it always sits inside the
window, then derive the expected Auckland / Berlin day-keys from
that instant. Keeps the test asserting the same tz-divergence
invariant without baking a calendar date into the suite.
2026-05-14 19:11:19 +02:00
Marc-André Bombeck 223b8a9208 test(personal-records): detection worker + queue + UI badge coverage
Adds the integration suite for the detection worker against a real
Postgres testcontainer. Covers measurement-driven PRs (steps,
RHR, VO2 max), idempotency on re-run, silent-flag propagation,
and the drift guard that every PR-trackable metric type lands in
the scan loop.

Also patches a pre-existing schema gap: the
(userId, metricType, metricSlot, achievedAt) unique index on
personal_records is NULLS-DISTINCT in Postgres, so the index alone
does not dedup the measurement-driven rows (metricSlot = NULL).
The worker now does an application-level pre-flight findFirst on
the same composite key before each insert, keeping both the
workout path (index covers it) and the measurement path
(application-level guard) idempotent without a Migration.
2026-05-14 19:02:47 +02:00
Marc-André Bombeck dd44a0df49 fix(build): resolve safety-contracts YAML path from cwd, not __dirname
Turbopack rewrites `__dirname` to a synthetic `/ROOT/...` token at
build time, so `readFileSync(join(__dirname, 'safety-contracts.fr.yaml'))`
crashed during page-data collection in the Docker build with an
ENOENT. The chunk error fixed in the previous commit was hiding the
issue.

The path now resolves through `process.cwd() + 'src/lib/ai/prompts'`,
which stays stable across `next build`, `next start`, and the test
runner. `outputFileTracingIncludes` in `next.config.ts` pulls the YAML
siblings into the standalone runtime image so the read succeeds at
deploy time.
2026-05-14 19:01:58 +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 87853eb2ac fix(validations): route source-priority parse breadcrumb via observer
`@/lib/validations/source-priority` imported `annotate` from
`@/lib/logging/context`, which pulls `node:async_hooks` via
`AsyncLocalStorage`. Any client component touching a type-only export
(e.g. `DEFAULT_SOURCE_PRIORITY` in `components/settings/sources-section`)
dragged async_hooks into the browser bundle and broke the Turbopack
build.

The validations module now exposes `registerSourcePriorityParseObserver`
with a no-op default. `@/lib/logging/context` registers the real
`annotate` callback on first server-side import, so the breadcrumb
still surfaces in ops dashboards while the client bundle stays clean.
2026-05-14 18:57:59 +02:00
Marc-André Bombeck 7bede83d22 fix(tz): split pure helpers out of resolver to keep client bundle clean
`@/lib/tz/resolver` imported `prisma` at module top so any client
component that pulled `detectBrowserTimezone`, `listSupportedTimezones`,
`formatInUserTz`, `userDayKey`, or `DEFAULT_TIMEZONE` dragged
`node:module` into the browser bundle and broke the Turbopack build
(register page, timezone picker, admin general/login sections,
`@/lib/export`).

Pure helpers now live in `@/lib/tz/format` (no Prisma). The resolver
re-exports them so every server-side caller keeps its
`@/lib/tz/resolver` import path; the four client modules + the
export helper now import from `./format` directly.
2026-05-14 18:57:52 +02:00
Marc-André Bombeck ee560a435c fix(medications): glp1 drift test self-skips when research file is absent
The drift guard read `.planning/research/glp1-feature-inspiration.md`
unconditionally and threw an ENOENT at collection time on CI, where
`.planning/` is local-only. The hard-pin block (TS module vs cited
EMA EPAR / psp4.13099 values) is the real production guarantee and
keeps running everywhere; the soft-pin blocks that grep the research
markdown now `describe.skipIf` when the file is missing.
2026-05-14 18:57:45 +02:00
Marc-André Bombeck 4feeafab96 fix(health-score): derive asOf deterministically from input dates
The fallback for missing `attribution.windowEndAt` was `new Date()`,
which made consecutive `computeHealthScore()` calls drift by a
millisecond on loaded CI runners and turned the determinism guard into
a flake. The fallback now picks the most recent input date across the
weight and mood series (Unix epoch when neither has entries) so the
result is byte-identical across calls. Production routes already
supply their own `windowEndAt` and stay unaffected.

Adds a focused test asserting per-component `asOf` stability across
back-to-back invocations.
2026-05-14 18:57:38 +02:00
Marc-André Bombeck 7f133a1aa7 feat(jobs): daily medication-inventory expire-stale queue (03:00 cron) 2026-05-14 18:56:46 +02:00
Marc-André Bombeck a0b9c11e68 feat(medications): inventory card on medication detail page 2026-05-14 18:54:13 +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 7b7a896c44 feat(api): batch routes enqueue pr-detection (silent on >50 batch)
Measurement and workout batch ingest now fire a pr-detection job
after each successful insert. Batches above 50 entries propagate
silent=true so a multi-year Apple Health or HKWorkout backfill
writes the records without spamming hundreds of pushes. Both
enqueue calls are wrapped in audit-log entries and a swallow-and-
annotate failure path so a flaky queue cannot 500 an ingest call —
the 30-minute fallback cron picks the user up in any case.
2026-05-14 18:47:22 +02:00
Marc-André Bombeck d34c30eab6 feat(api): medication-inventory CRUD + intake-event hook for dose decrement 2026-05-14 18:46:49 +02:00
Marc-André Bombeck 4600c2385c feat(jobs): pr-detection pg-boss queue (concurrency=5, 30min fallback cron)
Adds the pr-detection queue and its handler to the reminder worker.
Ingest paths enqueue per-user jobs via enqueuePrDetection(); a
30-minute fallback cron iterates every user as a safety net for
ingest paths that skip the hook. Local concurrency is 5 so a
backfill spike drains without crowding the reminder check or the
daily insights workload running on the same node.

The payload carries a silent flag — ingest hooks set it on bulk
historical batches so a multi-year Apple Health backfill writes
the PR rows without firing hundreds of push notifications.
2026-05-14 18:44:59 +02:00
Marc-André Bombeck 05c20c9ba6 feat(personal-records): detection worker — MAX/MIN per metric, workout slots, warmup gate
Scans Measurement and Workout history per user and writes
PersonalRecord rows for every all-time best. Direction comes from
the W8d helper (MAX = higher wins, MIN = lower wins, null = never
a PR). A seven-sample warm-up gate per metric keeps a one-off
out-of-range sensor reading from locking in as the baseline.

Workout slots cover longest run / longest distance / fastest 5 km
for running and cycling. Ties write a row but mark it as
push-suppressed so the user matched (not beat) their best. The
unique index plus skipDuplicates makes the handler idempotent.
2026-05-14 18:42:59 +02:00
Marc-André Bombeck 34ec6d3bd3 feat(medications): pen-inventory state-machine + 30-day clock helpers 2026-05-14 18:42:52 +02:00
Marc-André Bombeck 570b14d83f feat(schema): MedicationInventoryItem + 30-day in-use clock (Migration 0056) 2026-05-14 18:41:18 +02:00
Marc-André Bombeck a7cc5de9b5 test(dashboard): regression for tile baseline alignment
Pins the load-bearing CSS class names introduced in W20a so a future
refactor that drops the heading-row height lock or the value-row
baseline alignment fails CI before reaching the dashboard.

- Heading-row contract: `h-5` + `whitespace-nowrap` + `truncate`
  on the label slot. Defensive — the live label is locale-abbreviated
  upstream, but a missing short-key fallback or an unexpectedly long
  fallback string should still not wrap to a second line.
- Value-row contract: `items-baseline` + `text-3xl` + `leading-none`
  across all five top-row tiles (Weight, BP, Pulse, Body Fat, Mood) so
  the digits land at the same y-coordinate inside the grid.
- Arrow-slot contract: always rendered, end-anchored via `ml-auto`,
  fixed 16×16 frame; renders a muted "—" placeholder when slope30
  is null so the slot width is deterministic across tiles.
- i18n integrity: every `dashboard.*Short` key resolves to a
  non-empty string in en / de / fr / es / it / pl. Complements
  the broader i18n-locale-integrity test with a precise diagnostic
  for the dashboard tile namespace.
2026-05-14 18:34:40 +02:00
Marc-André Bombeck 135b37512a feat(dashboard): inline trend arrow + baseline-aligned value row
Locks the TrendCard layout so the top-row Dashboard tiles align across
the strip:

- Heading row gets a fixed `h-5` so a one-line label and a (defensive)
  truncate behave identically. The label class adds `whitespace-nowrap`
  + `truncate` so even an unexpectedly long string never wraps to a
  second line — pairs with the locale-abbreviated keys wired in the
  previous commit.
- Value row drops `flex-wrap` and pins `items-baseline` so the
  headline number, unit, and trend arrow all sit on the same y-baseline.
  At the same font-size (`text-3xl leading-none`) every tile's digits
  now land at the identical height (Weight 80 kg used to sit above BP
  122 mmHg when one tile's heading wrapped).
- Trend arrow moves to the end of the value row (after the unit) per
  Marc's brief, with a deterministic 16×16 slot. When the metric has no
  slope yet, the slot renders a muted placeholder dash so the layout
  width stays consistent across tiles.

Marc verbatim: "rechts neben der Zahl, also nachdem die zum Beispiel
Kilo oder so kommt, der Pfeil kommt, wie gerade der Trend ist".
Verified at Pixel-5 (393 px, 2-col grid ≈ 180 px per tile) and
iPad-portrait (5-col ≈ 140 px) — heading row stays single-line, value
row baseline stays flat across the strip.
2026-05-14 18:33:35 +02:00
Marc-André Bombeck 75fce6cfc6 test(ai): safety-contracts parity + sentinel-preservation + brand-name guard
Adds a dedicated parity test for the safety-contract matrix that
complements the existing i18n locale-integrity test:

  - Every EN ground rule must have a non-empty translation in DE /
    FR / ES / IT / PL.
  - No non-EN locale may carry a body identical to its EN counterpart
    (verbatim-EN = placeholder bug).
  - Sentinel literals (KEYVALUES, END, snapshot token, example tag
    open/close) are IDENTICAL across all six locales.
  - GLP-1 brand list is IDENTICAL across all six locales — international
    brand registry, never translate.
  - JSON contract enums (severity / sourceWindow / sourceMetric / etc)
    are IDENTICAL across all six locales — parser keys.
  - Every parser_critical rule declares at least one must_contain
    token (defence-in-depth — a rule that loses its pin would silently
    bypass the refusal-probe).
  - Every locale exports a non-empty defer-to-clinician phrase list
    and a non-empty reply-language directive.

While the parity test was being wired, the dailybriefing schema
ground rule gained must_contain tokens (dailyBriefing / keyFindings /
sourceWindow / sourceMetric) since it is parser-critical.
2026-05-14 18:32:46 +02:00