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.
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.
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.
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
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).
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.
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'.
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.
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.
Commits, scope, test counts, gate results, deviations, and the
medication-detail-page layout map handoff for W19e (reminders /
cadence / compliance) and W19f (titration ladder).
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.