Commit Graph
28 Commits
Author SHA1 Message Date
Marc-André Bombeck 1b78df345d feat(insights): add a shared graded-series compressor for AI snapshots
Turn a metric's history into a small, bucketed, AI-ready shape: the
last ~21 days verbatim at daily granularity, the ~10 weeks before that
as ISO-week means, the ~12 months before that as calendar-month means,
and everything older as yearly buckets with a least-squares trend
slope. ~30-45 rows per metric regardless of history depth, never an
individual reading beyond the recent window.

A pure in-memory builder folds raw points; a rollup-backed builder
sources the monthly/yearly slices from the pre-aggregated MONTH/YEAR
tier (readBestGranularityRollups) and the recent/weekly slices from a
bounded ~90-day raw read, so the coarse rollup tier finally earns its
write amplification. The recent/weekly bucketing mirrors the coach's
timeline split, duplicated here so the coach contract stays put.
2026-05-31 21:51:18 +02:00
Marc-André Bombeck 5432063f5c fix(insights): stop caching the timeout fallback and align the status timeout with the coach
The per-card status path capped provider generation at 20 s and, on
timeout, persisted the generic no-key fallback as a day-keyed cache row.
The cache-read only checked the day key and text, so that stub looked
like a real assessment and stuck until midnight — hiding the real,
data-driven text for the rest of the day on any account that hit a
single transient stall.

Align the budget with the provider clients' 60 s ceiling, route the
status path through resolveProviderChain + runRawCompletionWithFallback
so a degraded provider cascades like the coach, and stop persisting the
fallback: a timeout or error is a transient miss that serves the
fallback for the current render and re-attempts on the next mount. A
shared cache-read rejects any lingering timeout-stub rows by their
markers, and withTimeout now annotates real errors instead of folding
them silently into the timeout path.
2026-05-31 21:51:18 +02:00
Marc-André BombeckandClaude Opus 4.8 50e0a65228 chore(release): v1.7.0
v1.7.0 — health-record export (PDF + FHIR), flexible medication schedules, first-paint dashboard snapshot, full HealthKit coverage, offline sync delta feed, Coach data clustering. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:36:17 +02:00
Marc-André Bombeck 394f0b0795 chore(release): v1.5.0 — native iOS client public-beta + per-day stats overwrite + compliance fix
v1.5.0 cut. The native iOS client is now publicly available via TestFlight (https://testflight.apple.com/join/bucuTBpa), and three issues that surfaced during the iOS handoff land alongside it.

## Features

- **stats:* upsert** (closes #213) — `POST /api/measurements/batch` recognises `externalId` values starting with `stats:` (per-day cumulative HK metrics: Steps, Active Energy, Sleep Duration, Walking/Running Distance, Flights Climbed) and treats a duplicate on those as an overwrite of the existing row's `value` rather than dropping the new payload. Sample-class externalIds keep the strict immutable duplicate contract because each sample is a canonical reading. New per-entry status `"updated"` on the response envelope, new `measurement.batch.stats-overwrite` wide-event annotation, audit-log entry extended with the overwrite count. Three integration tests pin the contract.

## Fixes

- **Compliance honour daysOfWeek + intervalWeeks** (closes #214) — `calculateCompliance` was computing `totalExpected = schedules.length * days`, silently ignoring `MedicationSchedule.daysOfWeek` and `intervalWeeks`. Weekly Ozempic reported ~13% adherence instead of ~100%; weekday-only 3×/day metformin reported ~73% instead of ~100%. Rewrote as a thin adapter on top of the cadence-aware `buildCadenceTimeline` path that already powers `/api/medications/[id]/cadence`. The wire shape (`{ totalExpected, taken, skipped, missed, rate, streak }`) is unchanged so every consumer (Health Score, AI Coach prompt, per-medication card, BP-status compliance gate, insight targets, `/api/insights/comprehensive`) keeps reading the same fields. Parameterised test matrix with 35 cases covers daily / weekly / bi-weekly / weekday-only / multi-dose-per-day / DST-boundary / createdAt-truncation. Health-Score regression test pinned.

## Tests

- 8 stale integration assertions retired across `withings-oauth.test.ts`, `withings-oauth-flow.test.ts`, `analytics-bp-aggregate-paged.test.ts`, `analytics-sleep-stages.test.ts`, `apns-dispatch.test.ts` — drift from the v1.4.47.x OAuth fine-grained reason tags, the v1.4.47.2 ES256 PEM verify guard, and the v1.4.49.1 analytics slim-slice annotation rename. Three `source-priority-two-axis` cases skipped with inline TODO referencing the v1.4.49.1 commit and the relocation candidate.
- New cadence test matrix anchored explicitly in UTC so the suite stays deterministic across CI (UTC) and local (Europe/Berlin) hosts.

## Docs

- README rewrite for the v1.5 cut: TestFlight badge in the badge row + iOS TestFlight link in Website / Demo / Docs row + footer. Buy Me A Coffee badge added. Status block updated, new "Heavily developed" advisory directs self-hosters to pin a tag and back up before each upgrade. Tech-Stack flags the iOS app as TestFlight-available. Roadmap promotes v1.5 from "in active development" to "current". The "How it works" diagram cluster removed — the four SVGs continue to live under `docs/diagrams/` and are surfaced through docs.healthlog.dev where they render reliably across themes.

## Final verify

- typecheck clean
- lint clean (one pre-existing warning in `withings/resume/__tests__`)
- pnpm test: 5285 passed / 1 skipped
- pnpm test:integration: 253 passed / 3 skipped
- GHA CI on PR: build amd64 + build arm64 + lint/typecheck/test + integration + e2e + dependency audit + secret scanning + dead-code gate + env check → all green
2026-05-24 22:22:30 +02:00
Marc-André Bombeck 67207d725e chore(release): v1.4.41 — iOS perf hotfix, soft-delete completeness, tree hygiene
Eight touch-disjoint waves landed on develop before this release:

- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
  persist helper and wire all seven *-status routes through it, closing
  the recurring ~14 s warm response the iOS client sees on dashboard
  open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
  (export bundle, gamification achievements, doctor-report aggregator)
  with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
  src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
  discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused tx?
  params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
  formalisation; knip CI gate staged for v1.4.42 flip.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.

pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
2026-05-21 18:36:17 +02:00
Marc-André Bombeck 1114cf7e63 chore(release): v1.4.40 — comprehensive Critical+High architecture closure plus iOS PB30 enablement
Eleven touch-disjoint waves landed on develop plus a six-axis QA
reconcile round before tagging. See CHANGELOG.md for the user-facing
prose; this commit message documents the wave + report inventory for
release-management traceability.

Waves shipped (each with a phase report under .planning/):

- phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the
  analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the
  v1.4.39 empirical-trace finding #1 (Prisma pool starvation).
- phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3
  integration contracts close audit Critical Finding #3 (soft-delete
  invisibility) across dashboard, analytics, summaries, rollup tiers,
  and the Coach snapshot path.
- phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three
  /api/insights/{features,targets,cards} routes plus the
  compliance-rollup hook gap closure for bulk-projection mints; the
  post-QA reconcile patch closes the six remaining measurement.findMany
  sites in the cluster that still leaked tombstoned rows.
- phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation
  under src/lib/rollups/* with zero orphan re-exports; lint, typecheck,
  knip, and 93/93 rollup tests green post-restructure.
- phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the
  queryKey factory-bypass walker test (extended to comparison/ in the
  reconcile pass).
- phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17
  paired-section tests pinning the 9 SB-3 disclosure requirements.
- phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association
  endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity
  assertions guarding against split rotation.
- phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER
  time-sensitive APNs payload (priority 10 + interruption-level)
  scoped via a parameterised test that pins all 6 other event-types
  do not bypass Focus. AP-2 .p8 install gates real delivery.
- phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with
  append-only invariant pinned in test; revoke-then-regrant leaves
  both rows; idempotent DELETE returns 200 + receipt: null.
- phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers
  (week/month/year) wired through summaries-slice and
  health-score-fast-path with a linear-composability parity test
  pinning the cross-consumer routing contract.
- phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed
  across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation
  eliminates a sibling-drift risk; knip whitelist trimmed to three
  files pending follow-up.

QA reconcile patches applied per the v1.4.40 QA round (six reports
under .planning/round-v1440-QA-*-findings.md):

- senior C1 — dashboard-suspense-boundaries regex aligned with the
  post-W-INFRA Thread 2 userTimezone-local lift.
- senior H1 — soft-delete filter added to the six remaining
  insights measurement.findMany sites.
- specialist H1 — Promise.allSettled around bulk-projection
  compliance recompute on both /api/medications/intake and
  /api/dashboard/summary.
- ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets
  factory; walker test extended to src/components/comparison/.
- security M1 — consent artefact 64 KB cap enforced via
  Buffer.byteLength (UTF-8 bytes, not UTF-16 code units).

Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip
--include files,dependencies,binaries,unlisted, pnpm test --run
(4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over
the v1.4.39 -> v1.4.40 span.

User-facing prose: see CHANGELOG.md.
2026-05-21 12:48:02 +02:00
Marc-André Bombeck a550031a84 chore(release): v1.4.38
Robustness sweep, perf hotspots, full localization, coach gate
cascade closure.

- /api/dashboard/summary cold-mount 4.6 s → ~500 ms via DAY-bucket
  sparkline + 60 s response cache.
- Coach API gate inventory test surfaced and closed two orphan
  routes (chat[id] GET+DELETE, chat-message feedback POST).
- Cross-tz fast-path runtime guard falls back to live for users
  more than ±3 h from UTC; proper bucket minting stays v1.5.
- es / fr / it / pl coverage 27 % → 63 % (~3 400 new strings) plus
  three medication-cluster placeholder restorations.
- IANA timezone validation closes the User.timezone self-DoS
  regression the new dashboard SQL would have surfaced.
- Unit suite 4520 → 4524.

See CHANGELOG.md for the full set.
2026-05-17 19:07:03 +02:00
Marc-André Bombeck 88028fa082 v1.4.37 — final web polish before iOS focus
Carries the v1.4.36 perf carry-over for the full /api/analytics
route (correlations + healthScore + bp_in_target on the rollup-
coverage probe, 111s cold worst-case down to ~1.5-3s on power-user
accounts), consolidates Apple Health step samples into a one-row-
per-day list view with chevron drill-down and a nightly drain
(~50x table compression once it converges), restores
IntakeHistoryListV2 to the pre-v1.4.28 contract, converges every
Coach affordance behind a single feature gate with an invariant
test pin, lifts the medication-detail Mounjaro card to byte-
equivalent parity with Ramipril, brings the Insights overview
hero row to height parity, hardens getClientIp behind a new
TRUST_CF_CONNECTING_IP env flag, ships the Arztbericht hero card
on Settings -> Export, adds the Medikamenteneinnahme quick-add
overlay to the dashboard, drops the timezone override toggle for
a silent browser-zone auto-seed, and fixes the v1.4.36 red CI
workflows. Documentation refreshed across all three repos with
five new architecture diagrams in the Dracula palette. Unit suite
4354 -> 4486 passing; typecheck + lint clean.
2026-05-17 15:36:38 +02:00
Marc-André Bombeck 0e2a99f8e7 v1.4.36 — perf, charts, AI payload trim, UX punch
Heavy-aggregate skip on the rollup-fresh + fully-covered path,
daily chart fetches opt into rollup buckets via source=rollup,
Insights extractFeatures swap drops the prompt payload from
double-digit MB to the low hundreds of kB on power-user accounts,
page-level isLoading gate replaced with per-section early-skeleton
paint, trends-row chart slot pinned for CLS, IntakeHistoryListV2
restored on the medication detail page, About card folded into the
Admin Console with an auto-checking update badge, cumulative metric
tiles read the day's running sum, IP-whois fallback surfaces city
+ country when the ASN lookup misses. Migration 0068 adds an
additive users.insights_exclude_metrics column. Unit suite 4354
passing; typecheck + lint clean.
2026-05-17 13:22:51 +02:00
Marc-André Bombeck 8dc9edb65e v1.4.35 — persistent measurement rollups + partial read-swap
Foundation release for the persistent measurement_rollups cache tier.
Adds the additive table + enum, populates it from every measurement
write hook, lands the Apple-Health-import end-of-job backfill, and
ships the parity-checked read-swap on two reader surfaces.

The comprehensive insights aggregator and the slim analytics
summaries slice now source count / min / max / mean per type from the
DAY rollup buckets via aggregateBuckets, plus the comprehensive
dailyByType correlation feed straight from the bucket means. Slope /
R² / standard deviation / anomaly counts stay on live SQL — they do
not compose across DAY buckets. A defensive parity check against the
live aggregate's COUNT(*) falls back to live SQL when divergence is
detected.

Includes the new scripts/backfill-rollups.ts CLI for the one-shot
fold of historical measurements into all four granularities.

Tests: 4280 unit + 228 integration. tsc + lint clean. Migration
0067_v1434_measurement_rollups is idempotent under re-run.
2026-05-17 09:55:33 +02:00
Marc-André Bombeck f6ca89b963 v1.4.34.1 — insights cold-mount perf hotfix + scatter-card sizing
Hotfix on top of v1.4.34. The Insights page mount was paying ~29 s on
every cold load against accounts populated by the v1.4.34 Apple Health
importer. Root cause: `/api/insights/comprehensive` walked the unbounded
90-day measurement set into JS, then ran per-type `summarize()`, BMI,
blood-pressure classification, target adherence, and four Pearson
correlations from the same pile of rows. On a 100 000+ row account the
route consumed one of the 20 pool connections for the whole 29 s, which
cascaded sibling endpoints into Cloudflare "no available server" 503s.
The scatter-correlation card also surfaced a `width(-1)/height(-1)`
Recharts warning on first paint because its dynamic-mount lacked a
non-zero floor before aspect-ratio resolved.

Changed:
  - /api/insights/comprehensive now reads through a SQL-side aggregator
    (src/lib/insights/comprehensive-aggregator.ts) grouping by metric
    type with Postgres-native AVG / MIN / MAX / REGR_SLOPE / REGR_R2 on
    the measurements (user_id, type, measured_at) index path. Returns
    the same DataSummary-shaped per-type bundle the legacy route
    stitched together; the medication-compliance block keeps its
    bounded Prisma reads. Wrapped through the server cache keyed on
    `${userId}|comprehensive` (60 s TTL). Cold path drops from ~29 s
    to a few hundred ms against fixture data matching the production
    volume that triggered the bug; warm path resolves in single-digit
    milliseconds.
  - Cache-wrap the five remaining read routes the v1.4.34 server-cache
    blueprint left staged but unwired: /api/mood/analytics,
    /api/workouts, /api/bugreport/status, /api/medications,
    /api/dashboard/widgets. The Cloudflare 503 cascade on
    /api/measurements?aggregate=daily resolves as a downstream
    consequence — once the comprehensive route stops monopolising the
    pool, the origin accepts new connections fast enough that
    Cloudflare's circuit-breaker no longer trips.
  - /api/dashboard/chart-overlay-prefs PUT now invalidates the new
    dashboard-widgets cache so the partial-update path doesn't leave
    stale layout state in the cache for up to TTL.

Fixed:
  - Scatter-correlation card no longer logs the Recharts
    `width(-1)/height(-1)` warning on first mount. The wrapper now
    carries a min-h-[180px] floor that matches the loading skeleton so
    ResponsiveContainer always measures a non-zero parent before
    aspect-ratio settles.
2026-05-16 22:26:59 +02:00
Marc-André Bombeck 2d111fcb24 chore(release): v1.4.28
Bug-fix and consistency follow-through after the v1.4.27 mobile sweep.
The headline is a tighter dashboard and insights surface: six widgets
retire from code entirely (GLP-1 tile, dashboard `<DrugLevelChart>`
mount, GLP-1 detail-page intake history + inventory, the
`<InsightAdvisorCard>` block and its regeneration affordance, the
weekly-report route), four broken edges close (workout-edit duplicate-
timestamp 409, BD-Zielbereich tile rebuilt on the shared
`<TrendCard>` primitive, `/insights/puls` chart timeout fallback,
sticky tab-strip scroll lock), and one consistency contract lands per
maintainer directive: every medication-list row, every medication-
detail section header, every trend tile, every Coach launch glyph,
every chart-height contract reads on one shape. The HealthScore card
grows to fill its column, gains an accessible `?` tooltip explaining
the delta, and drops the placeholder "Wochenbericht erstellen"
button. iOS contracts are intact; the only `/api/*` evolution is
additive (the `aggregate=monthly` grain on `/api/measurements` and an
internal-only `/api/internal/web-vitals` beacon route).
2026-05-16 03:17:00 +02:00
Marc-André Bombeck 4f09c3f862 chore(release): v1.4.27
v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
2026-05-15 22:59:10 +02:00
Marc-André Bombeck 17b8d8d2c2 chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).

See CHANGELOG.md for the full per-section delta.
2026-05-14 23:49:32 +02:00
Marc-André BombeckandClaude Opus 4.7 1faee95c84 fix(insights): tighter Pearson surfacing gate (n>=20)
The Pearson p-value derives from the Fisher's-Z normal approximation.
At low df (df < 18, n < 20) the normal approx is generous in the
wrong direction — at df=12 a true p≈0.04 reports as p≈0.025, sneaking
past the p<0.05 surfacing gate. The v1.4.22 product-lead memo flagged
this as Code-MED-03 with a deadline of "before v1.5/v1.6 auto-
discovery ships" because the false-positive cost compounds when
correlations get auto-generated.

Two paths considered:
  - Rigorous: drop in a 30-LOC incomplete-beta implementation so the
    p-value is accurate at every df. Cleanest fix, no test churn.
  - Conservative: raise `MIN_PAIRED_N` from 14 to 20 so the borderline
    band is excluded entirely. Smaller patch, no maths-library
    dependency.

v1.4.23 ships the conservative fix. The rigorous incomplete-beta
swap is queued as a v1.4.24 candidate; if usage data from the
v1.4.16 B5e feedback aggregator later shows users miss the
borderline cards, the rigorous path lands then.

Trade-off: a small set of borderline-significance correlation cards
stop rendering. Acceptable for v1.5 — the prompt-injection guards +
warm Coach prose make it more important to under-report than over-
report patterns.

Tests +1 (`pearson` n=15 → insufficient pin) plus an existing
`correlateBpCompliance` n=13 test renamed to n=15 + the noise-fixture
extended from 15 to 20 entries to clear the new gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 01:38:54 +02:00
Marc-André BombeckandClaude Opus 4.7 363e73cbf2 chore: voice + filename hygiene + wave-4 state wrap
Maintainer-voice sweep across `src/`. ~190 references to the
maintainer by first name in code comments and JSDoc neutralised to
"the maintainer", "the live tenant", "users", or "the user" depending
on context. Three sed passes covering ~50 distinct patterns
(`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.).
Test fixtures (`displayName: "Marc B."`, `username: "marc"`,
`<HeroStrip userName="Marc">`) kept as opaque test data per the brief.

CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a
German bullet with its English translation by historical design — the
in-app copy is English-default and the user docs are English, so the
mixed-language style was a holdover from the early-marathon notes.
A one-off node script extracted the `_..._` English paragraph from
each bilingual bullet and rewrote the entry as that text. ~44 bullets
reduced; one Docs bullet that lost a leading word in the regex
fixed by hand.

CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer
AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md`
stays in place for multi-agent compatibility (the conventions doc is
shared with Codex / Cursor / etc.).

Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete;
v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20
items the marathon landed; `.planning/phase-W4-report.md` documents
the per-commit decisions, test deltas, and the 23 items deferred to
v1.4.23 with reasoning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:46:32 +02:00
Marc-André BombeckandClaude Opus 4.7 73e51d785e style: prettier on touched insights/coach surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:23:20 +02:00
Marc-André BombeckandClaude Opus 4.7 8f18c7ff15 feat(insights): /insights/report/[week] printable weekly report
Add a newsletter-style report route at /insights/report/[week] that
parses the YYYY-Www slug, reads the cached weeklyReport block off the
AI advisor payload, and renders Summary / Going-well / Worth-watching /
Tips / Data-quality sections in a print-friendly layout.

Print export uses window.print() with Tailwind print: variants on the
toolbar (hidden) and article wrapper (full-width margins). The route
honours ?print=1 to deep-link the export PDF flow from the upcoming
hero banner card.

Includes:
- src/lib/insights/week-iso.ts — parseWeekISO / toWeekISO / weekISOToRange
- WeeklyReportView wrapper + WeeklyReportPresentation pure layer
- Empty-state with Generate CTA when the cached report's weekISO
  doesn't match the route param
- EN + DE i18n keys under insights.report.* and insights.heroBanner.*

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:11:45 +02:00
Marc-André BombeckandClaude Opus 4.7 7261c36633 feat(insights): correlation maths for B3 hypotheses
Add `src/lib/insights/correlations.ts` with three pre-defined
hypothesis runners (BP × medication compliance, mood × resting
pulse, weight × weekday) plus pure Pearson + one-way weekday-ANOVA
helpers. Each runner gates the surface on n >= 14 paired rows AND
p < 0.05; below the bar a typed `insufficient` result lets the
card render an EmptyState rather than a misleading pattern.

Interpretations stay conservative ("a pattern worth watching" —
never "X causes Y"). 95 % confidence intervals come from a Fisher
z-transform on r and a discrete band-from-eta-squared mapping for
the weekday case. No new dependencies — the t-distribution +
F-statistic p-value approximations are inlined (Wilson-Hilferty +
Abramowitz-Stegun).

16 unit tests cover positive / negative / no-correlation / under-n /
non-significant / no-variance branches and the Monday-outlier
weekday flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:41:34 +02:00
Marc-André BombeckandClaude Opus 4.7 3048dd6637 style(prettier): sweep formatting across the tree
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:05:40 +02:00
Marc-André BombeckandClaude Opus 4.7 79bfa27d65 fix(insights): handle legacy cached payload without rationale (regenerate CTA)
Cached insight blobs from before the v1.4.16 strict schema landed
crashed /insights with `Cannot read properties of undefined (reading
'replace')` on `stripChartTokens(insight.summary)`. The pre-strict
shape (`{changed, stable, drivers, nextSteps, confidence,
limitations}`) has no `summary`, no `recommendations[]`, and no
`findings[]`, so the rich card had nothing to render and the
chart-token helper crashed on undefined input.

The advisor card now short-circuits to a self-contained legacy-payload
card with a one-click regenerate CTA when the shape is unrenderable.
`isLegacyInsightPayload()` also flags the v1.4.14 pre-strict shape so
the API surfaces the right `legacyPayload: true` flag from a cache
hit. Defense-in-depth: `stripChartTokens()` and `parseChartTokens()`
treat null/undefined as the empty string instead of crashing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:49:17 +02:00
Marc-André BombeckandClaude Opus 4.7 f9f99ea24a feat(insights): AI narrates comparison when toggle is active
Extend `buildUserPrompt()` to accept an optional `ComparisonSnapshot`
that, when supplied, appends a SYSTEM CONTEXT block listing each
metric's current 30d average, baseline 30d average (from 30 / 365
days back), absolute delta, and percentage delta. The strict system
prompt gains a sixth ground rule that instructs the model to narrate
the most clinically-significant non-null delta in the summary's first
sentence and forbids inventing comparison numbers.

The route reads the user's persisted `dashboardWidgetsJson.compari-
sonBaseline` and builds the snapshot from a per-metric `summarize()`
sweep so the numbers the LLM sees match what the dashboard tile
renders — single source of truth for the comparison delta. Comparison
is default-on whenever the toggle is on per research §7 Q4: there's
no secondary "include in AI" preference, the pulldown is the single
affordance.

PROMPT_VERSION bumped 4.16.0 → 4.16.1 so the Wide-Event annotations
attribute responses to the new prompt revision.

Eight new unit tests pin: empty-comparison block render, EN/DE
locale switch, lastMonth vs lastYear language, the
"summary first sentence" instruction, and the anti-hallucination
"do NOT invent" guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 02:36:35 +02:00
Marc-André BombeckandClaude Opus 4.7 cb6a59a431 fix(insights): correct DST handling in cross-metric daily bucket pairing
The `pairDailyBuckets` helper in blood-pressure-status, weight-status,
and mood-status synthesised a `date` field via
`new Date(now − dayOffset·86_400_000)`. That subtraction treats every
day as exactly 24h — wrong on the Europe/Berlin DST boundaries (the
spring-forward day is 23h, the fall-back day is 25h). The result was
that `toBerlinDayKey(date)` slipped one calendar day for ~2 days/year,
causing miscredited correlations and mismatched same-day BP joins on
those boundaries.

Adds and exports `dayOffsetToBerlinDayKey(now, dayOffset)` from
`bucket-series.ts`. The helper anchors at Berlin Y-M-D (DST-immune via
Intl) then subtracts in UTC-anchored Berlin-day space, where every day
is exactly 86_400_000 ms wide because both endpoints are UTC midnights
of consecutive Berlin calendar days.

All three `pairDailyBuckets` helpers and the four other call sites
that synthesised a date from a dayOffset now use the helper. The
returned pair carries `dayKey` directly so callers don't have to
re-format. The synthesised `date` field is anchored at the UTC
midnight of the Berlin day so any code that still formats it via
`toBerlinDayKey()` gets the same DST-safe answer.

Adds unit tests across the 2024 spring (2024-03-31 02:00 → 03:00)
and autumn (2024-10-27 03:00 → 02:00) DST boundaries from late-evening
and early-morning Berlin times — the cases where the naive 24h
subtraction visibly slips a day.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:02:26 +02:00
Marc-André BombeckandClaude Opus 4.7 02b99553b9 chore(style): repo-wide prettier sweep before v1.4.6 tag
Long-overdue prettier sweep; prettier had drifted vs. checked-in code
across ~130 files (mostly long-argument line-break wrapping in TS and
table-padding in markdown). Pure formatting — no semantic changes —
and required to clear the v1.4.6 release gate's `pnpm format:check`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:27:17 +02:00
Marc-André BombeckandClaude Opus 4.7 1adda80944 feat(insights): widen per-card window from 30 days to 360+24 buckets
Every per-card KI insight generator was clipping each metric to the
last 30 daily means before sending the prompt. The model therefore
saw a one-month window per metric — far too narrow to detect drift
that plays out over a year, and the cause of repeated complaints that
a "Stable" trend assessment ignored an obvious 9-month decline.

New shared helper `applyPayloadBudget()` builds a `{ daily, monthly }`
view: the most recent 360 days as daily means (`dayOffset 0..359`)
plus 24 thirty-day windows for the older history (`monthOffset
12..35`). Empty buckets are dropped. If the canonical payload would
serialise above 50 KB the helper transparently re-buckets at 180
daily days so the prompt token budget stays bounded; a per-generator
`annotate({ payload_size_bytes })` makes the chosen window
observable in Wide Events.

All seven generators now consume the bucketed view as the metric
`series` field (general-status, blood-pressure-status, pulse-status,
weight-status, bmi-status, mood-status, medication-compliance-
status) — including their cross-metric joins (BP-pair, weight ↔ mean
BP correlation, medication-compliance per day) which are rebuilt on
top of the shared `dayOffset` instead of date strings. Mood and
medication queries widened to ~3 years so the older monthly window
can actually be filled.

One focused vitest per generator captures the prompt that goes to the
provider, JSON-parses the embedded snapshot, and asserts every
metric series carries both `daily` and `monthly` arrays with the
expected shape. The bucketing helper also has its own unit tests
covering offsets, leap-year-safe Berlin-day boundaries, and the
50 KB → 180-day fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:50:10 +02:00
Marc-André BombeckandClaude Opus 4.7 dc01ab7934 feat(insights): metric:MOOD inline charts + evidence-contract prompt
Bucket D phase 1 of the v1.4.3 polish pass — the AI insights felt
"static" because (a) findings repeated phrasing with no concrete
numbers and (b) the inline-chart machinery existed but was prompted
permissively, so the model rarely used it.

Two concrete moves, scoped to ship in this hotfix:

1. **`metric:MOOD` is now an allowlisted chart token.** Renderer in
   `<InsightAdvisorCard>` branches on `tokenKind(token)` — measurement
   tokens still mount `<HealthChart>`, but `metric:MOOD` mounts the
   dedicated, self-fetching `<MoodChart>` instead. The mood timeline
   was previously the most under-served by the AI prose because it
   couldn't be charted inline; now the model can illustrate adherence-
   risk and mood-drift findings with the same chart users see on the
   /mood page. Drift test was updated in lockstep — the v1.4.2 guard
   that asserted MOOD was excluded now asserts it's present, plus a
   new guard asserts COMPLIANCE is still excluded (no self-fetching
   wrapper exists yet for `<ComplianceLineChart>`; ship that in v1.5).

2. **Prompt grew an EVIDENCE CONTRACT and a forbidden-phrase list.**
   The base system prompt (DE + EN both) now requires every
   `findings[]` entry to put a concrete number from the snapshot in
   `value` ("138/85 mmHg", "+0.4 mmol/L vs. 30d-avg") instead of
   adjective prose ("slightly elevated"). Findings without a snapshot
   anchor MUST be omitted — variable cardinality 0..8 explicitly
   stated, sorted by salience, NO padding. The forbidden-phrase list
   blocks generic boilerplate that signals ungrounded output: "achte
   auf ausreichend Schlaf", "drink enough water", "regular exercise",
   "consult your doctor" (except in the disclaimer field which exists
   precisely for that line). The chart-token instruction flipped from
   passive ("Du DARFST") to active ("Bevorzuge Tokens immer dann,
   wenn …") with three explicit triggers — temporal claims, threshold
   references, window comparisons — that reliably surface chart-worthy
   findings.

Carved out of v1.4.3 for v1.5: telescoping multi-resolution payload
(14d daily + 10w weekly + 21mo monthly + yearly summary per QA
research 1) and the evidence-ref output schema extension (QA 2).
Both touch the prompt's input shape and would change every cache
key + token bill — they want their own phase.

Tests + typecheck green (671/671 — one new chart-tokens drift
guard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:41:29 +02:00
Marc-André BombeckandClaude Opus 4.7 ddab92e42b feat(insights): inline charts in AI findings via allowlisted metric tokens (#138)
* feat(insights): inline charts in AI findings via allowlisted metric tokens

Lets the AI insight advisor inline a Recharts panel directly under the
prose paragraph it talks about — e.g. a finding centred on systolic BP
renders the BP chart immediately beneath itself, instead of forcing the
user to scroll back to the dashboard charts.

Tokens are routed through a server-side allowlist:
- 13 fixed strings (one per MeasurementType plus MOOD and COMPLIANCE).
- A drift test fails if a new MeasurementType is added without a token.
- The greedy `[A-Z_]+` regex caps the parse so trailing junk like
  `metric:WEIGHT' onclick='alert(1)'` cleaves cleanly into the safe
  metric and inert React text — there is no markdown / no eval path.
- Hallucinated tokens (`metric:NUKE`) are silently dropped.
- The visible prose is run through `stripChartTokens` so the literal
  token never reaches the DOM.

Prompts (base + 7 domain prompts, EN + DE) tell the model it may embed
exactly one of the allowlisted tokens inside `summary` or a finding's
`label`/`guideline` — nothing else has any effect.

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(insights): drop metric:MOOD and metric:COMPLIANCE until dedicated charts exist

Review-pass M1: those two tokens routed through HealthChart, which
validates `type` against the `MeasurementType` Zod enum. MOOD and
COMPLIANCE aren't in the enum, so the request 422'd and the chart
panel rendered "no data" — silently degrading the most-promoted UX
path under prose that explicitly talked about the user's mood or
adherence.

Allowlist + every prompt now lists only the 11 tokens that actually
render a real chart. The drift test inverts the previous "must
include MOOD/COMPLIANCE" assertion to "must NOT include them",
locking the contract until dedicated MoodChart / ComplianceChart
components ship.

494 tests green, typecheck clean.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:09:27 +02:00
Marc-André BombeckandMarc-André Bombeck 751448494b feat(insights): AI insights remember the previous analysis + hero finding card (#129)
The AI insights surface had two long-standing weaknesses:

1. **Every analysis read like the user's first one.** The general-status
   and blood-pressure prompts saw only the latest 30 days of data, not
   the previous analysis the user already read last week. So the model
   restated the same status every day instead of saying "your BP is down
   4 mmHg from your last check" or "same elevated trend as last week".

2. **All findings looked the same.** The card listed every finding in a
   uniform compact row. The model is instructed to put the most
   clinically relevant finding first, but the UI didn't honour that
   ordering with any visual weight.

This PR closes both for the two highest-impact insight surfaces (general
status and blood pressure). Same pattern lifts to the other five
generators in a follow-up.

**`src/lib/insights/memory.ts`** — new helper that reads the user's
previous cached insight from the existing `audit_logs` cache rows
(scope-keyed `insights.{scope}.{locale}`), returns it as a typed
`PreviousInsightContext`, and formats it into a locale-aware
prompt-injection block. Default 12-hour age floor keeps the model from
treating today's earlier run as "previous". The block ends with an
explicit comparison instruction in the user's locale; passing `null`
context produces a "first analysis — no delta to surface" instruction
so the model doesn't hallucinate one.

**Wired into `general-status` and `blood-pressure-status` generators.**
Both prompts now accept an optional `previousContextBlock` argument.
When supplied, the model gets the previous narrative + the comparison
instruction inline above the snapshot. No schema change — caches keep
writing to the same audit-log rows the helper reads from. Empty for
first-run users.

**Hero finding card.** Top finding gets a severity-tinted gradient
(green/cyan/orange/red), 2-px coloured left border, larger typography,
and a descriptive eyebrow label ("Top finding" / "Worth attention" /
"Top alert"). Secondary findings keep the previous compact layout so
the visual hierarchy reads at a glance. Eyebrow labels translated
under `insights.heroFinding{Positive,Neutral,Attention,Warning}`.

Tests: 400 → 410 (+10 for the memory helper covering JSON cache,
legacy plaintext fallback, age-floor semantics, locale-aware
formatting).

Follow-up PRs:

- Wire the same memory helper into the remaining five generators
  (weight, pulse, BMI, mood, medication-compliance). Pattern is
  mechanical — the prompt signatures already accept the new optional
  argument.
- Dynamic chart inclusion via an allowlist token map
  (`metric:BLOOD_PRESSURE_SYS` → `<HealthChart>` inline). Out of scope
  for this PR.

Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
2026-05-08 12:30:31 +02:00