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.
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.
Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path.
Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container.
Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app.
The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces.
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.
Mostly table column normalisation under docs/audit/ + planning, object-
literal expansion in src/lib/ai/coach/snapshot.ts after the W4 F6 Apple
Health metric block landed, and small wrap/unwrap line adjustments in
the device + APNs routes / tests touched across W3/W4/W5 sessions.
No behavioural change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small reuse wins from the W6 simplify review:
- openapi/registry.ts swaps the lazy require() for a static import.
The ESLint disable + as-cast carried no real benefit; the only
callers always loaded the route table anyway.
- Extract useCoachPrefs() into a shared hook so the settings sheet
and message thread share one queryKey + one fallback shape. Removes
the drift between the two existing inline blocks.
- Replace the 9-deep wantsX boolean ladder in coach snapshot with a
single CoachScopeSource -> MeasurementType[] map. Drives both the
WHERE type IN (...) build and the appleHealthBlocks gate, so adding
a new metric is one entry instead of three parallel edits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1.4.22 B5 audit removed the placeholder cog from the Coach
drawer header because it had no real wiring. v1.4.23 H4 returns
the cog with an actual settings surface backed by a per-user
preferences row.
Schema: nullable JSONB column `users.coach_prefs_json` (migration
0038_coach_prefs). Null = legacy v1.4.22 defaults; non-null carries
`{ tone, verbosity, excludeMetrics, showEvidenceByDefault }` validated
by the Zod schema in `src/lib/validations/coach-prefs.ts`.
Routes: GET + PUT `/api/auth/me/coach-prefs`. PUT validates the body,
persists the canonical defaulted form, and emits an
`auth.me.coach-prefs.put` annotation with the four-knob meta. OpenAPI
registry covers both verbs; the spec regenerates without drift.
Coach prompt builder: `getCoachSystemPrompt(locale, prefs)` prepends a
per-user OVERRIDE paragraph for non-default tone (neutral / concise)
and verbosity (brief / detailed). Concise tone forces brief verbosity
regardless of the explicit selection — concise == short. Default
prefs leave the prompt unchanged so the v1.4.22 train stays in place.
Snapshot builder: reads the prefs row BEFORE any measurement query
and intersects `excludeMetrics` with the resolved scope. The model
never sees data the user opted out of — that's the load-bearing
order: filter first, query second.
UI: settings cog in the drawer header opens a right-edge `<Sheet>`
with three Select pickers (tone, verbosity) and one Switch column
(per-metric exclusions + showEvidenceByDefault). Save calls the PUT
route and invalidates the `["coach-prefs"]` query so the next reply
honours the new defaults without a reload. EN + DE i18n keys for
every label and hint.
Message-thread reads the same `["coach-prefs"]` cache and toggles the
`open` attribute on the evidence `<details>` element when
`showEvidenceByDefault` is true.
Tests +24 unit (coach-prefs schema, system-prompt prefix, snapshot
mock for new prefs row). Integration round-trip seeds a session,
PUTs a non-default prefs shape, asserts the GET reflects the saved
values + the DB row carries the canonical defaulted form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The W3-of-v1.4.22 sentinel parser silently dropped malformed rows and
fired `coach.keyvalues.parse_failed` only when the entire block was
unusable. Mixed-format drift (one row breaks, the other six parse)
landed without any signal. Surface it.
- `SentinelParseResult.malformedEntries[]` records each skipped row
with a typed reason (`missing_colon` / `value_overflow` /
`label_overflow` / `no_END_marker` / `byte_overflow` /
`schema_invalid`). Block-level failures (truncation, missing close
marker) land here as synthetic entries so the wide-event always
carries a typed cause.
- Per-row label and value caps moved out of the Zod schema into the
parser so the row-level reason can be `label_overflow` /
`value_overflow` rather than the catch-all `schema_invalid`.
- Chat route splits the annotation: `coach.keyvalues.parse_partial`
when at least one row parsed AND at least one row failed, the
existing `coach.keyvalues.parse_failed` stays for full-block
failures. Both annotations carry the per-line reason array.
- Backwards-compatible `parseKeyValueLine` wrapper preserved.
Tests +5: label_overflow, value_overflow, byte_overflow,
mixed-malformed entry assertions, and a tryParseKeyValueLine
direct-API surface check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the strict insight schema, Coach snapshot pipeline, and
provenance envelope with the seven Apple Health categories that landed
in Wave 2 (HRV, sleep, resting HR, steps, active energy, flights,
distance, VO2 max, body temperature). All fields are additive and
optional — web-only accounts pay zero extra SQL and the prompt's new
GROUND RULE 12 instructs the model to stay silent when the snapshot
doesn't carry the HealthKit blocks.
Ratchets PROMPT_VERSION 4.22.0 → 4.23.0 and refreshes the
sourceMetric / trendAnnotations enums in both EN + DE. Two new test
files pin the prompt body and the snapshot fan-out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a text-marker sentinel parser (`---KEYVALUES---` … `---END---`)
that strips the Coach's evidence block out of the prose before it
streams to the client, validates each line against a Zod schema, and
attaches the parsed entries to `provenance.keyValues`. The persisted
envelope round-trips so the disclosure renders on conversation
reload. Hard-capped at 8 lines and 1 KB to guard against
prompt-injection padding; malformed blocks log a wide-event and pass
the prose through unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the v1.4.20 "conversational summariser" persona with a partner
who sits alongside the user — warm, curious, conservative, with seven
numbered ground rules that prefer invitation over refusal when data is
missing and reserve specific numbers for a structured evidence block
at the end of each reply. The day-level timeline section from v1.4.21
stays verbatim so weekday questions still resolve cleanly. Bumps the
shared PROMPT_VERSION from 4.20.2 to 4.22.0 and broadens existing
schema tests off the 4.20.x pin onto the 4.x train.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `.planning/phase-v1421-hotfix-report.md` summarises the six
fixes (Daily Briefing regenerate · Coach day-level snapshot · Coach
prompt update · streaming-bubble dedup · drawer header · sources rail
scope picker), the test-count delta (2026 → 2036 unit), the
token-budget impact of the day-level snapshot (~190 → ~3000 tokens
per full-scope turn), and a recommendation to ship as a v1.4.20.1
hotfix.
STATE.md grows a Phase F section + status block tracking the six
commits on develop. Includes prettier polish across the touched
coach surfaces (sources-rail, drawer, message-thread, snapshot,
chat route).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the Sources rail listed BP / Weight / Pulse / Mood /
Compliance as a static legend. Marc asked for a real "what I can
see" surface — per-source checkboxes (include/exclude) plus a
window selector (last 7d / 30d / 90d / all-time) that flow through
the Coach request body.
Wire the controls all the way down:
- <SourcesRail> grows scope + onScopeChange props. Each row
becomes a 36px tall flex row with a real checkbox; the row
fades to 60% opacity when its source is excluded.
- A Select trigger above the list drives the window. Both control
surfaces share the existing insights.coach.window.* i18n keys;
add a new windowLabel key so the dropdown has an EN+DE label.
- <CoachDrawer> owns the scope state (resets on each mount per
the v1.4.20.1 plan — no conversation-level persistence yet) and
forwards the picked scope through useSendCoachMessage to the
chat request body. The payload only ships when the user has
narrowed away from the all-source last30days default so the
server can tell "no opinion" from "intentionally narrow".
- buildCoachSnapshot already accepts the optional CoachScope from
the previous commit; it now flows end-to-end.
No new dependencies — checkboxes are plain inputs styled with
Tailwind + Dracula tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Coach prompt only said "ground every number in the
SNAPSHOT" without telling the model that the snapshot now carries a
timeline.recent block of day-level rows with weekday labels.
Without that pointer the model kept defaulting to aggregate-only
phrasing on day-specific questions.
Add a DAY-LEVEL READINGS section to both EN and DE prompts:
- explain the timeline.recent / timeline.weekly shape
- tell the model to answer day-specific or weekday-specific
questions out of timeline.recent and to cite the actual reading
with date + weekday
- when no row exists for the day named, say so plainly and offer
the closest available day rather than the aggregate
- older weekday questions fall back to timeline.weekly
- the scope.sources field is the conversation's source-allowlist;
a question about a non-listed metric should be acknowledged as
out-of-scope rather than answered from the aggregate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Coach snapshot used to ship only aggregated statistics (mean,
slope, SD, range, count) per metric. A turn that asked
"why was BP higher on Monday?" had no day-level rows to ground in
and the model fell back to apologetic aggregate-only phrasing
("from the snapshot I cannot isolate the specific Monday").
Extend buildCoachSnapshot to fold in:
- timeline.recent: one row per UTC day for the last 14 days, each
tagged with its weekday so the model can answer day or weekday
questions directly. BP rows pair systolic + diastolic; days with
only one side measured are dropped to avoid fabricating a
complement.
- timeline.weekly: ISO-week buckets covering days 15..N where N is
the configured window. Keeps the prompt budget tight on 30/90-day
windows while still letting the Coach cite older weeks.
- per-day medication adherence rows from MedicationIntakeEvent so
the Coach can answer "did I miss my dose Tuesday?" without
inventing the schedule.
Also add the optional `scope` field to coachChatRequestSchema (sources
+ window) so the upcoming sources-rail picker can narrow the snapshot
without breaking older clients — missing fields fall through to
all-source last30days.
A typical 30-day, 5-source snapshot lands around 2.4-2.8 KB JSON
(≈700-900 prompt tokens) — comfortably inside the 25k/day Coach token
budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- weekly-report eyebrow chip drops uppercase + tracking-wide for the
sentence-case rule the rest of the app follows; print stylesheet
now neutralises bg + colour so monochrome output reads clean
(Design-M4, Design-L3).
- correlation-card "Try a 7-day experiment" CTA flips from default to
outline while disabled. Disabled-primary read as "the system is
broken right now"; outline reads as "this is on the roadmap"
(Design-M7).
- coach-drawer settings cog aria-label switches to the coming-soon
tooltip copy so SR users hear the same "arrives in v1.4.21" message
sighted users see (Design-M9).
- sources-rail static "fresh" dot becomes aria-hidden — v1.4.20 has
no real freshness state behind it; SR users no longer hear "Fresh"
five times in a row (Design-L4).
- summariseTitle slices on Unicode code points rather than UTF-16
code units so a user message ending mid-emoji ("🩺") no longer
produces a half-character on disk (Code-LOW-04).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every POST /api/insights/chat called extractFeatures(userId, false),
which ran prisma.measurement.findMany({ where: { userId } }) with no
date filter — fetching every measurement the user had ever recorded.
For power users with multi-year Withings imports that's tens of
thousands of rows on every Coach turn.
Add an optional sinceDays bound to extractFeatures (default unchanged
= full history; analytics dashboard + insights/generate keep their
existing behaviour) and have the Coach snapshot pass sinceDays: 90.
The Coach already only narrates the last7 / last30 / last90 windows,
so the 90-day floor preserves every metric the snapshot surfaces.
Fixes Senior-HIGH-3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The route gates on requireAuth (cookie or bearer), enforces the daily
token budget, runs the prompt-injection / off-topic refusal scanner,
and only then walks the user's provider chain.
The conversation is created via withIdempotency() when no
conversationId is supplied, so a retry under the same Idempotency-Key
replays the original assistant reply rather than spinning up a second
thread. Existing-thread turns skip the wrap.
Replies stream as SSE: one "token" frame per word-sized chunk, then a
"provenance" frame describing the windows + metrics the assistant
could see, then a "done" frame carrying the persisted message id.
Provider failure cascades to a single "error" frame and the assistant
message is never persisted. Conversation history is capped at 20 turns
in the prompt window via a synthetic summary placeholder so cost stays
bounded.
`buildCoachSnapshot()` reuses the analytics features pipeline so the
Coach narrates the same numbers the dashboard tiles render.
`getCoachSystemPrompt()` extends the strict-insight ground rules with
conversational framing while keeping the zero-hallucination rule.
5 integration tests pin the round-trip, refusal short-circuit, budget
429, foreign-conversation 404, and validation 422 paths against the
postgres testcontainer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the foundation layer for the AI Coach:
- `types.ts` defines the SSE event shapes (token / provenance / done /
error), the chat-request zod schema, and the conversation/message
DTOs the route + UI share.
- `persistence.ts` encrypts every message body with the active
ENCRYPTION_KEYS entry before insert and decrypts on read.
`summariseTitle()` keeps the rail snippet ≤80 chars with a smart
word-boundary cut.
- `budget.ts` introduces a per-(user, UTC-day) ledger with a 25 000-
token daily cap. `enforceBudget()` throws HttpError(429,
"coach.budget.exceeded") so the route can short-circuit before
spending. UTC alignment matches the LLM-provider billing cycle.
- `refusal.ts` runs prompt-injection and off-topic detection on inbound
messages. Pattern bank covers EN+DE injection wording, jailbreak
variants, and obvious off-topic asks (weather, code-help, recipes).
Health-allow tokens override the off-topic deny list so mixed
questions land on-topic.
- i18n keys `insights.coach.refusal*` + `insights.coach.error*`
added to both locale bundles.
- 28 unit tests cover the helpers with mocked Prisma.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>