The GLP-1 card overrode the shared next/last slot with appointment-phrased
labels (Next / Last, "Nächster Termin" / "Letzter Termin") while the generic
card used Next intake / Last intake. Drop the override so both cards name the
concept identically; the GLP-1 relative-day value phrasing (in N days / today)
is unchanged. Remove the now-dead label override props from the shared slot
and the orphaned glp1NextLabel / glp1LastLabel strings from all locales.
Relabel the Export section to Export & Import (heading + description,
all six locales; slug unchanged so existing links keep working) and
mount the import area in it. Add a cycle export tile that reuses the
health-record FHIR export with the reproductive section opted in, gated
on cycle tracking being enabled so a non-cycle account never sees it.
Compress the health-record export hero — format and date range share a
row, the practice-name field tightens, and the FHIR note rides the
footer with the generate action — so the card opens denser without
losing any controls. Add the import + cycle i18n keys across all
locales.
The Apple Health export.zip and generic JSON import routes shipped
without a web surface (issue #281). Add an import area with two
controls: an Apple Health export.zip drop / picker that streams the
upload and polls the job status to completion, and a generic JSON
control that takes a pasted body or an uploaded .json file, validates it
client-side, and reports the imported / skipped counts. A Download
example button mints a valid example payload as a Blob. Both controls
surface rate-limit and error states and announce progress via aria-live.
Adds the importJobStatus query key to the factory.
Both select primitives parked the trailing chevron visibly farther from
the right edge than the value text sits from the left. The shadcn trigger
gets a -2 px optical nudge to offset the chevron glyph's internal
padding. The native select drops the OS-drawn arrow (`appearance-none`)
and paints a chevron background at a 12 px right inset mirroring the left
`px-3` padding, kept on the single `<select>` element so call-site width
and margin classes still compose unchanged.
The open-cycle status line repeated the upcoming-dose timing already
shown in the card's next-intake slot; the calm on-track "next dose in N
days" phrasing is now suppressed, leaving only the actionable due /
overdue tiers.
Remove the "recommended next site" nudge from the GLP-1 card — it is
noise on the at-a-glance surface. Injection-site tracking stays: the last
site still reads on the last-injection line and the post-dose picker
still captures the next site.
Make "Period in review" the period-narrative card heading (a proper
title, not a faint uppercase label) and drop the possessive across all
locales.
Render the today-signal card only when there is an actual signal: an
all-clear day (every vital inside its personal range) or an account still
building its baselines now produces no card, rather than an empty header.
The single-status-display contract now expects three integration cards
(Withings, WHOOP, Google Health) instead of four after the moodLog card
removal.
Clicking the avatar (and the name beside it) in the sidebar user menu
now routes straight to the account settings. On the per-medication API
token row, drop the separate "Copy URL" button in favour of an inline
read-only endpoint field with a copy icon, and switch the generate-token
button to the standard outline variant so it no longer renders as the
lone primary-coloured control on the row.
Right-align the health-record export generate button. Remove the
duplicate doctor-report card from the export page (the doctor report is
reached through the health-record export now) and drop the now-unused
ArztberichtHeroCard. Remove the clinician share-link ("sharing") page,
its nav entry, slug, and section mapping; the backing model, routes, and
public clinician view stay in place for a later dedicated removal.
Remove the moodLog card, its render, and the moodLog-only UI plumbing
from the integrations settings — the integration is no longer offered.
The backing columns, routes, and status aggregator entry stay in place
for a later dedicated removal. Also shorten the integration save button
from "Save credentials" to "Save" across all locales, and add the
nav account-settings label.
The notifications settings panel white-screened with "x.map is not a
function" when a status payload reached the card as the whole
{ channels, events } object instead of the channels array (events is an
object map, not an array). Guard the channel map with an Array.isArray
check, default the mood reminder-hour read to optional chaining, and
add a regression test that renders the empty state instead of crashing
on a malformed payload.
When cycle tracking is on, the cycle ring takes the Strain slot in the
health-scores strip rather than adding a sixth tile: the wellness strip
gains a hideStrain flag the insights page sets from the cycle gate, so
Strain is hidden for cycle accounts and shown unchanged for everyone else.
Fix the oversized vertical gap on the insights overview. The cycle summary
card set data-revealed on the same node that carried wellness-tile-rise,
but the entrance keyframe selector is a descendant combinator, so the
animation never matched and the card stayed at the rise rule's opacity: 0 —
invisible yet still occupying its box. Wrap the card so data-revealed sits
on an ancestor, restoring the entrance and removing the phantom gap.
Move the gated cycle entry directly after Medications (before Insights) in
the sidebar nav instead of appending it at the end, spliced by the
Medications index so the position survives a list reorder.
Add an operator-only password-reset CLI. It runs under plain node in the
production standalone image (which strips tsx) via the in-image pg and
@node-rs/argon2, reusing the app's Argon2id parameters through a shared
.mjs module so a reset hash matches one the app would mint. It matches a
single user by username or email, reads the new password without echoing,
never prints it back, and exits non-zero on zero or multiple matches.
Documented in docs/ops/password-reset.md.
Surface the cycle phase ring in the main Insights health-scores strip as a
gated sibling tile. It reuses the cycle wheel SVG, sized to a score-ring
tile, and sources day-of-cycle + phase from the same calendar read +
deriveWheelState the cycle page wheel uses, so the two surfaces never
disagree. Mounted only for a cycle-tracking account (the same /api/auth/me
gate the sidebar nav and the cycle summary card use), so its calendar read
never fires otherwise; it renders nothing while resolving, on error, and
when there is no active cycle — no gap, no placeholder.
Cap the adaptive cell size in the mood and medication compliance heatmaps.
A short window (a single medication, a 30-day mood calendar) left only a
few columns, so the container-width cell sizing ballooned and the square
grid blew the tile to roughly three times its neighbours' height. The cell
ceiling keeps the grid dense and left-aligned within the tile-height rhythm
at mobile and desktop.
- Phase education card no longer carries its own log-today button (the
top-level log entry covers it).
- Predicted-period calendar days render as a soft dashed fill instead of an
underline, so they read as period rather than an unfinished dash.
- The derived batch route now attaches the deterministic per-score assessment
for the five scores so the dashboard grid (and iOS) paints the explanation
without a second per-id round-trip; guarded so it never fails the grid.
Remove the duplicate phase-education card from the calendar tab, keeping the
single instance under the wheel and surfacing its "you often notice this here"
chip row there. Drop the generic prediction-estimate caption on the predictions
tab for every goal except AVOID_PREGNANCY, where the contraceptive-safety caveat
stays as a deliberate medical note. Stop rendering the associations-not-causes
footer on the insights crosstab. In the cycle settings, align card-header
descriptions with their titles, and remove the standalone cycle export + purge
affordances now that cycle data rides the main export and account-level delete.
Move the web-push enable action into the card header row.
Add a compact cycle teaser to the main Insights overview for accounts with
cycle tracking enabled. The card shows the current phase, cycle day, and the
single headline phase finding, and deep-links into the full cycle insights tab
so the cycle tab stays the single source of truth.
The card mounts only when cycleTrackingEnabled resolves true from
/api/auth/me — the same gate the sidebar cycle entry uses — and renders nothing
for every other account. Phase and cycle day derive from the same calendar read
plus deriveWheelState the cycle wheel uses, and the headline reuses the shared
CyclePhaseHeadline, including its honest empty line when nothing has cleared the
significance check.
The cycle view now opens the insights tab when the URL carries ?tab=insights,
validated against the known tab values; CycleView is wrapped in a Suspense
boundary for the search-params read.
The phase-crosstab server emits display "mood" and "glucose", but the
client unit/label maps only covered the vital channels, so a surviving
mood or glucose phase correlation passed an undefined key to t() and
crashed the insights tab. Add the missing maps and harden both unit
lookups to fall back to an empty unit on any unmapped display.
- Add CYCLE_FERTILE_SOON to the cycle-purge push_attempts eventType list so a
data reset leaves no dated reproductive-intent delivery trace.
- Rate-limit the custom-symptom mutations (POST create, PATCH, DELETE) under a
shared cycle:symptom:custom:<userId> bucket (30/min), mirroring the insights
route, so a session can't flood the encrypted-label catalogue.
- Re-run the active-count cap inside PATCH when a symptom reactivates
(hidden → active); a hidden row doesn't count toward the cap, so without the
recheck a user could exceed it by hiding then re-enabling rows.
- Gate the log-day "limit reached" copy on the cap errorCode rather than any
create failure, so a transient/validation error no longer masquerades as the
cap being hit; thread the returned key so a new custom symptom auto-selects.
- Move the history-chart bar fill alphas into named phase-tokens
(HISTORY_REST_OPACITY / HISTORY_PERIOD_OPACITY) so bar intensity lives in one
token source alongside FLOW_OPACITY.
- Strengthen the confirmed-ovulation calendar marker (ring 1.5px → 2px, fill
alpha 0.16 → 0.22) so it stays distinct from the predicted dot on small light
screens; keeps its data-attr + aria naming.
- Raise the phase-education chip border from /10 to /15 for legible contrast on
the tinted tile.
The log sheet's chip grid imported the icon allow-list from custom-symptoms.ts,
which pulls node:crypto + the AES helpers into the client bundle and broke the
Turbopack build for /cycle. Split the client-safe constants and Zod schemas into
custom-symptoms-shared.ts; the server module re-exports them so the API routes
are unchanged, and the client imports from the shared module.
Mirror the v1.13 custom-mood-tag CRUD for cycle symptoms. A user can mint
their own symptoms beyond the 15 seeded ones, picked from a label + icon
popover via a dashed ghost-chip in the log-day sheet's symptom grid.
- POST/GET /api/cycle/symptoms/custom and PATCH/DELETE on /[key]: mint a
custom:<uuid> key, encrypt the free-text label at rest (AES-256-GCM),
per-user cap, owner-scoped, gated behind requireCycleEnabled. Soft-hide by
default, hard-purge on ?purge=true.
- The day-log write already resolves a symptom key against the catalogue plus
the caller's own rows, so a custom: key links by construction.
- The log-day sheet fetches the user's customs and merges them under a new
custom category in the chip grid; the ghost-chip + popover reuse the chip
styling and the Lucide icon set.
- Seed the global custom symptom category (migration 0134).
- The purge route (DELETE /api/cycle/all) now drops the user's custom symptom
rows too, so no intent-revealing label survives a purge.
- The label is never logged: only the icon + counts reach annotate/auditLog.
- i18n: cycle.symptom.custom.* across all six locales.
Add a CYCLE_FERTILE_SOON reminder that fires two days before the
predicted fertile-window start. Twice-gated and default-OFF: it only
surfaces when CycleProfile.goal is TRYING_TO_CONCEIVE and fertileWindowStart
is populated, so fertile language never reaches the avoid / general / peri
goals. Reuses the existing cycle-reminder cron, discreet mode, clientManaged
suppression, idempotency ledger, and push-attempts plumbing verbatim; the
body carries the non-medical, not-a-contraceptive framing.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>