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>
The retired RATED factors leave no seeded factor with a scale narrower
than the 1..5 rating envelope, so the per-tag out-of-scale "skipped" path
is no longer reachable through the catalog (it stays covered by the
`resolveRatedFactors` unit tests). Update the bulk integration tests to
use surviving factors and assert the envelope rejection, and swap a
deactivated tag for an active one in the tag-key round-trip. Refresh the
stale per-tag-scale comment in the rated-factor schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
MOOD is mood-entry backed, not a MeasurementType; it was filtered out of
DISCOVERY_BEHAVIOURS but not DISCOVERY_OUTCOMES, so it leaked into the
measurement.findMany type filter and threw PrismaClientValidationError,
failing the period-narrative warm job.
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>
Render the pulse Einschätzung above the VO₂ max cardio-fitness cross-link so
the plain-language read leads and the cross-link trails. Lead the cross-link
card with the canonical TileHeader so its Gauge glyph matches every other tile
header (foreground colour, same size and position) instead of the muted,
offset placement.
Allow the documented trailing CTA in the spine-order guard for pulse while
keeping the target-before-assessment check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
The BYOK Anthropic client fell back to the retired claude-3-5-sonnet-latest;
match the chain-runner default (claude-sonnet-4-6). The admin ai-settings GET
displayed a gpt-4o-mini fallback that disagreed with the actual provider
default (gpt-4o); align the display string so the surfaced model matches what
the provider would use.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mapCycle now emits AVERAGE_HEART_RATE and MAX_HEART_RATE from the cycle
score's average_heart_rate / max_heart_rate — both were already on the typed
WhoopCycle interface but dropped on the floor. mapSleep now reads
stage_summary.disturbance_count (added to the WhoopSleep interface) and emits
SLEEP_DISTURBANCE_COUNT. All three guard nulls / unscored records and carry
distinct field tags (avg_hr / max_hr / disturbances) so they don't collide
with the existing strain / energy / stage rows under the shared resource
externalId. Update mapping.md to document the three and note the cycle HR
pair were previously fetched-but-dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
The overview empty-state gated on the comprehensive query reporting zero
measurements, but a transient 500 / network drop settles the query with no
data too — so a user with history saw the false 'no data yet — add a
measurement' prompt with no way to recover. Add an isError branch above the
empty-state that renders a load-error message plus a Retry that refetches the
query, mirroring the vitals-dashboard pattern. Reuses the existing
insights.loadError + common.retry keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The v1.12.1 diversity block (variety lead, data-strength line, steady-run
repetition signal, FDR-screened cross-metric relations) reached only the
generic archetype cards. The seven hand-tuned headline cards — the ones users
see most — ran at temperature 0.3 with only the previous-context block, so they
stayed prone to repetitive openers and forced filler steps. Thread the same
assessmentContextBlock through the specialized get*UserPrompt builders and
raise their temperature to 0.45 to match the archetypes.
Soften the unconditional "close with one step" across the specialized sections
and the activity / body-composition / sleep archetypes to the base prompt's
conditional form — close with a step only when the finding implies one,
otherwise affirm and name one thing to watch — so a steady metric no longer
manufactures a filler step. EN/DE parity preserved; guard computeSteadyRun
against an absent graded slice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
general-status and blood-pressure-status read the full medication-intake
history with no time floor, scanning every row for accounts with years of
data. Both only consume the trailing ~365 days (the adherence fold and the
continuity series / 7-30 day compliance gate), so add a scheduledFor floor at
now - 365d.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reference-grounding builder (buildSystemPromptWithReferences) and its
SOURCES list shipped to no one: both live paths called the plain system prompt,
so the model was asked to cite referenceId values it was never shown — and the
parser rejects invented ids, so the citation machinery the schema and UI
footnote already support stayed dark. Wire the builder into both consumers,
passing the metric sections the snapshot actually carries (a weight-only run
never lists BP guidance). Apply the fenced-JSON parse fallback here too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anthropic and the local provider do not send a JSON mode, so a compliant model
still wraps its { "summary": … } reply in a code fence or prefixes a sentence,
which broke JSON.parse and surfaced the raw fenced string as the assessment.
Add a shared stripJsonFences helper (drop the fence, narrow to the first { …
last }) and apply it as a parse fallback in parseSummaryFromContent. No-op on
already-clean JSON.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The insights ground-rules carried changelog tokens ("v1.4.16 phase B8", "W4d")
inside the prompt strings the model reads — token waste and inconsistent with
the project voice. Move the rule content to clean numbered rules; the markers
belong in code comments, not shipped prompt prose. Update the test pins to the
cleaned rule headers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OpenAI BYOK + admin-shared fallback defaulted to a small model and
Anthropic to a 2024 Sonnet, so a user on the operator's shared key got the
weakest model for the Insights assessments. Bump the OpenAI default to gpt-4o
and the Anthropic default to claude-sonnet-4-6; both stay overridable by user
config. Sync the provider docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The snapshot builder probed rollup coverage up front to gate the thick
extras phase, then called computeSummariesSlice which re-ran the
identical probeRollupCoverage query. Thread the already-probed map into
the slice as an optional argument so the snapshot's hot path issues the
probe once. The argument is optional with the internal probe as the
fallback, so every other caller is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dashboard snapshot lives in the analytics cache bucket, which had no
stale window, and every measurement write hard-evicted the whole
${userId}| prefix. A high-frequency iOS Apple-Health sync therefore
busted the snapshot into a cold rebuild on the next read all day long
(the same class as the v1.8.7 insight-cache eviction).
Give the analytics bucket a stale-while-revalidate window, read the
snapshot via cachedSwr, and switch the measurement-write invalidation
from a hard deleteByPrefix to markStaleByPrefix. The snapshot now serves
the prior value immediately while a single background recompute warms a
fresh one. Slim / thick / summary cells read via plain cached are
unaffected: a marked-stale entry has expiresAt === now, so their next
read is a clean miss and rebuilds fresh. Widget reorder keeps its hard
point-evict so a layout change paints synchronously; mood and medication
writes keep their hard analytics sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
findCached returned null and the response was only persisted after the
handler completed, so two concurrent requests carrying the same
Idempotency-Key both passed the lookup and both executed the side-effect
(duplicate writes for the very clients idempotency protects).
Claim the key by inserting a pending sentinel row under the existing
(userId, key, method, path) unique constraint before the handler runs.
A racing request either finds the pending row or loses the insert race
and is refused with 409. The claim is promoted to the completed response
on a cachable result, released on a non-cachable result or a
secret-shaped body, and released on a handler throw. The pending row
carries a short claim TTL so a crashed handler self-heals and the
existing expiry-sweep cleanup still applies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The analytics compliance streak keyed days with host-local
getFullYear/getMonth/getDate against a timeline whose slot.day is minted
in the user's IANA zone, so the streak drifted off by a day whenever the
server clock's zone differed from the user's. Consolidate onto the
detail-page streaksFromTimeline helper threaded with the medication's
timezone so both call sites share one correct day-keyed walk.
Also fix the DST step in that helper: it stepped a fixed +24h while its
comment claimed 25h, which could skip a local day on the 23h
spring-forward boundary and run the window short. Snap each cursor to
the user-local midnight and advance +25h then re-snap, mirroring the
cadence expander, so every local calendar day is visited exactly once
across 23/24/25h boundaries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-sample cumulative drain strips already-collapsed stats: rows
from the day bucket, so a trailing watch sync that lands after a day's
total was minted leaves the bucket holding only the late samples. The
upsert update branch then replaced the full day total with the partial
late sum, and the contributing samples were already hard-deleted, making
the lost count unrecoverable.
Probe the existing total inside the write transaction and fold the late
sum into it (existing + late) instead of overwriting. The late samples
are genuine additional readings, unlike the legacy-step pass where the
existing total is HealthKit's own aggregate and folding would double-
count. Per-sample rows are still hard-deleted so a re-run cannot
re-accumulate them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Render systolic and diastolic as side-by-side columns in a single strip
card rather than two stacked cards, reclaiming the vertical space the
operator flagged. Each column keeps its header and its four stats, and the
single chart's brushed window still updates both columns independently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The taken / skipped / missed counts on the medications page used raw
Dracula green / orange / red, which clear only ~1.9:1 on the white card
in light mode and fail WCAG AA for small text. Route them through the
success / warning / destructive tokens, which carry the light-mode
overrides that clear AA in both themes. The word labels stay as the
non-colour differentiator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
The wellness-score strip rendered inside the vitals dashboard, below the
daily briefing. Lift it out so it sits as its own full-width section
directly under the hero and above the briefing — the overview spine is
now hero, wellness, briefing, vitals, trends, retrospective, signals.
The page now owns the one shared derived batch and feeds the same query
instance to both the wellness strip and the vitals grid, so the lift
adds no second request. Each wellness tile and the section heading lead
with the canonical TileHeader (a per-metric icon plus the metric name in
the foreground colour), matching the rest of the Insights card language.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The centred score number painted off the band token (success/warning/
destructive), which cleared only ~1.5:1 against the dark wellness card
and read as illegible. Switch the number to the foreground colour so it
is legible on both the dark and the light card. The band semantic stays
carried by the ring arc fill, the data-band attribute, and the
aria-label, so the colour change costs no information.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>