Commit Graph
1004 Commits
Author SHA1 Message Date
Marc-André Bombeck 472da13da7 docs(planning): W21 design findings 2026-05-14 22:01:28 +02:00
Marc-André Bombeck 230e7531b0 docs(planning): W21 security findings 2026-05-14 22:01:16 +02:00
Marc-André Bombeck 51f23ef318 docs(planning): W18 Low + apply-with-care cluster phase report 2026-05-14 21:53:23 +02:00
Marc-André Bombeck 3b5f06d6b3 refactor(insights): extract useInsightStatus hook for sub-page status queries
Five insights sub-pages (blutdruck / bmi / gewicht / puls / stimmung)
each declared the same 13-line useQuery block plus an identical
StatusData interface. The hook routes through the existing queryKeys
factory so the cache keys stop drifting on the next typo. Medikamente
keeps its bespoke query because the compliance-status payload carries
a different shape (per-medication breakdown). Closes W10 simplifier
S12.
2026-05-14 21:51:26 +02:00
Marc-André Bombeck 7d625343f9 docs(planning): note safeRequestProp widening follow-up in W15 phase report 2026-05-14 21:51:04 +02:00
Marc-André Bombeck fcda115123 fix(api-handler): widen safeRequestProp narrow-catch to tolerate undefined requests
Follow-up to commit `61d156e7`. The narrow catch initially recognised
only the V8 private-field TypeError wordings, but unit tests routinely
invoke route handlers as `GET()` with no arguments — the wrapper then
reads `args[0]` as `undefined` and the read callback raises
`TypeError: Cannot read properties of undefined (reading 'url')`,
which is not a private-field error.

The classifier is renamed to `isTolerableRequestProbeError` and now
also matches the "cannot read properties of undefined / null"
TypeError shape (both modern V8 wording and the older
`Cannot read property 'X' of undefined` form). Any other TypeError
still rethrows so production regressions stay visible.

This restores the no-arg-direct-invoke contract that
`src/app/api/auth/me/source-priority/__tests__/route.test.ts` and
similar vitest suites rely on (`GET()` with no request to assert
the auth-gate path returns 401).

Verification:
  pnpm typecheck                                       → clean
  pnpm vitest run api-handler-safe-request-prop        → 11 / 11 pass
  pnpm vitest run src/app/api/auth/me/source-priority  → 6 / 6 pass
  pnpm vitest run (full unit)                          → 3751 / 3751
2026-05-14 21:50:29 +02:00
Marc-André Bombeck bbc0afd555 refactor(analytics): export ContributingSource so its callers stop spelling the union
The literal "manual" | "withings" | "appleHealth" was spelled in
six places across health-score.ts and the analytics route, plus once
in the card. The lib helper and the route now read from a single
ContributingSource alias; the card keeps its local copy on purpose so
the client bundle stays free of analytics imports. Closes W10
simplifier S9 (lib + route scope).
2026-05-14 21:48:24 +02:00
Marc-André Bombeck 4ab0d17808 docs(planning): W15 hygiene cluster phase report
Wave 5 hygiene wave summary covering the four atomic commits and
their verification matrix:
  fddb9754 — 380 dead i18n keys retired (25 false-positives kept)
  41c7425b — BASE_SYSTEM_PROMPT + INSIGHTS_SYSTEM_PROMPT removed
  61d156e7 — safeRequestProp narrow-catch + Tailwind @source base
  76dea94b — Cat-C comment path rewrite on /api/insights/targets
2026-05-14 21:48:11 +02:00
Marc-André Bombeck 76dea94bae chore: Cat-C typo and naming polish from W10 dead-code probe
W10's dead-code candidates report (§C2) flagged a half-quoted reference
inside `src/app/api/insights/targets/route.ts:807`. The comment claimed
the AI prompts lived at `src/lib/insights/prompts/{base-system,
general-status}.ts`, but those files actually sit under
`src/lib/ai/prompts/` and the brace-expansion abbreviation was both
wrong-pathed and easy to misread as a stray-brace typo.

The comment is rewritten in full path form so future readers can grep
the referenced files directly and so the source citation matches the
real location.

Verification:
  pnpm typecheck                         → clean
  pnpm lint                              → clean
  grep -rn 'general-status}' src         → zero hits
2026-05-14 21:46:48 +02:00
Marc-André Bombeck 781e6e180d docs(planning): W20-rest P6 polish phase report
Shipped 3 of 11 P6 polish items in this wave under touch-disjoint
constraints (W15 owns messages/*.json + system prompts, W18 owns
charts core / errors / audit-log / deviceType / drift-guard / CSS
anti-patterns / S1-S12):
  - P6-1 Pearson incomplete-beta replacement
  - P6-3 Coolify auto-deploy maintainer toggle
  - P6-8 GH translation-feedback issue template

Deferred to v1.4.26 (8 items + rationale):
  - P6-2 sentinel HK envelopes (no parser yet)
  - P6-4 Coach lastYear window (spec ambiguity + 9 surfaces)
  - P6-5 Coach prefill on row tap (Wave 4b stable footprint)
  - P6-6 per-night sleep-stage chart (W18 surface + new endpoint)
  - P6-7 locale-native date formats (i18n only — W15)
  - P6-9 hand-review FR/ES/IT/PL prose (i18n only — W15)
  - P6-10 Cat-B endpoint triage (per-endpoint maintainer go/no-go;
    7 recommendations enumerated in the report)
  - P6-11 mood verbal labels (already shipped in v1.4.25 polish pass;
    no remaining surfaces)
2026-05-14 21:46:41 +02:00
Marc-André Bombeck 030896755c refactor(settings): collapse moveSource + moveDeviceType into reorderLadder helper
The two reorder helpers were 21-line near-duplicates that drifted apart
twice during W8c before being realigned. Extracted the shared swap
into a generic reorderLadder<T>() helper so both callers reduce to
read-ladder, call helper, write back. Closes W10 simplifier S3.
2026-05-14 21:46:38 +02:00
Marc-André Bombeck 61d156e71b fix(api-handler): narrow safeRequestProp catch + harden @source path resolution
Two carry-overs from the W7d dev-server repair, flagged for v1.4.26 in
the post-W7d follow-up list.

1. `safeRequestProp` previously caught every exception from the read
   callback so logging instrumentation never crashed the wrapped
   handler. That was overly broad — a regression in a header parser
   or a `null` access inside `read` would silently fall back to the
   default and hide the bug. The catch is now narrowed to the V8
   private-field `TypeError` that Next 16 raises when the request is
   a synthetic placeholder on force-static handlers; every other
   error rethrows. Three message-shape variants are recognised
   (`private member`, `private field`, `private name`) to cover
   current V8 + Bun + older runtimes.

2. `src/app/globals.css` now imports Tailwind with an explicit
   `source("../..")` base. Tailwind v4 auto-detection otherwise walks
   up from the CSS file's resolver root, which can land at an
   unexpected level depending on how the build tool reaches the
   stylesheet. Pinning the base mirrors the cwd-anchored fix the
   safety-contracts loader landed in commit `dd44a0d` and stops the
   planning / docs directories from leaking class-shaped fragments
   into the generated CSS regardless of where the build is invoked
   from.

The narrow-catch helper is exposed through a `__testables` export so
its contract can be unit-tested directly. The new test file covers:
  - the three private-field message variants classify as match
  - generic TypeErrors, RangeErrors, and bare Errors classify as
    non-match (must rethrow)
  - safeRequestProp returns the read result on a real request
  - safeRequestProp returns the fallback on a private-field TypeError
  - safeRequestProp rethrows any other exception

Verification:
  pnpm typecheck                           → clean
  pnpm lint                                → clean
  pnpm vitest run api-handler-safe-request-prop → 9 / 9 pass
2026-05-14 21:45:56 +02:00
Marc-André Bombeck 7b766bedab fix(a11y): wire aria-controls + aria-expanded on GLP-1 dose-history details
The native <details>/<summary> element conveys state via the open
attribute, but the rest of the app's accordions also surface
aria-controls + aria-expanded so SR users hear the same contract on
both. The disclosure now references the inner panel by ID and the
expanded state mirrors the React-side toggle. Closes W10 design L3.
2026-05-14 21:45:38 +02:00
Marc-André Bombeck 766b64e44c feat(github): translation-feedback issue template
The maintainership banner that surfaces on AI-initial locales (FR / ES
/ IT / PL) links to issues/new?template=translation.md, but the
referenced template was never added to the repo. GitHub fell back to
a blank issue body and the funnel produced unstructured reports that
were hard to triage.

The new translation.yml form asks for locale, the route/component
where the wording appeared, current vs. suggested phrasing, and the
kind of issue (grammar, register, terminology, …). The banner URL
moves to template=translation.yml to match the filename.

No test churn — the banner component test does not pin the URL.
2026-05-14 21:44:29 +02:00
Marc-André Bombeck f4606fb1cf fix(charts): replace hsl(var(--token)) with bare var(--token) refs
The Dracula tokens are stored as hex values; wrapping them in
hsl() yielded an invalid CSS color string that the browser fell
back from to default. Mirrors the W10 reconcile-A C1 fix already
applied to the sleep-stage chart. Three call sites remained:
mood-chart CartesianGrid stroke, scatter-correlation CartesianGrid
stroke, and the scatter-correlation tooltip background + border.
2026-05-14 21:43:57 +02:00
Marc-André Bombeck c2e5c8602f test(insights): cover detectGlp1Plateau direct branches with mocked prisma
Existing coverage only exercised the deterministic prompt formatter.
The Prisma-driven branches around window arithmetic, meds[0] pick,
and threshold comparison were untested — the previous regressions
flagged by W10 (security sanitise + timezone) could have slipped.
Ten new tests pin the seven return paths plus the multi-medication
first-pick contract and the drug-name first-word display token.
2026-05-14 21:42:54 +02:00
Marc-André Bombeck e23dd28619 ci(coolify): explicit maintainer toggle for auto-deploy gate
Three releases in a row shipped via the host-side SSH retag fallback
because the COOLIFY_WEBHOOK / COOLIFY_TOKEN secrets were absent, the
deploy step warned and exited 0, and the workflow stayed green. The
silent miss meant the gap surfaced only at deploy time when /api/version
refused to advance.

The vars.COOLIFY_AUTO_DEPLOY repository variable now disambiguates
intent:
  on  — fail the step when secrets are absent (catches the gap loudly
        on the next release).
  off — unconditional skip with a notice (host-side SSH is the chosen
        path).
  unset / other — legacy warn-and-skip behaviour.

Variable lives under Settings → Secrets and variables → Actions →
Variables (not Secrets — it is intentionally readable). The warn-only
default keeps existing repos working; flipping the toggle to 'on'
turns the silent miss into a noisy error.
2026-05-14 21:41:58 +02:00
Marc-André Bombeck 41c7425b55 chore(ai): drop dead BASE_SYSTEM_PROMPT and INSIGHTS_SYSTEM_PROMPT constants
Both constants were @deprecated since v1.4.20 with zero callers
anywhere in the source tree. The Coach surface migrated to
`getBaseSystemPrompt(locale)` + the per-locale safety-contract matrix
(W14c); the Insights surface uses `getInsightsSystemPrompt(locale)`
plus per-page-type prompts introduced in the W3/W6 redesign. The
backwards-compatibility re-exports outlived their purpose.

Removed:
  src/lib/ai/prompts/base-system.ts — `BASE_SYSTEM_PROMPT`
  src/lib/insights/prompt.ts        — `INSIGHTS_SYSTEM_PROMPT`

Verification:
  grep -rn 'BASE_SYSTEM_PROMPT[^_]\|INSIGHTS_SYSTEM_PROMPT[^_]' src
                                  → zero hits
  pnpm typecheck                  → clean
  pnpm lint                       → clean
  pnpm vitest run src/lib/ai src/lib/insights → 1205 / 1205 pass
2026-05-14 21:41:13 +02:00
Marc-André Bombeck c80e0a089d fix(insights): exact Student's-t p-value via incomplete beta
Pearson's two-sided p-value previously relied on the standard-normal
approximation of the t-distribution tail. The approximation is generous
in the wrong direction at low df: at df=12 a true p of 0.04 reports
back as p of 0.025 and the card sneaks past the p<0.05 surfacing gate.
v1.4.23 H6 sidestepped the issue by raising MIN_PAIRED_N from 14 to
20, but the gate was always defence-in-depth rather than correctness.

Auto-discovery work planned for v1.5/v1.6 widens the metric pair grid
past the three pre-defined cards, so the false-positive cost of the
normal approximation starts to compound. The fix lands the rigorous
path now: a regularised-incomplete-beta evaluation of the Student's-t
survival, via the Numerical Recipes Lentz continued-fraction
recurrence, with a Lanczos lnGamma prefactor. The two-sided p is
I_x(df/2, 1/2) for x = df / (df + t^2); the symmetric-tail identity
I_x(a,b) = 1 - I_{1-x}(b,a) keeps the recurrence in its convergent
half-plane.

Reference values agree with R's pt() to ~1e-4 across the surfacing
range. Three new tests pin: r=0.5 df=18 lands in [0.005, 0.06], r=0
df=18 returns p=1 via the symmetric-tail branch, r near 1 df=18
returns a strictly positive p (the old normalCdf call underflowed to
0). The existing noise-fixture test had to be regenerated against a
truly orthogonal LCG-derived stream — the previous fixture produced
r=-0.49 which the exact survival correctly flags as significant
(the normal approx was loose enough to mask it).
2026-05-14 21:40:44 +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 f16d4b3f0e fix(api): rename coach.batch.too_large to measurement.batch.too_large
The batch ingest route lives under /api/measurements, not the Coach.
Aligns the errorCode with the sibling DELETE route, which already
emits measurement.delete.too_large.
2026-05-14 21:37:53 +02:00
Marc-André Bombeck ab54d86bf8 docs(planning): W19f titration ladder + Wave 4b closeout phase report
Closes Wave 4b. The GLP-1 medication detail page section stack is
now complete: DrugLevelChart (W19c) -> SideEffectsSection (W19d) ->
SchedulingSection (W19e) -> TitrationSection (W19f) ->
IntakeHistoryList (existing).

40 new tests across the three commits (22 ladder helper, 8 route,
10 component). Migration 0060 stays unassigned.
2026-05-14 21:28:27 +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 b4ed2eda9e feat(api): GET titration-ladder + current-step for GLP-1 medications
Returns the EMA-reference ladder for the medication's drug, the
user's matched current step (snapped within ±10 % of their latest
dose), elapsed whole-weeks on that step, the immediate-next step,
and the observational escalation-due flag.

  GET /api/medications/[id]/titration
    -> { drugId, drugInn, ladder, currentStep, currentStepIndex,
         weeksOnCurrentStep, nextStep, escalationDue, sourceEMA }

Defence-in-depth gating:
  - requireAuth() + medication ownership check (404 on mismatch);
  - 404 when treatmentClass !== 'GLP1' (the UI already gates here);
  - 404 when the medication name doesn't map to a catalog brand.

Reads the user's latest dose from the MedicationDoseChange stream
(history of record); falls back to parsing the legacy free-text
medication.dose only when the stream is empty.

Pure-read; no writes; annotate() instrumentation only. Same shape
as the W19e cadence route — math lives in the pure helpers, prisma
lives in the route.

8 cases pass covering 401, 404 paths (non-owner, non-GLP-1,
unknown brand), the happy path on a 5 mg Mounjaro user, the
non-standard-dose null path, the ladder-ceiling no-next-step path,
and the escalationDue=true boundary past EMA dwell-time.
2026-05-14 21:23:14 +02:00
Marc-André Bombeck bdaf128931 feat(medications): pure titration-ladder helpers from W19a knowledge
W19f reads the EMA titration schedules already pinned in the
glp1-knowledge layer (titrationStepsMg + titrationIntervalWeeks) and
normalises them into a TitrationStep contract the upcoming API
route and detail-page section will share:

  - getLadder(drugId) / ladderFromRecord(record)
  - findCurrentStep(drugId, latestDoseMg) — snaps within ±10 %, null
    outside the tolerance window so non-standard doses don't get
    falsely highlighted
  - nextStep(drugId, currentStep) — null at the ceiling
  - weeksOnCurrentStep(drugId, currentStep, doseChanges, asOf) —
    counts elapsed whole-weeks since the latest matching dose-change
    row
  - escalationDue(drugId, currentStep, weeksOnStep) — observational
    flag (titrationIntervalWeeks reached AND next step exists)

The escalationDue helper is a *display* flag, not advice — the UI
copy stays observational (MDR boundary, parallel to W19c ground
rules). No DB access; the route owns prisma, this module owns math.
Same shape as the W19d / W19e pure modules.

22 cases pass covering every EMA-approved drug's ladder, tolerance
boundaries, the ceiling edge, weeks-on-step for empty / matching /
future histories, and the escalationDue toggle.
2026-05-14 21:21:57 +02:00
Marc-André Bombeck 631b841c5f docs(planning): W19e cadence + compliance phase report 2026-05-14 21:16:21 +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 3ef49679ed feat(medications): cadence + compliance read endpoint 2026-05-14 21:11:54 +02:00
Marc-André Bombeck 4af09170bb feat(medications): pure cadence + compliance helpers for GLP-1 detail 2026-05-14 21:10:45 +02:00
Marc-André Bombeck f0f3143cab docs(planning): W19d side-effect taxonomy phase report
Commits, scope, test counts, gate results, deviations, and the
medication-detail-page layout map handoff for W19e (reminders /
cadence / compliance) and W19f (titration ladder).
2026-05-14 21:02:52 +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 4e2b7be472 feat(medications): pure side-effect taxonomy + severity Likert helpers
Single source of truth for the GLP-1 side-effect taxonomy — entry →
category mapping plus the reverse index, ordered category list, and
a 1-5 Likert → semantic-label helper. The DB stores `category`
denormalised for cheap range scans; this module is the authority and
the API write path uses `categoryForEntry()` instead of trusting a
client-supplied category.

Severity helpers return locale-agnostic ladder keys (`mild` …
`verySevere`); the UI maps each to a translated label so the picker
stays purely structural. Tests pin every contract the API validator
and picker depend on: 21 entries × 5 categories with the EMA-derived
counts (GI 5, METABOLIC 4, INJECTION_SITE 4, COGNITIVE 4,
GLP1_SPECIFIC 4); every entry maps to exactly one category;
severity-ladder index is monotonic.
2026-05-14 20:54:35 +02:00
Marc-André Bombeck 7d56f6f51f feat(db): MedicationSideEffect table + 21-entry × 5-category taxonomy
W19d sub-wave — structured GLP-1 side-effect logbook derived from
EMA EPAR §4.8 ("Undesirable effects"). 21 entries rolled up into 5
surface categories (GI / METABOLIC / INJECTION_SITE / COGNITIVE /
GLP1_SPECIFIC) with 1-5 Likert severity per row.

Composite index on (user_id, medication_id, occurred_at) for the
detail-page timeline read; a second (user_id, occurred_at) index
serves the Coach snapshot aggregation. Severity is bounded with a
DB-level CHECK constraint, defence in depth against client drift.
Notes column is unbounded TEXT, application-layer capped to 280
chars in the Zod schema introduced in the follow-up commit.
2026-05-14 20:52:39 +02:00
Marc-André Bombeck 575e951c50 docs(planning): W19c-Safety GROUND RULE 15 + refusal probes phase report 2026-05-14 20:46:52 +02:00
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