Commit Graph
582 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.8 eec3557137 feat(cycle): symptom-by-phase patterns on the insights tab
Add the cycle-native counterpart to the phase×vitals crosstab: for each
logged symptom, where in the cycle it clusters. A pure aggregator tallies
each symptom's occurrences across the phase-labelled days (the same phase
map the calendar uses), surfaces only symptoms logged on at least three
phased days, and reports per-phase counts + the dominant phase + its
share. The insights tab renders it as a proportional phase-hued bar with
the dominant phase named in words (never colour-only), observational and
never causal.

Also correct the phase-crosstab OpenAPI row enum, which had drifted
behind the runtime (mood + glucose channels were missing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:47:04 +02:00
Marc-André BombeckandClaude Opus 4.8 c21131e26e feat(cycle): second-audit feature, QoL, and wiring close
Close the second-round audit UI cluster:

- Cycle reminders: add the missing notification-event labels so the
  period-soon / period-confirm toggles read correctly on the
  notifications page, and point to them from cycle settings.
- Per-cycle history: render a recent-cycles list (range, length, period
  length, confirmed-ovulation chip) — the cycles were fetched but only
  the aggregate stats showed.
- Confirmed ovulation: carry it on the prediction DTO and surface it as a
  solid BBT-chart marker + a predictions row + a history chip.
- Log-day sheet: deselecting an enum now clears it (edits route through
  the nullable PATCH); period start/end work on any selected date for
  retroactive correction; "end period" shows only while one is open.
  Extract the payload builders as pure, unit-tested functions.
- Cycle-tracking enable toggle in account settings so any account can opt
  in before the gated page is reachable; wire the cycle data-purge button
  to the hard-delete route and document the route in the OpenAPI spec.
- Calendar legend: add the symptom dot, render fertile as a ring to match
  the grid; tighten the luteal prior bound to the engine clamp; mute the
  BBT ovulation-label colour for contrast.
- Share the cycle-start backward-walk between the wheel and the chart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:32:25 +02:00
Marc-André BombeckandClaude Opus 4.8 3375bbabac fix(cycle): second-audit safety, privacy, and write-path hardening
Address the second-round audit (server cluster):

- AVOID_PREGNANCY surfaces the fertile window, so it must carry the
  stronger "not a contraceptive method" disclaimer — wire the existing
  strong copy via a shared cycleDisclaimerKey(goal), server and client.
- Re-anchor the FOLLOWING neighbour cycle on a back-filled period start so
  an inserted-between-two-cycles start no longer leaves a stale open cycle.
- Resolve the sensitive-category encryption flag once per bulk/import
  batch instead of re-reading the profile per day; hoist the bulk cycle
  attribution in-memory (no per-entry query).
- Drop the raw driver message from the bulk wide-event meta (annotate meta
  is not run through the central redactor); keep the stable reason + code.
- Hard-purge now clears the cycle audit trail and the cycle reminder rows
  in the push-attempts ledger, so no dated reproductive trace survives.
- Discreet mode collapses the Web-Push coalescing tag and drops cycle
  metadata (phase) from APNs, matching the existing APNs event collapse.
- Add the reproductive intent field names to the central redactor denylist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:15:22 +02:00
Marc-André BombeckandClaude Opus 4.8 31a14ce7bf feat(cycle): premium cycle ring, BBT chart, and full read-state coverage
Finish the cycle web vertical's signature surfaces:

- Rebuild the cycle wheel to the wellness-ring premium signature — a
  clockwise clock-wipe that draws the phase arcs on from the 12-o'clock
  day-0 origin, a phase-tinted bloom on the active arc, the shared
  specular sheen, a day-number count-up, and a soft current-day marker
  halo. All on the wellness-tile surface, gated once per session and
  zeroed under prefers-reduced-motion.
- Add the basal-body-temperature chart to the predictions tab: the
  current-cycle BBT series with phase-tinted points, fertility signs in
  the tooltip, and an estimated-ovulation reference line (suppressed in
  raw-chart mode). Surface basalBodyTempC + ovulation-test + mucus on the
  calendar day DTO so the chart reads from the existing calendar load.
- Cover the read states the prediction/disclaimer surfaces were missing:
  pass the disclaimer to the predictions panel, render it on the calendar
  tab, add per-tab error + retry, and gate the settings spinner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:09:00 +02:00
Marc-André BombeckandClaude Opus 4.8 4b10bdc5da feat(cycle): single-day read endpoint, symptom severity, and a richer log-day sheet
Add `GET /api/cycle/day-logs?date=YYYY-MM-DD` returning the full day-log DTO
(or null) for one tz-anchored day so the web log-day sheet pre-fills instead
of opening blank — a blank re-log no longer nulls untouched or HealthKit-sourced
fields. Delete resolves the row id from the same read.

Persist an optional 1-4 Likert severity per symptom link (migration
0133_v1150_cycle_symptom_severity), carried through the shared symptom-link
writer and surfaced in the day-log DTO.

Sheet now captures the previously missing fields — spotting between periods,
pregnancy/progesterone home-test results, contraceptive kind — plus per-symptom
severity chips, a one-tap end-period boundary, and a Delete action. The wheel
ring marker sits at the 12-o'clock origin again (drop the double rotation), the
cycle view goes two-column on desktop, and the empty ring carries a first-period
call to action. New copy is localized across all six locales; the prediction and
fertility surfaces keep their disclaimers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:39:28 +02:00
Marc-André BombeckandClaude Opus 4.8 3e398295a3 feat(account): accept a third gender value
Add "OTHER" to the accepted gender values (the column is a free String, so no
migration) and surface it in the account selector. This lets a non-binary or
trans user record their gender and opt into the cycle-tracking surface through
the per-user enable flag, rather than being gated out by a binary field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:40:01 +02:00
Marc-André BombeckandClaude Opus 4.8 55f40df0ac refactor(cycle): wire the phase-correlation card into the insights tab
The cycle insights tab rendered an empty placeholder while the phase-contrast
card + its `/api/cycle/insights` feed sat unused. Wire them together: a
`useCycleInsights` read drives the headline finding + the luteal-vs-follicular
crosstab on the insights tab, matching the mood-factor crosstab styling. Drops
the placeholder card + a duplicate disclaimer component in favour of the ones
already in use, and removes the unused recency/phase-result/label exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:26:13 +02:00
Marc-André BombeckandClaude Opus 4.8 e984ada5f1 feat(cycle): import Apple Health reproductive data into cycle, add cycle export
Promote the reproductive HealthKit category types out of the
Apple-Health-import privacy-hold list and route them into cycle day-logs
instead of measurements, and surface cycle data on the three export
surfaces.

Import promotion:
- A new HealthKit -> cycle mapping module is the single source of truth for
  how MenstrualFlow, IntermenstrualBleeding, CervicalMucusQuality,
  OvulationTestResult, SexualActivity, PregnancyTestResult,
  ProgesteroneTestResult, Contraceptive and the symptom category types fold
  into CycleDayLog fields / seeded symptom keys. The XML importer
  accumulates same-day reproductive samples and writes one canonical
  day-log per day with source APPLE_HEALTH and a synthetic per-day
  externalId for idempotent re-import. The fold is gated on cycle tracking
  being enabled, so a non-cycle export never provisions cycle rows. A
  contraceptive method nudges the profile goal toward avoiding pregnancy
  only when the goal is still on its default. Basal body temperature stays
  a measurement and mirrors onto the day-log. Pregnancy/Lactation status
  and the pregnancy-bleeding types stay deferred; the cycle awareness types
  remain server-derived.

Export:
- FHIR emits LMP (LOINC 8665-2), average cycle length (64700-8), period
  length (64698-4) and the current phase as opt-in reproductive-health
  observations grouped into their own document section.
- The doctor PDF gains an opt-in cycle section (recent cycles, average
  length and variability, current phase), privacy-default off like mood.
- The full-backup export, the weekly backup worker and the off-host DR
  snapshot include the cycle tables, and the admin restore re-creates them;
  the day-log note round-trips as ciphertext and is never decrypted onto a
  report surface.

Mapping table documented in docs/api/cycle-healthkit-mapping.md. Adds unit
+ integration coverage for the mapping, the import routing, the FHIR
observations and the backup round-trip. OpenAPI regenerated. Six-locale
strings added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:16:54 +02:00
Marc-André BombeckandClaude Opus 4.8 7333d8e40f feat(cycle): add the cycle-tracking web vertical
Build the web UI for the cycle-tracking category against the existing
`/api/cycle/*` routes: a month calendar + circular phase wheel, a fast
log-day sheet, a predictions panel, phase-insight cards, and a single-page
settings form, all under a gated `/cycle` tab.

- Calendar + wheel: a hand-rolled SVG phase ring (0 KB runtime, matching the
  wellness-ring aesthetic with calm per-phase hues) showing the current
  phase + day-of-cycle, alongside a month grid where logged period days show
  a filled pip, the predicted next period renders as a ranged band (never a
  single date), the fertile window shows only when the goal allows it
  (the API suppresses it server-side otherwise), and ovulation + per-day
  symptoms carry markers. Reads `GET /api/cycle/calendar`.
- Log-day sheet: one-tap period start, flow / symptom / ovulation-test /
  mucus chips, basal-temperature, intercourse + protection, and a free-text
  note. Optimistic write to the day-log + period routes; invalidates the
  cycle query keys.
- Predictions panel: next-period range with a confidence pill, goal-gated
  fertile window, a still-learning state for fewer than three cycles or
  raw-chart mode, cycle-history stats, and the fixed non-medical disclaimer
  (reusing the drug-level-estimate disclaimer pattern).
- Insights cards render whatever phase-correlation cards the API surfaces,
  with a graceful empty-state until the feed lands.
- Settings: a single dynamic form where the inclusive goal selector drives
  the copy (conception framing only for trying-to-conceive), plus typical-
  length priors, raw-chart mode, sensitive-category encryption, discreet
  notifications, and export / delete. PATCHes `/api/auth/me/cycle-prefs`.
- Nav: a Cycle entry in the sidebar and the mobile More hub, gated on
  `cycleTrackingEnabled` from `/api/auth/me` and hidden otherwise.

Add the cycle query keys + dependent-key bundle to the factory, full copy
across all six locales, and dedicated per-theme phase hues. Plain
second-person voice, WCAG 2.1 AA, no new dependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:06:20 +02:00
Marc-André BombeckandClaude Opus 4.8 6fdbb8d889 feat(cycle): predicted-period notifications + reproductive-data privacy docs
Add two cycle reminders through the existing notification cascade and the
honest self-hosting privacy framing for reproductive-health data.

Notifications:
- New event types CYCLE_PERIOD_SOON (fires ~2 days before the predicted
  next-period start, from the cached prediction) and CYCLE_PERIOD_CONFIRM
  (a gentle "did your period start?" on/after the predicted start while no
  period is logged, within a short grace window). Both default OFF and are
  gated again on cycleTrackingEnabled — opt in twice for a sensitive surface.
- A 15-minute pg-boss cron mirrors the mood-reminder worker: it scans users
  with cycle tracking enabled + predictionEnabled, computes who is in-window
  from the cached forecast, and dispatches one push per event per local day.
  Idempotency anchors on the push_attempts ledger (no new table). The queue
  is declared, registered in allQueues, scheduled, and wired to a boss.work
  handler, with a source-grep guard so it cannot ship unregistered.
- Honours notificationPrefs.cycle.clientManaged (skip the server push when
  the device owns local reminders, with a suppressed annotation, mirroring
  the medication path) and CycleProfile.discreetNotifications (generic
  "HealthLog reminder" body so no cycle event is named on the lock screen).
- Every attempt is recorded in push_attempts via the unchanged dispatcher.

Privacy / disclaimer:
- docs/self-hosting/reproductive-health-data.md — the "no cloud vendor to
  subpoena" framing with the honest residual-threat caveat (your own host,
  provider, backups, and devices remain part of the surface), citing the
  2021 FTC settlement and 2025 verdict as public record only.
- docs/security/cycle-data-threat-model.md — a compact "protects against /
  does not protect against" note, plus README pointers.
- CyclePredictionDisclaimer component (reusing the GLP-1 estimate-disclaimer
  pattern) for every fertility/prediction surface: descriptive, not a
  contraceptive method, not medical advice, never a safe-to-have-unprotected
  -sex claim. 6-locale copy under cycle.disclaimer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:00:42 +02:00
Marc-André BombeckandClaude Opus 4.8 77a652a47c feat(cycle): cycle-phase correlation insight + Coach phase dimension
Add the menstrual-cycle phase as a channel in the existing FDR-guarded
correlation machinery, the same way rated mood factors were folded in —
no new statistics, the same Welch + Benjamini-Hochberg pipeline, the same
day floors and q = 0.10 gate.

Two complementary mechanisms, both confined to a gender-gated surface:
- a categorical luteal-vs-follicular contrast per outcome metric (resting
  heart rate, HRV, sleep, steps, weight, basal/wrist/skin temperature),
  surfacing one headline finding (resting-heart-rate-by-phase, falling
  back to HRV); and
- the continuous phase ordinal folded into the lagged-Pearson matrix.

Phase never reaches the general correlations route — the temperature
channels held out of the general matrix are un-blocked only here, behind
the cycle gate, so a reproductive inference can never leak onto a
non-gated surface. Every row shows n, effect size, p and q; nothing is
fabricated on thin data and nothing is framed as causal.

Surface it through a new gated GET /api/cycle/insights and a drop-in
card that mirrors the mood-factor crosstab.

Add a cycle dimension to the Coach snapshot: current phase + day-of-cycle,
the next predicted event (period range + confidence + method, fertile
window goal-gated), and the headline phase finding — present only for a
cycle-enabled account, so a non-cycle snapshot is unchanged. A new ground
rule keeps the Coach descriptive: never contraception-grade, never a
"safe day" claim.

OpenAPI regenerated; phase-correlation + snapshot-inclusion tests added;
copy localised across all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 09:59:02 +02:00
Marc-André BombeckandClaude Opus 4.8 041b44510e test(medications): make the cycle-status day-count timezone-stable
The open-cycle "next dose in N days" tests anchored their fixtures in the
runtime calendar while the component buckets days in Europe/Berlin, so a CI
runner in UTC near midnight read "now" as the next Berlin day and drifted every
count by one. Pin the clock to a fixed Berlin-noon instant and build the
fixtures as exact 24 h offsets so the whole-day delta is deterministic in any
timezone. Verified under TZ=UTC.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:32:10 +02:00
Marc-André BombeckandClaude Opus 4.8 189202f4c6 fix(insights,medications): reconcile review findings on the v1.14.0 surfaces
- Medication cycle status: re-hydrate `nextDueAt` (it arrives as an ISO string
  over the wire) before the Berlin day-bucketing, so "next dose in N days" no
  longer drifts by a day near midnight or off Berlin. Covered by a new
  string-input test.
- Wellness rings: drive the conic sheen off a fixed white so it always lightens
  (a near-black light-mode foreground under overlay was darkening the tile into
  a grey band); give the ring a transform transition so it scales smoothly with
  the tile lift instead of snapping; cap the green-band glow at a few breaths
  instead of an infinite drop-shadow repaint; deepen the light-mode stress and
  recovery end-stops for non-text contrast; track the sm/md column count to the
  active-score count so two or four scores never leave an empty cell.
- Mood factor crosstab: bound the snapshot's cross-metric read to the 365-day
  window and exclude soft-deleted rows, so the factor deltas the model cites
  match the visible board over the same canonical row set (no recency-biased
  truncation, no soft-delete skew); thread the user's source priority so the
  per-day canonical pick agrees with the screen; render the delta neutrally
  rather than green/red, since a higher vital is good for steps but bad for
  resting heart rate and the board is observational.
- Log-intake: surface an error when a pinned slot can't resolve to an instant
  instead of silently logging the dose through the unscheduled path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 00:22:29 +02:00
Marc-André BombeckandClaude Opus 4.8 dfda2a1c7e feat(insights): correlate rated mood factors against vitals
Rated mood factors (work / sleep-quality / stress, scored per entry) now
participate in the cross-metric correlation surface. Until now their score
series was captured but read by zero analytics.

Two paths, both on the existing FDR-controlled engine:

- A factor x vital crosstab on the mood insights page and the mood
  snapshot: for each factor the user scores, split its days low vs high by
  the factor's own median, then run the same Welch test + day floors +
  Benjamini-Hochberg correction the tag crosstab uses. Surfaces lines like
  "sleep runs ~38 min below baseline on the days you rated work low"
  (n-gated, q <= 0.10, never fabricated on thin data).
- Factor channels (FACTOR:<key>) fold into the period-narrative discovery
  matrix as both lag source and lag target, so the drivers the coach and
  narrative reference can cite a factor relationship.

An inverse factor (higher rating = worse day) gets the documented sign-flip
once, at the series boundary, so "up" always reads as a better day and the
low/high split and phrasing stay honest. Strings land in all six locales;
the surface carries the standing "association, not cause" caption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:58:40 +02:00
Marc-André BombeckandClaude Opus 4.8 7db9608c4c fix(insights): align trend-card mood title, tighten assessment gap, lift briefing heading
Four small Insights layout fixes:

- Trends row: render the mood mini chart in the same flat shell the
  weight/blood-pressure mini charts use (`bg-card border rounded-md p-2`
  with a plain title div) instead of the Card/CardHeader chrome that
  stacked extra space above and below the heading. The three trend cards
  now share one title baseline and one height.
- Coach settings: place tone and response length side by side (two
  columns from `sm`, stacked on mobile) so the sheet no longer needs an
  extra scroll length for two short selects.
- Daily briefing: move the heading out of the card into an `<h2>` above
  it, matching the heading-above-card pattern the trends and wellness
  sections use, so the card opens straight on its content.
- Assessment cards: align the loading skeleton and the mood better-days
  card to the canonical `gap-1.5` + `pb-1` heading-to-body rhythm the
  populated state already uses, removing the loose ~16-24px gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:52:56 +02:00
Marc-André BombeckandClaude Opus 4.8 ba17b08a01 feat(medications): split the Add button into log-intake vs new-medication
The top medications-page Add button now opens a choice menu with two
paths: log an intake against an existing medication — including a
backdated one — or create a new medication (the existing wizard,
unchanged).

The log-intake dialog lists the active medications, optionally pins a
schedule slot, and picks a date and time that defaults to now but accepts
any past instant. It submits to the existing per-medication intake route:
takenAt carries the (possibly backdated) instant, and a chosen slot
supplies scheduledFor so the write routes through the server's canonical
slot upsert — the same snap path a live Taken tap uses — keeping the
one-row-per-dose-slot invariant intact. No new endpoint.

Submit orchestration lives in a pure, dependency-injected runLogIntake
helper alongside the shared take/skip logic, invalidating the medication
dependent query-key bundle so the dashboard and compliance views refresh.
New strings land in all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:50:08 +02:00
Marc-André BombeckandClaude Opus 4.8 2beaec4149 feat(medications): surface the open-cycle status on the medication cards
Wire the per-med `complianceDisplay.currentCycle` descriptor into a calm,
rate-decoupled status line on both the generic and GLP-1 medication cards.

A sparse weekly or rolling med (a GLP-1 injection) that is simply between
doses has no closed cycle in flight, so the compliance percentage alone reads
as a scary 0% that says nothing about whether the user is on schedule. The new
`MedicationCycleStatus` card-part reports the open cycle instead: "next dose in
N days" / "due today" / "overdue", or a neutral "no closed dose cycles yet"
when there is not enough history to score. It complements the percentage rows;
it does not replace them.

Driven by `currentCycle.state` (on_track / due / missed / none). Colour follows
the existing success / warning / destructive ramp and pairs each tier with a
Lucide glyph so colour-blind users can disambiguate (WCAG 1.4.1). The relative
day phrasing buckets on the user's calendar day, matching the next/last slot.
Strings land in all six locale bundles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:49:29 +02:00
Marc-André BombeckandClaude Opus 4.8 20d7187b0d feat(insights): premium animated wellness-score rings
Rebuild the wellness-score strip on /insights as a signature, high-end moment
instead of a flat dial.

- Five distinct per-metric hues — readiness green, recovery teal, sleep
  indigo-blue, stress amber, strain violet — hand-tuned per theme so each holds
  contrast on the white light-mode card. Fixes the dark-theme collapse to a
  cyan/green wash (the semantic info token resolved to the same cyan as three
  rings) and the light-theme wash-out of the raw cyan on white.
- A spring-eased arc sweep that overshoots ~6% then settles, a left-to-right
  staggered reveal with each number trailing its own arc, a conic specular
  sheen that sweeps once, a token-tinted bloom (a gentle breathing pulse only
  on a top-band ring), a segmented instrument-style track, and a faint
  film-grain tile surface for material depth.
- A "vs your normal" ghost arc under the live arc for recovery, stress and
  strain, derived from the trailing score series already on the payload, so the
  user sees today against their baseline at a glance.

The reveal plays once per browser session (sessionStorage-gated) so a
background sync refetch never re-triggers it. All motion is CSS-first — no
animation dependency, @property + a sampled linear() spring — and is fully
disabled under prefers-reduced-motion. The arc stays a hand-rolled SVG gauge;
the band semantic still rides data-band + the band word + the aria-label, never
colour alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 23:40:00 +02:00
Marc-André Bombeck 016f866965 fix(insights): health-score BP pillar from all-time window + surface ACTIVITY_STEPS in Insights 2026-06-05 22:47:46 +02:00
Marc-André Bombeck e7d34eb853 feat(insights): premium wellness-score tiles + SVG ring redesign 2026-06-05 22:09:06 +02:00
Marc-André BombeckandClaude Opus 4.8 d3d58a0c30 chore(release): v1.13.1 — wellness rings fill to the actual score
The score-ring's RadialBar swept the full circle for any value because the
angular domain was only pinned on the PolarRadiusAxis (which governs radius,
not sweep). Recharts then auto-scaled the angular domain to the single datum,
so every ring read as 100%. Add a PolarAngleAxis with a fixed [0,100] number
domain so the bar sweeps value-percent of the track. Affects the overview
wellness tiles and the score-anatomy detail view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:18:52 +02:00
Marc-André BombeckandClaude Opus 4.8 038be6ac57 chore(release): v1.12.11 — unify settings notice colours and callout style
Route every Settings callout, badge and status note through the semantic
colour tokens (success / warning / info / primary) instead of raw palette
classes, so the same meaning reads identically across cards and stays
legible in light mode. Add a single SettingsInfoTile primitive — icon +
tone + text on one fixed surface — and adopt it for the threshold-override
caution note, replacing a bespoke box.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:42:18 +02:00
Marc-André BombeckandClaude Opus 4.8 83cf4eff2e fix(medications): split the card next/last dose lines into label-left, value-right rows
The next-intake and last-intake lines on the medication cards ran the
label and the time together on one run of text, reading as crammed. Lay
each line out as a two-column row — bold label pinned left, value flush
right — so the two rows read as distinct, aligned columns. The label
truncates first on a narrow viewport; the time never truncates.

The GLP-1 card routes through the same shared slot and now overrides only
the left-column label with its appointment-phrased wording, so the value
column carries the time and the optional rotation nudge. Its value strings
drop the now-redundant leading label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:34:44 +02:00
Marc-André BombeckandClaude Opus 4.8 a3e741c23c feat(mood): revise factor + tag catalog (migration 0126)
Tighten the operator-finalised mood catalog to three RATED factors and a
trimmed default BINARY tag set.

- Seed `factor_sadness` (RATED, 1..5, inverse — higher = worse) under the
  `factors` category, with its label in all six locales and a CloudRain
  glyph. The RATED set is now work / sleep quality / sadness.
- Retire `factor_conflict`, `factor_family`, `factor_social`,
  `factor_stress`: flip each to BINARY and deactivate. The catalog rows
  stay so historical entries still resolve their labels (the insights read
  joins by id without an active filter); they just leave the RATED set and
  the default picker.
- Trim the default BINARY tag set roughly in half: deactivate the
  sleep-tag category (now covered by the sleep-quality slider), the mood
  level restatements, the nutrition long-tail, and low-signal day-type
  tags. Kept rows are the high-signal, correlatable tags. Nothing is
  deleted; inactive rows still resolve labels for existing entries.

Reference-data only — no schema change, idempotent upsert + closed-key
updates, no per-user data touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:28:47 +02:00
Marc-André BombeckandClaude Opus 4.8 b3a626d83c docs: add navigational READMEs to key code directories
Add a README to src, src/lib, src/app, src/components, prisma, scripts,
and e2e so a technical visitor browsing the tree gets oriented: what
lives in each directory, the most important files, the local
conventions, and pointers to related dirs and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:26:34 +02:00
Marc-André BombeckandClaude Opus 4.8 2f52d49d91 fix(insights): tighten the assessment card header-to-text gap
Trim the assessment card flex gap to gap-1.5 (from gap-2 md:gap-3) and the
header bottom padding to pb-1 (from pb-2) so the TileHeading sits close to its
prose, matching the header-to-body rhythm of the sibling tiles. Applied across
the success, preparing, no-provider, and no-analysis states; the loading
skeleton keeps its own spacing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:53:32 +02:00
Marc-André BombeckandClaude Opus 4.8 465c8b8e33 feat(insights): drive metric stat strip from the chart range tab; add chart tile header
Remove the draggable chart brush. The Min/Max/Median/Mittelwert strip now
always reflects the data visible under the chart's active range tab (7/30/90/
all): the chart reports per-type stats for its range-sliced series and the
sub-page threads them into the strip. No drag, no "selected range" pill — the
range tab is the single selector for both chart and strip. Blood pressure
recomputes both systolic and diastolic.

Give each metric chart card the canonical TileHeader (icon + title, same size
and spacing as the assessment/target/stat tiles) via an optional titleIcon;
the dashboard and mini mounts keep the compact heading.

Drop the now-unused charts.selectRange and insights.subPage.stats.selectedRange
keys from all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:53:32 +02:00
Marc-André BombeckandClaude Opus 4.8 278007ce36 feat(insights): give wellness dials a white ring on a gradient tile
Echo the hero card on the wellness-scores strip: each ring tile now rides
a saturated purple-pink gradient (.score-tile-gradient) and the dial paints
a white arc over a translucent-white track with rounded ends.

The gradient mixes over the fixed dark --dracula-bg rather than the theme
--card, so it stays dark in both themes and white copy clears WCAG AA
either way (white on the purple endpoint ~5.0:1, pink endpoint ~6.1:1).

ScoreRing gains a variant prop: onGradient (white arc, white number) for
the tile, band (band-coloured arc) by default so the score-anatomy detail
view stays legible on its plain card. The band semantic is never lost to
the white arc - it still rides data-band, the aria-label, and the band-word
label under the ring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:49:12 +02:00
Marc-André BombeckandClaude Opus 4.8 ef188e0503 feat(measurements): add average/max heart rate + sleep disturbance types
Add three MeasurementType values for WHOOP fields a coverage audit found
uncaptured: AVERAGE_HEART_RATE and MAX_HEART_RATE (the cycle score's
whole-day aggregates, kept distinct from spot PULSE / RESTING_HEART_RATE /
WALKING_HEART_RATE_AVERAGE) and SLEEP_DISTURBANCE_COUNT (the per-night
disturbance tally). Units: bpm / bpm / count.

Wiring mirrors the existing per-type surfaces, traced from VO2_MAX and the
v1.11.0 WHOOP-native score classes:

  - canonical enum + unit + plausibility range in
    src/lib/validations/measurement.ts (every ingest / analytics / rollup
    path derives from measurementTypeEnum.options, so the mean-based rollup
    tier picks the new types up automatically).
  - Prisma enum + migration 0125 (ALTER TYPE ADD VALUE IF NOT EXISTS,
    non-transactional, idempotent — same pattern as 0111).
  - category overlay (cardiovascular for the HR pair, sleep for the count).
  - list-view label key / Lucide icon / chart-colour.
  - i18n label key in all six locales.
  - chart-token allowlist + orphan-enum stripper.
  - PersonalRecord direction (HR pair null — no clean goal axis, like
    WALKING_HEART_RATE_AVERAGE; disturbance count MIN, like
    BREATHING_DISTURBANCES).
  - enum-coverage drift guard (count 60 -> 63, doctor-PDF exclusions for the
    device-derived signals) + apple-health HK-counterpart exclusion (these
    are WHOOP-only; HealthKit ships no first-class daily-aggregate HR
    quantity nor a disturbance tally).

The types render on the measurement list + Insights chart surfaces; like the
v1.11.0 WHOOP-native scores they get no dedicated Insights sub-page (the
sub-page map stays curated) and no LOINC / FHIR mapping (not clinical-grade).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:45:58 +02:00
Marc-André BombeckandClaude Opus 4.8 d2edaeb8d1 refactor(insights): point the pulse page to the cardio-fitness surface for VO2 max
VO2 max had two divergent surfaces — the dedicated /insights/cardio-fitness
page (full chart + plain-language assessment) and a legacy compact chart-row
on the pulse page. Replace the pulse-page row with a clear cross-link to the
dedicated page so there is one VO2 max surface, and remove the now-unused
chart-row component + its test. New cardioLink copy added across all six
locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:07:56 +02:00
Marc-André BombeckandClaude Opus 4.8 4a88db6966 fix(ui): route status colours through AA-safe semantic tokens
Raw text-dracula-* utilities used as text fail WCAG AA contrast in light
mode (the Alucard light theme carries no override for the raw primitives).
Swap every text usage and its paired bg/border tint across the settings
integration/notification/sharing/api cards, the insights recommendation,
confidence, daily-briefing, health-score and coach-feedback surfaces, the
bug-report banners, the copy-confirmation icon and the chart comparison
caption to the theme-aware --success / --warning / --info / --destructive /
--dose-accent tokens, which both themes override to an AA-safe tone. Dark
mode is unchanged; decorative aria-hidden gradients and chart series fills
stay on the raw primitives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:07:56 +02:00
Marc-André BombeckandClaude Opus 4.8 1f00946bfe refactor(insights): tighten the subpage spine and clarify the VO2 max chart
Pull the subpage spine to a denser space-y-3/4 rhythm to match the tighter
stat strip, and refresh the stale brush comment now that the windowed stats
are wired. Give the cardio-fitness page its Gauge stat-strip glyph so VO2
max leads with the same standard treatment as the other HealthKit metrics;
the dedicated subpage already renders the full chart. Clarify the VO2 row's
sparse-series note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:49:46 +02:00
Marc-André BombeckandClaude Opus 4.8 d0d0f49385 feat(insights): add loading skeleton and error+retry to HealthKit subpages
The HealthKit metric pages consumed only `{data, isEmpty}` and ignored the
hook's `isLoading`/`error`, so they painted nothing in flight and fell
through silently on failure. Branch on `isLoading` to a stat-strip + chart
skeleton, and on `error` to a message plus a Retry that re-issues the
analytics query (mirrors the vitals-dashboard pattern). Expose `refetch`
from the analytics hook and add a `subPage.loadError` string in all six
locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:49:46 +02:00
Marc-André BombeckandClaude Opus 4.8 ad2f038963 feat(insights): tighten the stat strip and add a side-by-side multi-series mode
Drop the strip card to a denser py-3/gap-2 rhythm with 44 px stat cells and
text-base values so the numbers-first header gives away less vertical space
on every metric subpage while staying readable. Extract the per-series block
(header + four stats + brushed-window pill) into a reusable piece and add a
`series` prop that renders multiple series as columns in ONE card (two on
desktop, stacking on narrow mobile). Single-series callers keep their
contract unchanged; the brushed-window stats stay per-series.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:49:46 +02:00
Marc-André BombeckandClaude Opus 4.8 80c5f14748 feat(insights): reorder the mood page to the calendar-first spine
Lift the Stimmungskalender above the line chart and place the better-days
assessment directly under the Ziel card. `MoodInsightsSections` now renders by
region (`heatmap` / `assessment` / `rest`); the page threads them so the spine
reads heading → summary → calendar → chart → target → assessment →
classification + breakdowns. All three regions share the one `moodInsights`
query so the fetch is deduped. Every `data-slot` is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:46:42 +02:00
Marc-André BombeckandClaude Opus 4.8 56934fd658 style(insights): move mood colored text onto semantic feedback tokens
The mood confidence chips, the better-days / influence / crosstab delta
read-outs, and the mood-chart comparison caption coloured their TEXT with
raw `--dracula-*` values. Those stay bright green/cyan/red/purple in light
mode (Alucard) and fail WCAG AA on the white card. Swap the coloured-text
surfaces to the semantic feedback tokens (`--success` / `--info` /
`--destructive`) and the theme-aware `dose-accent` purple, which carry the
light-mode overrides that clear AA in both themes. Decorative chart fills,
strokes, heatmap cells, and legend swatches keep their `--dracula-*` tokens —
those are non-text and already token-based.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:46:37 +02:00
Marc-André BombeckandClaude Opus 4.8 3cd04533be style(insights): tighten mood chart heights and unify the overview rhythm
The mood line chart plots a 1-5 score band; the 240/280 px full-mode height
left a dead strip below the line on the mood subpage. Trim the default to
200/220 px (a per-mount --chart-height override still wins) and lower the
mood-correlation scatter height so the cards fit their content without the
empty gap.

The overview used space-y-8 between top-level blocks while the vitals wrap used
space-y-6, reading as two tiers. Unify to space-y-6 for one consistent vertical
rhythm down the page, in line with the denser-Insights direction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:40:44 +02:00
Marc-André BombeckandClaude Opus 4.8 4be9767428 fix(insights): normalize the assessment-card tile-header icon size
The TileHeader nodeIcon path rendered the caller-passed icon node verbatim and
dropped the canonical h-5 w-5 text-foreground class TileHeader forwards, so a
caller passing an unsized node could drift off the contract. Forward the class
onto a sizing span that pins any child SVG to the canonical box regardless of
what the caller passed. Existing call sites that already pass a pre-sized icon
stay visually identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:40:44 +02:00
Marc-André BombeckandClaude Opus 4.8 68ab78d6d8 feat(insights): merge the two mood "what stands out" surfaces into one card
The mood page carried two separate "what stands out" tiles: the narrative
takeaway feed and the FDR-controlled discovered relations, each a full card
under the same heading. Fold them into a single card — one header, the
narrative one-liners, then the discovered relations as a labelled subsection.
Each half gates independently; the card degrades to nothing when both are empty.

Drop the muted FDR explainer paragraph: the statistical caveat rides the
subsection info-icon tooltip, and every finding now reads in the standard
foreground text colour. Remove the orphaned discovery.description string from
all six locales and add the subsection label.

Give the better-days board the assessment-card treatment (insight-assessment
hook, tighter gap/padding, the insight-in entry) so it reads as the mood
assessment, consistent with the per-metric subpages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:40:44 +02:00
Marc-André Bombeck f25f7f3597 settings: full-width timezone select + match the excluded-sites card surface 2026-06-05 13:40:12 +02:00
Marc-André BombeckandClaude Opus 4.8 4dde3e1d8b feat(insights): recompute the stat strip for a brushed chart window
Wire the metric sub-pages so brushing a window in the chart recomputes the
Min/Max/Median/Mittelwert strip for the visible domain. The page lifts the
shared brushed-window state, passes `selectableDomain` + `onDomainStats` to
the chart, and threads the per-series window stats into the strip. With no
selection the strip keeps the precomputed full-range summary, so the default
is unchanged and costs no extra work.

The strip renders the windowed numbers when present and pins a "selected
range" pill to its header so the user reads the stats as their selection.
Blood pressure brushes both series at once — the single chart reports
systolic and diastolic stats and each strip reads its half.

Covers weight, pulse, blood pressure, and the ~37 HealthKit pages that ride
the shared scaffold. BMI (a derived metric with no first-class strip) and
sleep (no strip) are left as-is. New copy added in all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:35:54 +02:00
Marc-André BombeckandClaude Opus 4.8 0995664893 feat(charts): add opt-in brush that reports per-type windowed stats
Add an optional Recharts <Brush> to the shared HealthChart, gated behind a
new `selectableDomain` prop. When set, the chart mounts the brush beneath
the plot and reports the Min/Max/Median/Mean over the brushed window per
type through `onDomainStats`. A range-tab change or a series refetch clears
or clamps the selection so the brush never indexes past the data.

The dashboard, trends-row, VO2 row, sleep, and rationale mounts do not pass
the prop, so they render unchanged — the feature is fully isolated behind
the flag.

`computeWindowStats` is a pure helper (no React/Recharts) so the chart can
summarise the array it already holds without a refetch and the maths is
unit-tested directly. The median uses the same linear-interpolated midpoint
as `summarize()` so a window and the full-range summary read on one
algorithm. `useChartDomainStats` lifts the per-type window into shared state
with a stable callback and a shallow guard against repeat windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 13:35:44 +02:00
Marc-André Bombeck 41eda2d163 insights: reword the deferred chart-window stats note without a TODO marker 2026-06-05 13:09:52 +02:00
Marc-André BombeckandClaude Opus 4.8 22ba4193b2 fix(insights): dedupe coach disclaimer and add wellness-strip error affordance
Suppress the generic Insights disclaimer footer on the Coach route, which
already renders its own composer disclaimer — the layout shell now skips
the footer when the selected segment is the coach page and keeps it
everywhere else. Thread the shared derived-batch error state and refetch
into the wellness strip so a failed batch surfaces a compact error +
Retry in place (the strip is top-of-page) instead of vanishing silently;
the one Retry recovers both the wellness and vitals strips. Adds a
scores load-error string across all locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:58:39 +02:00
Marc-André BombeckandClaude Opus 4.8 036370fc88 fix(insights): normalize stat-strip and target panel header rhythm
The stat-strip and target-summary panels were bare card-styled divs with
a flat space-y gap, so their tile header sat at a different offset from
the Card-wrapped tiles (assessment, mood, medication) on the same subpage
spine. Wrap both in Card / CardHeader pb-2 / CardContent so the
header-to-body offset matches. All content and data-slots are preserved;
the panel semantics ride on role + aria-label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:58:29 +02:00
Marc-André BombeckandClaude Opus 4.8 f1f6018f6e fix(insights): align vitals tiles and section headings with the shared tile header
Bring the Vitals grid in line with the harmonized card language. The
sparkline-delta tile led with a muted-uppercase caption + a muted icon;
it now leads with a foreground icon and a foreground heading matching the
canonical tile header, keeping the trailing delta. The Vitalwerte and
Mobility section headings and the provisional baseline / HRV tile headers
move onto the shared tile header (foreground icon + heading) so the
Wellness, Vitals, and Mobility sections share one heading construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:58:23 +02:00
Marc-André BombeckandClaude Opus 4.8 18e36e3226 insights(medications): drop the GLP-1 dose-listing timeline and densify the compliance tiles
Remove the chronological GLP-1 therapy-event list from the medications
insight page — it duplicated data already on the per-medication detail
page and added no read on this surface. The page spine collapses to
compliance grid -> target -> assessment.

Route each per-medication compliance card header through the canonical
TileHeader (Pill glyph + med name as the heading) and fold the dose,
category, and streak into one compact muted line. Tighten the card gap,
header padding, progress-bar height, and content spacing so the same
data (7d/30d bars, taken/skipped/missed, heatmap, per-med sentence)
reads in noticeably less vertical space, consistent with the rest of
the harmonised Insights tiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:36:16 +02:00
Marc-André BombeckandClaude Opus 4.8 424d6ee5e1 insights(mood): drop the per-card generic disclaimer, defer to the page footer
The better-days board, the tag-influence list, and the tag × metric
crosstab each repeated a generic associations-not-causes / talk-to-your-
doctor caveat. That sentence now lives once in the page-level Insights
footer, so the three mood cards no longer restate it; the genuinely
specific FDR caveat on the discovered-relations board stays. Drop the
now-unused betterDays / influence / crosstab disclaimer keys from every
locale and update the two component tests that pinned the removed copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:36:12 +02:00
Marc-André BombeckandClaude Opus 4.8 fcd8eccf6e insights(mood): order classification first, route every tile through TileHeader
Reorder the mood subpage spine so the classification tiles (in-target
share + stability band) lead, then the better-days assessment, then the
descriptive breakdown sections. The classification answers where the user
stands; the better-days board, directly beneath, answers what associates
with their good days.

Every mood card header now routes through the shared TileHeader (icon +
white heading) so the subpage speaks one card language. The in-target and
stability tiles gain proper headings (new inTargetTitle / stability.title
keys across all six locales) and drop the icon-in-a-circle headline glyph;
the narrative feed and the discovered-relations board move into headed
cards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:36:12 +02:00
Marc-André BombeckandClaude Opus 4.8 85c9f3ff3f insights(mood): align mood-chart palette to Dracula tokens
The mood line chart hand-picked hex for its bands and lines: a
three-band red/orange/green ramp that skipped the yellow rung the mood
heatmap and distribution chart use, and a non-token purple primary.
Switch the bands to the same four-step red→orange→yellow→green ramp the
other two mood surfaces ride, and route the line/overlay colours through
the --dracula-* tokens (purple primary matching the health chart, pink
moving average, cyan trend) so the three mood surfaces read as one
palette and a theme change flows through a single source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:36:12 +02:00