Commit Graph
605 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.7 ff1e335c19 feat(charts): storyboard annotations on HealthChart
Add an additive annotations[] prop to <HealthChart> that paints a
vertical Recharts <ReferenceLine> per AI-narrated event on the
underlying timeline. The line + tooltip + comparison overlay remain
untouched — annotations sit between the cartesian grid and the data
lines so they read as orientation, never as data.

The /insights page wires advisor.payload.insights.storyboardAnnotations
into the 90-day BP chart, mapping the four canonical categories to
Dracula colours (medication=pink, event=cyan, milestone=green,
warning=orange).

Pure helper resolveAnnotationPositions() handles the date->pointIndex
mapping with a 7-day snap window (so annotations don't stack on
monthly buckets) and a 24-char truncation for the small-viewport
label. The unit suite pins exact-day pinning, near-day snapping,
out-of-window dropping, and label truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:17:45 +02:00
Marc-André BombeckandClaude Opus 4.7 f1868453af feat(insights): hero strip weekly-report banner card
Add a slim banner-card on <HeroStrip> that surfaces a fresh weeklyReport
between the title block and the action row. Three actions:

- Read   -> Next.js Link to /insights/report/[week]
- Share  -> navigator.share() with clipboard fallback + sonner toast
- Export PDF -> deep-link to the report URL with ?print=1 so the report
  page auto-fires window.print() after first paint

The banner only renders when the parent passes weeklyReportReady; the
insights page derives that prop from advisor.payload.insights.
weeklyReport so the banner appears the moment a fresh PROMPT_VERSION
4.20.2 generation lands.

EN + DE translations under insights.heroBanner.* (Read / Share / Export
PDF + share success/failure toasts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:14:27 +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 6d4cba1aad feat(insights): weeklyReport + storyboardAnnotations schema + prompt
Extend aiInsightResponseSchema with weeklyReport (ISO week + summary +
goingWell/worthWatching/tips bullets + optional dataQualityNotes) and
storyboardAnnotations (date + label + category + detail; max 20).

Both blocks are nullable + optional so legacy 4.20.1 caches round-trip.

Bump PROMPT_VERSION 4.20.1 -> 4.20.2 and add GROUND RULE 10 (weekly
report sections match layout, conservative phrasing, no causal claims)
+ GROUND RULE 11 (storyboard annotations are factual user-logged events
with neutral prose) to both EN and DE prompts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:06:04 +02:00
Marc-André BombeckandClaude Opus 4.7 bbd00ca848 docs(planning): mark Wave-B B3 complete + report
Tick B3 in `.planning/STATE.md` against the six commit drops on
`develop` (correlation maths, schema + prompt rule,
TrendAnnotation + TrendsRow, CorrelationCard + CorrelationRow,
analytics route + page wiring, this report). Replace the stale
v1.4.16 host-load report with the v1.4.20 narrative — what
shipped, synthetic-data spot checks for each hypothesis, and
flagged-uncertain items for maintainer review (confidence-band
thresholds, eta-squared mapping, interpretation localisation
strategy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:54:53 +02:00
Marc-André BombeckandClaude Opus 4.7 c83cb2311d feat(insights): wire CorrelationRow + TrendsRow into /insights
Compute the three pre-defined correlation hypotheses
(BP × compliance, mood × pulse, weight × weekday) on the trailing
30-day window in `/api/analytics` and surface the typed result
shapes on the response payload. Each runner is gated on n >= 14
+ p < 0.05; below the bar the result.status === "insufficient"
and the per-card EmptyState paints in place of a misleading
near-zero scatter.

The insights page now mounts:

- `<CorrelationRow>` between the Daily Briefing and the Advisor
  card — the layout slot the artboard reserves for the patterns
  surface.
- `<TrendsRow>` immediately below it, fed from
  `advisor.payload.trendAnnotations` (lifted client-side via
  `trendAnnotationsSchema.safeParse`). Legacy cached payloads
  predating PROMPT_VERSION 4.20.1 surface the per-metric
  empty-state hint instead of a half-rendered annotation.

The wide-event annotation captures per-hypothesis status so admin
observability can attribute coverage gaps without re-querying
the DB.

10 source-scan tests pin the load-bearing imports, the JSX mounts,
the analytics-route surfacing, and the malformed-payload null
fallback in the advisor hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:53:29 +02:00
Marc-André BombeckandClaude Opus 4.7 42591c6cec feat(insights): CorrelationCard + CorrelationRow components
Two cards-and-row primitives for the Insights redesign:

- `<CorrelationCard>` — single hypothesis. Tone-coloured left bar,
  scatter sparkline (deferred-bundled), confidence chip, source
  chip ("based on N paired readings · last 30 days"), interpretation
  prose, and a disabled "Try a 7-day experiment" CTA with a
  Coming-soon tooltip. Below-threshold results render the
  EmptyState in place of the chart so we never paint a misleading
  near-zero scatter.
- `<CorrelationRow>` — 2-up on `>= md`, single column on `< md`.
  Row-level correlation-disclaimer ("Patterns are observational,
  not causal — talk to your doctor if BP runs high.") sits below
  the card grid once per row, not per card.

13 tests cover OK + insufficient rendering, the disabled CTA's
tooltip wiring, the confidence chip variants, the disclaimer
position, the responsive grid, and the EN / DE locale switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:49:00 +02:00
Marc-André BombeckandClaude Opus 4.7 2ae69d7e64 feat(insights): TrendAnnotation + TrendsRow components
Add two presentational pieces for the Insights redesign:

- `<TrendAnnotation>` — pure renderer for the AI-authored
  one-sentence annotation that sits below each Trends-row chart.
  Empty state is locale-aware ("Awaiting more data to annotate
  this trend"). Optional confidence chip surfaces a discrete
  low / moderate / high band.
- `<TrendsRow>` — 3-up responsive grid (1 column < md, 3 columns
  >= md) wrapping BP / Weight / Mood mini charts with their
  annotations. Recharts ships through the existing `next/dynamic`
  defer-load pattern so the bundle savings stay intact.

Adds the EN + DE i18n keys (trendsRow, trendAnnotation,
correlationRow). 15 unit tests cover the locale switch, the
empty-state hint per metric, the confidence chip variants, and
the row layout chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:47:08 +02:00
Marc-André BombeckandClaude Opus 4.7 f9afaf77a0 feat(insights): trendAnnotations schema + prompt rule
Extend `aiInsightResponseSchema` with an optional, nullable
`trendAnnotations: { bp?, weight?, mood? }` block — one short
sentence per metric, capped at 200 characters. Each field is
independently optional so the model emits only the metrics it can
ground in the snapshot.

Insight-generator prompt picks up GROUND RULE 9 (EN + DE):
single-sentence, observational, ≤ 200 chars, banned causal
phrases enumerated. PROMPT_VERSION bumps 4.20.0 → 4.20.1 to
signal the format change so cached payloads from B1 still parse
through the `.nullable().optional()` field.

13 tests cover the boundary (200 chars), the empty-string
rejection, the legacy-payload round-trip, and the EN/DE prompt
rule presence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:44:20 +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 90d3a80267 docs(planning): tick B2b complete, record phase B2b report
B2b drawer UI shipped in four atomic commits on develop:
- TanStack Query hooks + SSE streaming client
- Drawer shell + message thread + source chips
- Composer + history rail + sources rail
- /insights wires the drawer via the hero strip's "Ask the coach"
  button + suggested-prompt chips

Tests: 1781 → 1833 unit (+52). Integration unchanged at 78.
Six new test files cover the streaming parser, every drawer column,
the thread state machine, locale-aware labels, and the page wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:34:10 +02:00
Marc-André BombeckandClaude Opus 4.7 e54179997b feat(insights): wire Coach drawer into /insights via hero strip
Closes the loop between the hero strip's "Ask the coach" /
suggested-prompt chips and the Coach drawer:

- `<HeroStrip>` gains an `onAskCoach` prop. When supplied, the
  "Ask the coach" button drops its disabled state + Coming-soon
  tooltip and clicking calls the handler. Older parents that
  haven't adopted the drawer still get the disabled affordance —
  no breaking change.
- `<SuggestedPrompts>`'s `onPick` already routed up via
  `onPickPrompt`; the page wires it to a setter that pre-fills the
  drawer's input AND opens it in the same tick.
- `/insights` owns `coachOpen` + `coachPrefill` state. The drawer
  mounts with `key={prefill ?? "blank"}` so the lazy `useState`
  initialiser fires fresh on every chip transition, sidestepping
  the `react-hooks/set-state-in-effect` rule.

Tests:
- Updates the hero-strip ratchet to allow either disabled (no
  handler) or enabled (handler supplied) states.
- Adds three guards under `insights-polish.test.ts` that pin the
  page-side wiring (CoachDrawer import, hero handler hook-up,
  state ownership).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:32:14 +02:00
Marc-André BombeckandClaude Opus 4.7 8dac3891b8 feat(coach): composer, history rail, sources rail
Fills the three drawer slots from commit 2 with their full
implementations:

- `<CoachInput>` is a Dracula-styled `<textarea>` card. Plain Enter
  and Cmd/Ctrl+Enter both submit; Shift+Enter inserts a newline.
  The mic button is disabled with an "Voice input arrives with the
  iOS app in v1.5" tooltip — the natural shipper of voice is the
  native client. A footer disclaimer ("Coach replies are
  generated. Clinical decisions belong with your doctor.") sits
  beneath the box.
- `<HistoryRail>` reads from `useCoachConversations()`, exposes a
  substring filter, marks the active conversation, and offers a
  hover-revealed delete affordance with a confirm-then-go flow
  driven by `useDeleteCoachConversation()`.
- `<SourcesRail>` lists the five contracts the snapshot helper
  surfaces (BP, weight, pulse, mood, compliance) with a static
  fresh-dot indicator. A v1.4.21 follow-up swaps in
  `<IntegrationStatusPill>` once the rail decides whether to lean
  on `/api/auth/me` or a dedicated `/api/insights/data-sources`
  endpoint.
- `<CoachDrawer>` mounts these as the defaults so callers only
  need to pass open + onOpenChange + prefill; the slot props stay
  as escape hatches for tests.

Tests added: composer keyboard / disabled / streaming states (10),
sources-rail render + locale (5), history-rail render + locale +
empty / active states (7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:30:04 +02:00
Marc-André BombeckandClaude Opus 4.7 195448ae19 feat(coach): drawer shell, message thread, source chips
Lays the visual scaffold for the AI Coach drawer (B2b):

- `<CoachDrawer>` wraps `<Sheet side="right">` and exposes the
  three-column layout from the artboard (history rail · message
  thread · sources rail) on `lg+`. Mobile (<lg) collapses to a
  single column with the message thread in focus and the rails
  hidden — they get reachable via tray menus in v1.4.21.
  History / sources / composer are slot props so the parent wires
  them in commit 3 without coupling them to the shell.
- `<MessageThread>` renders user / assistant bubbles, supports an
  in-flight streaming bubble fed by `useSendCoachMessage()`, and
  preserves a "user-pinned-to-bottom" auto-scroll heuristic so
  reading earlier turns doesn't yank the viewport when a new token
  lands.
- `<SourceChips>` renders the labels-only provenance envelope —
  metric + window + n-count, no raw values — matching the wire
  contract from B2a.
- i18n keys land under `insights.coach.*` for tagline, new chat,
  composer placeholder, history rail, sources rail, metric labels,
  and window labels. Both locales updated.

Drawer state (open / prefill) is consumed via a controlled prop +
lazy `useState` initialiser; the close path resets thread + composer
in the `onOpenChange` handler so we don't trip
`react-hooks/set-state-in-effect`.

Tests cover: source-chip render permutations (8), message-thread
empty / persisted / streaming / error states (8), provenance
locale-aware labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:34 +02:00
Marc-André BombeckandClaude Opus 4.7 c42c0a50cb feat(coach): TanStack Query hooks + SSE streaming client
Adds the data layer for the AI Coach drawer (B2b):

- `useCoachConversations()` reads `GET /api/insights/chat` for the
  history rail (default page; cursor pre-wired for v1.4.21 infinite
  scroll).
- `useCoachConversation(id)` reads the single decrypted thread.
- `useDeleteCoachConversation()` removes the thread with optimistic
  list cache update + rollback on failure.
- `useSendCoachMessage()` POSTs to `/api/insights/chat`, walks the
  body via `fetch().body.getReader()`, and decodes SSE frames
  (`data: <json>\n\n`) with `TextDecoder`. Token / provenance / done /
  error events update a `streaming` state object that the message
  thread will render as the live assistant bubble. On `done` we
  invalidate the conversation cache + the rail so the next mount sees
  the canonical persisted shape.
- `parseSseChunk()` is exported as a pure helper so the parser is
  unit-testable without a live network stream — 11 cases cover
  single-frame, multi-frame, byte-by-byte interleaving, malformed
  payload tolerance, and the full token + provenance + done round-trip.

No third-party SSE library introduced — `fetch` + `getReader()` is
enough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:20:04 +02:00
Marc-André BombeckandClaude Opus 4.7 e7287d34e7 docs(planning): tick B2a complete, record phase B2a report
The Coach backend half of v1.4.20 wave B is complete; STATE.md
splits B2 into B2a (backend, done) + B2b (drawer UI, deferred to a
later dispatch) and records the test-count delta plus the migration
file in the status block. phase-B2a-report.md captures the five-
commit timeline, schema, helpers, route surface, token-budget
rationale, provenance JSON shape, and flagged-uncertain items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:13:09 +02:00
Marc-André BombeckandClaude Opus 4.7 3168ba8e0a feat(coach): GET list, GET single, DELETE conversation endpoints
GET /api/insights/chat returns the caller's conversations newest-first
with cursor pagination (default limit 20, hard cap 50). Each row
carries id, title, timestamps, and a message count so the rail can
render without paying the per-message decrypt cost.

GET /api/insights/chat/[id] fetches one conversation with every
message decrypted in memory before serialisation. Provenance metadata
round-trips alongside the assistant turns.

DELETE /api/insights/chat/[id] hard-deletes the thread; the FK chain
cascades the message rows.

Cross-user lookups map to 404, never 403, so the existence channel
never leaks across accounts.

6 integration tests cover the list ordering, owner-only filter,
decryption round-trip, and the 404 boundary on both verbs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:11:12 +02:00
Marc-André BombeckandClaude Opus 4.7 16f3ece90f feat(coach): add POST /api/insights/chat SSE streaming endpoint
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>
2026-05-10 16:07:47 +02:00
Marc-André BombeckandClaude Opus 4.7 a68557d04b feat(coach): types, persistence, budget, and refusal helpers
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>
2026-05-10 16:01:15 +02:00
Marc-André BombeckandClaude Opus 4.7 5527001a01 feat(coach): add CoachConversation, CoachMessage, CoachUsage models
Persist Coach chat threads with AES-256-GCM-encrypted message bodies
and a per-user, per-day token-spend ledger. Encryption reuses the
active ENCRYPTION_KEYS entry so rotation continues to work without a
backfill. metric_source_json carries provenance labels only (window +
metric names, never raw values), keeping it queryable for analytics.

Cascade-on-user-delete is wired through the FK chain so GDPR delete
removes Coach data with the rest of the account. The integration-suite
truncation list picks up the new tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:54:28 +02:00
Marc-André BombeckandClaude Opus 4.7 3bdb4380ad docs(planning): tick B1 complete, record v1.4.20 phase B1 report
STATE.md: Wave B1 ticked (hero strip / daily briefing / suggested
prompts / schema / prompt bump). The Apple-Health vitals tile row
is annotated as descoped + deferred to B3 — the per-section status
cards already render the same numbers, so a tile row above them
would duplicate the surface.

New `phase-B1-v1420-report.md` captures the 5 SHAs (9873363 →
7d29596), the test-count delta (1672 → 1753, +81 tests), the
deferral list (Coach wiring → B2, weekly-report → B4, Health Score
→ B5, Briefing variants → backlog, source-chips → B3), and 5
prompt-copy decisions flagged for maintainer review.

The older `.planning/phase-B1-report.md` (v1.4.18 achievements
marathon) is left untouched; v1.4.20's report uses the v1420
suffix to avoid the collision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:47:56 +02:00
Marc-André BombeckandClaude Opus 4.7 7d29596998 feat(insights): wire <HeroStrip> + <DailyBriefing> into /insights
Replaces `<InsightsPageHero>` with the new `<HeroStrip>` band and
mounts a full-width `<DailyBriefing>` directly below it, ahead of
the existing advisor + status cards which remain untouched.

`useInsightsAdvisorQuery` now lifts the optional `dailyBriefing`
block off the cached payload and validates it client-side via
`dailyBriefingSchema.safeParse`. A malformed cached briefing falls
back to null so the briefing card paints its empty-state CTA
instead of a half-rendered card. Legacy v1.4.19 cached payloads
(no `dailyBriefing` field) round-trip unchanged.

The CompareToggle migrates from the hero meta band into the
DailyBriefing's metaSlot — keeps the page-level comparison control
adjacent to the briefing it scopes.

The hero greeting reads the user's username (HealthLog has no
separate display-name field) and stops at the first whitespace so
"alice" / "alice.smith" both surface "Good morning, alice".

`insights-polish.test.ts` v1.4.19 A3 ratchet updated: the original
"exactly one onRegenerate prop on the page" assertion was a
string-count check that mis-fires now that the briefing card's
empty-state CTA legitimately wires the same handler. The
load-bearing constraint stays — the advisor card must NOT carry
onRegenerate — and is enforced via a tighter JSX-block check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:44:40 +02:00
Marc-André BombeckandClaude Opus 4.7 59e63a0641 feat(insights): add <HeroStrip> + Dracula gradient utilities
New `src/components/insights/hero-strip.tsx` — replaces the v1.4.16
`<InsightsPageHero>` band with the wider strip from the v1.4.20
redesign:

  - locale-aware time-of-day greeting (morning / afternoon / evening
    / night → 4 i18n keys, with "night" mapping to "Good evening" in
    both locales since 03:00 + Guten Morgen reads odd)
  - briefing-paragraph subtitle (falls back to a generic line when
    no briefing is cached)
  - personal-baseline + 'Generated <relative-time>' meta band
  - 3-button action row: weekly-report (disabled, B4), ask-the-coach
    (disabled, B2), re-run analysis (wired through onRegenerate).
    Disabled buttons surface a 'Coming soon' tooltip via title= so
    we don't drag in a Radix tooltip provider for static labels.
  - <SuggestedPrompts> chip strip below the action band; onPickPrompt
    is a TODO until B2 lands the Coach drawer (parent passes a no-op)

New CSS utilities in `globals.css`:
  - .hero-gradient — Dracula purple → pink linear gradient over card
  - .glow-purple — soft purple aura via box-shadow
  - @keyframes pulseDot + .pulse-dot — for the future "live" dot
    affordance; reduced-motion users get a static dot

Old `<InsightsPageHero>` stays in place (used by the dashboard
preview) and gains a JSDoc @deprecated note pointing to the new
component. EN + DE translations added in lockstep.

Test suite: +22 specs covering greeting buckets per locale + per
hour, paragraph fallback, disabled-button affordance, regenerate
wiring, suggested-prompts mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:41:33 +02:00
Marc-André BombeckandClaude Opus 4.7 92f332efe6 feat(insights): add <DailyBriefing> card component
New `src/components/insights/daily-briefing.tsx` renders the
full-width briefing card from the v1.4.20 redesign — narrative
paragraph + 0-5 key-finding rows below it, each row carries a
tone-coloured left bar (green/orange/cyan), metric icon, headline,
detail line, and an optional delta badge in the matching tone.

States covered:
  - loaded: paragraph + findings + optional 'Generated <time>' meta
  - empty (briefing=null + !loading): EmptyState with 'Generate
    briefing' CTA wired through onRegenerate
  - loading: animate-pulse skeleton matching the rest of the app's
    loading affordance
  - empty findings: paragraph still renders; the findings block
    hides cleanly so the card doesn't paint a void section header

Optional metaSlot in the card header so the comparison toggle can
migrate from the hero to the briefing card in commit 5.

EN + DE translation keys added (insights.dailyBriefing.*). Test
suite: +18 specs covering rendering + states + locale + tone bars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:38:52 +02:00
Marc-André BombeckandClaude Opus 4.7 9713bf1606 feat(insights): add <SuggestedPrompts> chip-strip component
New `src/components/insights/suggested-prompts.tsx` renders the
"Try asking" chip row from the v1.4.20 Insights redesign hero strip.
Five default prompts (whyMonday, weightVsPulse, weekVsMonth,
tellMyDoctor, medicationWorking) read from
`insights.suggestedPrompts.*`, click invokes `onPick` with the
localised string so the parent can route to the future Coach drawer.

Mobile-first: chips wrap to multiple rows on narrow viewports.
Dracula purple chip styling matches the existing card vocabulary.
Custom prompts array accepted for tests + future per-user sets.

EN + DE translation keys added in lockstep — i18n-locale-integrity
test stays green. Test suite: +9 specs covering default rendering,
locale switching, custom prompts, and chip-slot pinning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:37:15 +02:00
Marc-André BombeckandClaude Opus 4.7 9873363fa3 feat(insights): add Daily Briefing schema + prompt v4.20.0
Extends `aiInsightResponseSchema` with an optional `dailyBriefing`
block (paragraph + 0-5 keyFindings, each with tone / headline / detail
/ delta / sourceWindow / sourceMetric). Nullable + optional so v1.4.19
cached payloads still parse cleanly.

Bumps PROMPT_VERSION 4.19.0 → 4.20.0 and adds GROUND RULE 8 in both
EN and DE system prompts, instructing the model to emit a briefing
block grounded in the user's own snapshot numbers (no extrapolation,
no medical advice claims, hard-cap at 5 findings).

TDD: new daily-briefing-schema.test.ts covers legacy back-compat,
well-formed parsing, > 5 findings rejection, empty-paragraph
rejection, finding-field validation, and the PROMPT_VERSION 4.20.x
match. Existing medical-reference-prompt.test.ts assertion stays
broad enough to ride the bump.

+31 tests (1672 → 1703); typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:35:26 +02:00
Marc-André BombeckandClaude Opus 4.7 81a40f7955 docs(planning): F6 docs accuracy audit recorded
- STATE.md: F6 ticked complete with the two healthlog-docs commits
  (19eb8de S3 + arm64 + 3d3ea21 production-state alignment)
- phase-F6-report.md: 38 pages checked, 2 CRIT + 10 HIGH inline,
  9 MED + 5 LOW deferred, 2 flagged-but-uncertain items captured
- v1421-backlog.md: F6 audit findings appended (co-edited with F5)

Foundation phases F0/F1/F2/F5/F6/FX complete; develop ready for B1
dispatch (Insights redesign hero + Daily Briefing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:28:36 +02:00
Marc-André BombeckandClaude Opus 4.7 a2d959c7c1 docs(planning): F5 best-practice audit recorded, v1.4.21 backlog seeded
- STATE.md: F5 ticked complete with the four atomic commits referenced
- phase-F5-report.md: scope + per-commit justification + deferral list
- v1421-backlog.md: new backlog file for F5 LOW carry-over plus the
  FX cleanup carry-over (source-comment sweep, bilingual CHANGELOG,
  CLAUDE.md/AGENTS.md filenames)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:24:19 +02:00
Marc-André BombeckandClaude Opus 4.7 f7977d113e chore(package): add description, repository, license metadata
Fills the npm package metadata fields that GitHub package-cards,
package.json scrapers, and SBOM tooling pick up: description, license
(AGPL-3.0-only), homepage, repository, bugs, keywords. No runtime
behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:23:20 +02:00
Marc-André BombeckandClaude Opus 4.7 779e5a36ca chore(github): expand dependabot to GitHub Actions + Docker
Existing npm group covered runtime deps. Adds weekly bumps for the
.github/workflows/* action versions and the base image referenced in
Dockerfile so neither falls behind silently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:23:17 +02:00
Marc-André BombeckandClaude Opus 4.7 072941bae6 chore(github): add issue + PR templates
- bug_report.yml: structured fields for version, deployment, repro
  steps, logs; routes security to SECURITY.md
- feature_request.yml: problem-then-proposal template, surface
  multi-select aligned with the app's actual touch points
- config.yml: disables blank issues, points usage questions to docs
  + Discussions, points security to private advisories
- PULL_REQUEST_TEMPLATE.md: reminds the contributor to target develop
  (not main), checklist mirrors local CI, no-PII reminder

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:23:15 +02:00
Marc-André BombeckandClaude Opus 4.7 e7b6b27451 chore(community): add Code of Conduct (Contributor Covenant 2.1)
Adopts the canonical Contributor Covenant 2.1 (verbatim, with
conduct@bombeck.io as the enforcement contact). CONTRIBUTING.md links
to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:23:07 +02:00
Marc-André BombeckandClaude Opus 4.7 fe660d407c docs(contributing): document develop / main branch model
CONTRIBUTING.md gains a "Branch Model" section with an ASCII flow
diagram and the two-rule summary (develop = daily target, main =
release-only). Hotfix flow + end-user-vs-contributor guidance
included. Mirror page on docs.healthlog.dev landed in healthlog-docs
5d96861.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:19:05 +02:00
Marc-André BombeckandClaude Opus 4.7 77f9741672 docs(planning): F1 develop branch + FX user-facing cleanup recorded
- STATE.md: F1 ticked complete, F3+F4 collapsed into FX (combined
  cleanup wave), all bullets reflect what shipped on origin/main
- phase-F1-report.md: GHCR workflow audit + branch model contract
- phase-FX-report.md: 3-repo PII/jargon/German scrub (HealthLog,
  healthlog-docs, healthlog-landing) with carry-overs and the
  pre-release PII grep gate spec

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:17:03 +02:00
Marc-André BombeckandClaude Opus 4.7 a1cf9bcb37 docs: scrub PII + internal jargon + German leaks from user-facing artifacts
Retroactive cleanup of CHANGELOG.md and docs/audit/v1.4.X-summary.md
files. Removes maintainer's real name, specific health figures tied to
the live tenant, internal release-process jargon, and stray German
strings in English-only release notes. Pre-release PII-grep gate added
to the v1.4.20 release pipeline. No git history rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:14:28 +02:00
Marc-André BombeckandClaude Opus 4.7 f80492a5a3 docs(planning): bootstrap v1.4.20 STATE+ROADMAP, archive v1.4.19
- STATE.md replaced with v1.4.20 scaffold (F0/FX foundation, B1-B5
  Insights redesign per phase-D-v1419-product-lead-review.md, Wave D
  QA, Phase E release)
- ROADMAP.md: v1.4.20 milestone at top, v1.4.19 archived as previous
  milestone summary
- phase-E1-report.md + phase-E3-report.md: dangling v1.4.19 release
  artefact updates committed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:27 +02:00
Marc-André BombeckandClaude Opus 4.7 3fa07ad54b docs(planning): v1.4.20 marathon handoff document
Captures the next-session brief: foundation tasks F1-F6 (develop branch
model, CONTRIBUTING docs, retroactive jargon cleanup, release-notes
language audit, repo best-practice audit, doc-hallucination QA), then
the 5-phase Insights redesign + AI Coach work, then standard QA + release
on the new branch flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:46:46 +02:00
Marc-André BombeckandClaude Opus 4.7 00daf962ea docs(audit): v1.4.19 release summary
Marc-Brief plus what-landed / deferred / CI / production-deploy /
v1.4.20-preview sections. Mirrors the v1.4.18 report shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:43:24 +02:00
Marc-André BombeckandClaude Opus 4.7 89f00cf832 chore(release): v1.4.19
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.19
2026-05-10 14:34:01 +02:00
Marc-André BombeckandClaude Opus 4.7 1f5ad68033 docs(planning): record Wave D reconcile + reviewer findings
STATE update + reconcile report captures the six reviewer briefs and
the inline triage outcome (one CRITICAL fixed, seven HIGH applied,
three HIGH carried over). Backlogs updated: the v1.4.20 file gains a
"Phase D reconcile carry-over" block with file:line references; the
v1.5 file inherits the strategic risk watchlist (conversation
persistence eviction, correlation FDR control, streaming UX,
prompt-injection refusal, safeParse legacy-payload audit pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:30:57 +02:00
Marc-André BombeckandClaude Opus 4.7 1f0d9ad1f7 style: prettier on touched admin section
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:30:10 +02:00
Marc-André BombeckandClaude Opus 4.7 977f124b9e fix(ui): widen TabsList padding so the focus ring fits inside the clamp
`overflow-y-hidden` on the tabs strip clipped the bottom of the
focus-visible ring (`ring-[3px]`) on triggers because the parent
padding was only 3 px — the 3 px ring ended exactly at the
content-box edge. Bump the padding to `p-1` (4 px) so keyboard users
see the full ring while the y-axis clamp still hides the painted
scrollbar Marc reported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:26:18 +02:00
Marc-André BombeckandClaude Opus 4.7 5a8ad3d895 fix(charts): drop the redundant resize sync from useViewportWidth
The useState lazy initialiser already runs on the client during
hydration and reads the live `window.innerWidth`, so the post-mount
handler() call was a no-op that triggered a setState inside useEffect.
The eslint react-hooks/set-state-in-effect rule flags this pattern;
removing it lets the resize + orientationchange listeners alone drive
updates without a hydration-time double set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:25:50 +02:00
Marc-André BombeckandClaude Opus 4.7 1258b24864 fix(admin): render token-name timestamps in Berlin time
The auto-issued token-name viewer rebuilt the dd.MM.yyyy HH:mm suffix
from `Date.getUTC*` accessors, producing 19:46 instead of the 21:46 a
Berlin-time admin would expect for a token issued at 19:46 UTC. Route
the parsed Date through `Intl.DateTimeFormat("de-DE", { timeZone:
"Europe/Berlin", … })` so the embedded timestamp matches every other
display surface in the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:25:28 +02:00
Marc-André BombeckandClaude Opus 4.7 6b35cad079 refactor(v1.4.19): apply simplify-review safe suggestions
Mechanical clean-up sweep across five hot spots flagged by the simplify
audit:

- Lift the 11-key auth.* audit-action label map out of the two admin
  components that rebuilt it on every render. The shared
  useAuthActionLabels() hook in _shared.tsx is now the single home for
  new entries (login-overview + recent-audit-preview consume it).
- Drop the dead chipClass branches in IntegrationStatusPill: only the
  connected state actually paints a Dracula-green chip; error and
  disconnected fall back to the badge variant.
- Add thin DateInput / DateTimeInput wrappers under components/ui that
  bake in the lang={locale} hint so 14 callsites across 9 files no
  longer have to remember the prop. A future date input cannot
  silently regress.
- Remove the leading icon-only flex wrappers left behind when the
  duplicate card titles were dropped on /admin/feedback,
  /admin/danger-zone, and the thresholds editor.
- Strip the five "v1.4.19 X removed because…" narration comments from
  insights/page.tsx and the dashboard. git blame already answers what
  changed and the comments aged into history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:24:35 +02:00
Marc-André BombeckandClaude Opus 4.7 ef74241a68 fix(measurements): paint Sys/Dia badge on mobile blood-pressure rows
The mobile card list checked m.type against the short forms BP_SYS /
BP_DIA, which never appear in API payloads — the canonical enum is
BLOOD_PRESSURE_SYS / BLOOD_PRESSURE_DIA. The badge condition was
therefore permanently false and the disambiguation tag never rendered
on Pixel 5. Route the label through MEASUREMENT_TYPE_LABEL_KEYS so the
mobile branch shares the desktop table's single source of truth, and
add a regression test that asserts both labels paint in both branches
under EN and DE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:15:56 +02:00
Marc-André BombeckandClaude Opus 4.7 14eff96474 docs(planning): Phase B (A8 application) report + v1.4.20 backlog
Wave B completed: 6 / 6 CRITICAL applied, 21 / 25 HIGH
applied, 4 HIGH deferred (3 not-an-issue, 1 owned by v1.4.20
Insights redesign), 31 MED + 16 LOW seeded as v1.4.20
backlog. Final verification clean. STATE.md status block
appended; A8 raw findings added to repo for future reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:01:38 +02:00
Marc-André BombeckandClaude Opus 4.7 ae2f671321 style: prettier on touched test + e2e files
Run prettier on the test, e2e, and component files this v1.4.19
A8 wave touched. The .planning/* and docs/audit/* prettier
warnings are pre-existing baseline noise — left alone to keep
this commit narrow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:58:30 +02:00
Marc-André BombeckandClaude Opus 4.7 713b494f04 fix(admin): humanise auto-issued token names in viewer
The native auto-login flow names tokens by suffixing the issuing
call's `Date.now().toISOString()`, so the admin viewer rendered
"iOS auto-login 2026-05-05T19:46:20.603Z" — readable as debug
output, not as something a person picked. Added a renderer-side
formatter that detects the trailing ISO timestamp and rewrites
the cell to "iOS auto-login · 05.05.2026 19:46". Manual token
names without an ISO suffix pass through unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:57:04 +02:00
Marc-André BombeckandClaude Opus 4.7 bc81fd7db2 fix(admin): translate native-token audit actions, link recent rows
Two related improvements to the admin recent-activity surface:

- New audit actions `auth.token.autoissue.native`,
  `auth.token.refresh`, and `auth.token.revoke` were leaking
  through both the dashboard preview and the Login Overview
  viewer as raw event keys. Added translations and wired the
  three keys through the action-label map shared by both
  components.
- Recent-activity rows on /admin were a dead-end: a failed
  login row showed "Unknown" + "Failed" with no link, no IP,
  no way to drill in. Each row is now a Link into
  /admin/login-overview, with the IP shown inline at sm+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:55:34 +02:00