First load showed bare dark tiles, then the mood chart finished alone
and the remaining charts popped in one after another. Every widget now
renders a structured silhouette of its final shape, charts mount in
parallel but stay behind height-stable skeletons until all visible
charts report ready — with a two-second fallback so one slow widget
cannot hold the page — and the reveal fades in once, without layout
shift. The intake repair script becomes self-contained on plain pg so
it actually runs inside the production container, with the runbook
updated to the working invocation.
Users can now tell the assistant about themselves in an encrypted
free-text profile that feeds the coach and briefing prompts as clearly
fenced context, with the coach allowed one targeted follow-up question
when the picture is incomplete. Operators on closed registrations can
mint expiring invite links with a QR code from the admin area; a valid
invite opens registration exactly as many times as it allows and records
who redeemed it. A nightly pass watches three deterministic signals —
low adherence, blood pressure above its target band, a falling recovery
trend — and sends at most one coach nudge per week through the existing
notification channels, gated behind the same opt-outs as every other
assistant surface.
The Withings OAuth callback finally reports its outcome — connected or
a readable reason — instead of dropping the user on the settings page
without a word. The adherence number carries one name everywhere and an
explanation right where it is shown. Demo mode announces itself with a
banner instead of failing silently in English. Bug reports become
reachable from the mobile menu, the about section returns to the
settings navigation, charts link to the target settings they draw,
integrations and source priority point at each other, and the
medication wizard warns when reminders are switched on without a
configured channel. The longest explainer texts shrink to two
sentences.
An off-schedule row now tells the user when the dose had been due, and
its row menu offers to attribute it to that slot when the slot is still
unserved. A pinned intake is a deliberate decision with a visible badge,
it always counts as taken late — never on time — and the pin can be
released again. The read model binds pinned rows by their slot anchor so
a pin outside the catch-up window survives reloads, and the suggestion
respects the schedule's own windows instead of a hard-coded tolerance.
The clock format followed the UI language with no way out — an English
interface forced AM/PM everywhere. A new profile preference (automatic,
24-hour, 12-hour) feeds every formatter through the shared locale
context, persists on the user row and rides the profile endpoints. The
insight pill list in settings gains the same eye toggle the overview
tiles have, so ordering and visibility live in one place; the page-side
'manage detail pages' block folds into a link there. Includes the
locale bundles for both features and the regenerated API contract.
The two-o'clock status crons generated for every account, ignoring both
the per-user coach opt-out and the operator kill switch; they now share
one gated discovery that also skips users the pregenerate pass will
rewarm anyway, and the mood card finally gets the nightly run the other
six always had. Deleted measurements no longer feed any prompt. Locales
without an assistant language fall back to English consistently instead
of German texts with English fallbacks. A rate-limited regenerate stops
claiming success, the dashboard briefing state distinguishes a missing
provider from a pending generation and serves a stale briefing with its
timestamp instead of hiding it, narrative warming actually processes
oldest first, and worker failures reach the error tracker through one
shared reporter while the insight queues gain real retries.
Every detail section now sits on the card surface with the standard
padding instead of a bare border on the page background, the schedule
tab loses its duplicated summary header, and the reminder tab folds
into the schedule tab next to the times it gates (old deep links
redirect). The overview tab becomes a real landing surface — next due
dose, reminder state and supply runway with jump links — instead of a
lone compliance bar. History rows calm down: settled rows carry a
single row menu, actionable rows exactly one visible take button plus
the menu, and the quick-mark toast gains undo. The card menu drops the
advanced-settings entry and its edit action opens the editor it
promises. Schedule-tab controls share one height system, and the
injection charts load on demand so the chart library leaves the
initial bundle.
The score ring's bloom was clipped by the SVG's own viewport — the
drop-shadow sits on a child of the viewBox-filling arc — so the glow
never extended past the circle; the SVG now overflows visibly on every
host. The insight pill strip gains chevron buttons and wheel scrolling
so an overflowing row is reachable with a mouse. Safari favourites
rendered the transparent icon on a white plate; a flattened
apple-touch-icon and a maskable manifest icon fix both. The
time-of-day mood chart adopts the damped fill the weekday chart already
uses, eighteen delete confirmations move onto the destructive button
variant (one of them had the wrong text colour), five confirmation
dialogs gain pending states, the sources dialog stacks its primary
action on top on mobile, and the token form aligns input and button
heights.
The medication list fired one compliance request per card every 30
seconds, and each request scanned the medication's full intake history
and expanded the recurrence calendar five to six times. The endpoint now
mints bands once per request, serves the sub-windows from that single
ledger, bounds the event read to a year of selected columns and sits
behind a write-invalidated 15-minute server cache; the cards poll at
five minutes. The dashboard drops two redundant checklist queries by
reading counts from the snapshot, the BMI chart derives from the weight
series instead of refetching it, the detail page no longer serialises
behind the auth probe and seeds its query from the list row. Expensive
analytics reads gain a shared per-user rate limit, and the service
worker stops serving cross-version shells, enables navigation preload
and trims caches in one pass.
The project licence changes from AGPL-3.0 to the PolyForm Noncommercial
License 1.0.0 for this and future versions: free to use, modify and
self-host for noncommercial purposes; commercial use needs a separate
arrangement. Releases up to and including v1.15.18 were published under
AGPL-3.0 and remain available under that licence. LICENSE carries the
verbatim PolyForm text, and the SPDX identifier is updated in
package.json, the version endpoint, the OpenAPI info block, the image
labels and the README.
An intake edit accepted any takenAt value, so a date typo could park a
dose a month away from its slot and poison the today feed, the rollup
and the rolling next-due anchor at once. The update schema now rejects
future timestamps (with clock-skew tolerance) and values older than five
years, the route rejects edits before the medication's start date, and
the edit dialog caps the picker at now, shows the scheduled slot and
warns when the new time sits more than 48 hours from it. The dialog's
note field is gone — it was never persisted and silently discarded
input.
Blood-pressure fixtures in the in-target integration suite were copied
from live readings; they are now synthetic with the same in/out split so
every assertion holds. Hard-coded operator hostnames move behind env vars
(DEPLOY_LOGS_URL, HEALTHLOG_PROD_URL), the codex client identifies via
the public repository URL, the OpenAPI servers entry points at a neutral
example host, and remaining workflow, docs and comment references to the
operator's instance or working notes are generalised. The compose
whitelist also gains DEPLOY_WEBHOOK_SECRET, which was missing and made
the deploy webhook unusable on compose installs.
The dose-window editor pulled DOSE_WINDOW_DEFAULTS from
@/lib/analytics/compliance, dragging the server-only scheduling graph
(which imports node:async_hooks) into the client bundle. Turbopack
rejected the client chunk; tsc does not model the boundary so the type
check stayed green.
Move the constant into a leaf module with no server imports and
re-export it from compliance so the server path is unchanged. The
client-reachable editor now imports the leaf directly.
Replace the Zeitplan tab's read-only summary placeholder with an inline
editor for the everyday schedule levers: the dose times-of-day and each
dose's on-time window via the shared <DoseWindowEditor>. Frequency and
cadence-kind stay structural — the tab links out to the wizard for those.
<ScheduleTimesEditor> self-saves via PUT /api/medications/[id] with the
full schedules array, preserving every cadence field from the snapshot
verbatim and changing only the times and windows; a window whose dose
time was removed is dropped before the write. Reminder grace is a
read-only echo with a jump to the Reminders tab, its single owner.
Wire the per-dose window into the creation/edit wizard so a new
medication and a structural edit set windows with the same semantics the
schedule tab uses. Step 7 renders the shared <DoseWindowEditor> below the
times picker; the payload carries doseWindows on each schedule draft, the
create body emits only the explicit ranges that still name a live dose
time and differ from the default band, and edit-hydrate round-trips the
persisted windows back onto the active draft.
The step root data attrs stay stable for e2e.
Introduce <DoseWindowEditor>, the configurable on-time window control
shared by the schedule tab and the wizard. Per dose time the user keeps
the precise time (the symmetric +/-1h default band) or sets an explicit
range such as 07:00-09:00; the row shows the resulting on-time band plus
the cadence-aware late tail. The component is controlled over the
persisted doseWindows contract ([{timeOfDay,start,end}]) and validates
HH:mm, start<=end, and timeOfDay membership to mirror the server schema.
Pure window math lives in dose-window.ts (band derivation, late tail,
default-vs-explicit detection, serialise) so it is unit-tested without a
render. Adds the medications.detail.zeitplan namespace across six
locales.
Replace the Verlauf tab's flat intake table with the per-slot dose-history
ledger the shared band engine mints. The tab now renders EVERY expected slot
with its status — on-time, late, skipped, missed, upcoming — even when the
dose was never taken, and tags every off-schedule intake as a standalone
ad-hoc row at its real time. Rows group by day, most-recent first.
Genommen / Übersprungen sit on the dose row and recompute instantly: the
cached ledger and the headline adherence are mutated optimistically (the
engines are pure, so the client mirrors their accounting) before the write
fires, then the dependent-key invalidation reconciles; a failed write rolls
the optimistic snapshot back. A header add dialog records a dose — including a
backdated or off-schedule one — and an off-window take surfaces the "diesem
Slot zuordnen?" nudge, pinning it onto a chosen slot via forceSlotInstant
rather than orphaning it. A compact took/skipped bar sits at the top of the
tab; the card body still never tints by status.
Adherence is derived from the same ledger the user is reading (taken over
taken + missed; skip, ad-hoc, upcoming excluded), so the history view and the
percentage cannot contradict. New status + dialog copy in all six locales.
The medications-list card kebab no longer opens a modal editor or the
stacked advanced-settings sheet. Edit navigates to the detail page's
Zeitplan tab, History to Verlauf, Advanced to Erweitert. The list-page
wizard becomes create-only; editing an existing medication goes through
the detail hero's "Vollständig bearbeiten".
Dissolve the advanced-settings sheet entirely: its five stacked groups
(Lifecycle / External API / Data / Danger zone) now live as inset-grouped
sections in the Erweitert tab. Drop the orphaned modal component, the
standalone SettingsSection wrapper, and the lazy intake-count query the
sheet's danger zone needed (the tab fetches it on demand). Remove the two
tests that only exercised the retired surfaces.
Make /medications/[id] the canonical full-page home for one medication:
a Radix tab strip (Übersicht · Zeitplan · Erinnerung · Bestand · Verlauf ·
Injektion · Erweitert) with ?tab= URL state, a read-only hero, and a
"Vollständig bearbeiten" jump into the wizard for structural edits.
Everyday levers move inline into the tabs.
Übersicht surfaces the adherence bars, Erinnerung owns notifications +
reminder grace, Bestand reads the pen/vial inventory (all medications,
calm empty state), Verlauf renders the editable intake history, and
Injektion (injectable routes only) carries the existing GLP-1 charts +
side-effect log. The ledger, the per-dose window editor and the
injection body-map land in the following waves.
The legacy /medications/[id]/history route now redirects to ?tab=verlauf
for bookmarks and deep links. Tab labels are i18n across six locales.
Swap the hand-rolled per-group slot substitution for the same pure,
total-order rebuildTilesWithReorderedVitals helper the inline edit mode
uses, so the pill-sort control renumbers tile order identically and stays
in lockstep with the overview arrange surface.
Tapping the top-right regenerate button POSTs an inline ~1500-token warm
generation that routinely runs past the 8 s read budget. The force branch
shared that 8 s AbortController, so a slow-but-successful generation was
discarded: the abort returned null, the mutation only invalidated (re-
reading the OLD cache), and the falling-edge toast claimed success while
the text never changed — even though the server had often written the
fresh briefing.
Give the force branch its own 45 s budget (matching the out-of-band warm
job) so a slow success is kept, not dropped. Tag every fetch outcome
(fresh / empty / timeout / no-provider) and surface the last regenerate
outcome through the tab strip, which now toasts "refreshed" only on a
genuinely fresh result, shows a "still working, try again" hint on a
timeout, and stays silent when no provider is configured. The read path
keeps its fast 8 s cap.
The tab strip gains a settings cog immediately left of the regenerate
button, linking to the new Insights settings section (overview arrange +
pill sort). It mirrors the Dashboard cog idiom and travels with the
regenerate affordance so the two top-right controls sit together.
The Insights overview customise surface now has a home in Settings,
analogous to the Dashboard section. It hosts two blocks:
- "Arrange the overview" embeds the inline section show/hide + order
editor, persisting to insightsLayoutJson.sections.
- "Sort the navigation pills" is a dedicated drag-reorder control for
the tab-strip pills, persisting to insightsLayoutJson.tiles[].order —
a field already separate from the section order in layout v2, so the
two concerns stay independent. Both write through the existing
/api/insights/layout contract the tab strip and overview share.
Wire the new slug into the settings registry, the section router, the
mobile/desktop nav, and the placeholder icon map. Adds the section copy
and the pill-sort + overview-arrange strings across all six locales.
IN_BED is the TOTAL time in bed (≈ CORE + DEEP + REM + AWAKE), not a sleep
phase, but it was a stacked Bar segment alongside the phases. Stacking the
total on top of its own constituents doubled every bar — a 7 h night drew as
~14 h dominated by the in-bed segment — and inflated the tooltip's per-night
total, which sums the stacked series. Drop IN_BED from STAGE_ORDER so the bar
height and the tooltip total are the real night. The STAGE_COLORS.IN_BED
token and the inBed label stay for the last-night hypnogram, which still
renders the in-bed span. STAGE_ORDER is now exported so the test pins that
IN_BED is not a stack series and the total equals CORE + DEEP + REM + AWAKE.
The v1.15.9 take-window highlight washed the whole medication card green
(ring + tinted background) when a dose was in its on-time window. A card
turning green reads as an imperative "take it now" and drowns the rest of
the page once several doses fall due. Drop the surface tint entirely so the
card stays a constant neutral surface; the take-now / overdue / very-overdue
status remains, expressed only through the discreet top status line / pill
(text + glyph), never as a background or border colour. Tests assert the
neutral surface across every dose status plus the surviving status pill.
The default insights layout shipped a curated visible subset (v1.15.11),
authored for the overview Vitals/Mobility grid. Once the tab strip began
gating its nav pills on the same layout, that subset dropped ~20 nav pills
for a fresh account with data in Sleep, Steps, Active-Energy, body
composition, Walking/mobility, Audio, Daylight, etc. — pills that show
today — and caused a flash-then-vanish on first paint.
The grid renders a fixed mapped subset regardless of tile visibility, so
defaulting every tile visible does not bloat it; what it restores is the
nav's everything-with-data default. The user hides what they don't want.
Also:
- mood distribution + weekday charts: switch axis tick fill from the raw
--dracula-fg (white-on-white on the light-mode card) to the shared
--muted-foreground axis-text token; dark mode unchanged.
- drop the orphaned insights.editMode.manageTiles key from all six locales
(replaced by manageSubPages) and add a manageSubPagesHint noting that
hiding a detail page also removes its top-nav pill.
- pin the all-visible default with a layout test and a tab-strip
default-nav regression test.
The Anpassen tile manager only listed the ~14 vitals overview-grid
tiles, so the rest of the routed sub-pages had no toggle anywhere. Now
that the tab strip reads the same per-tile visibility, the manager
becomes the one place to show, hide, and reorder every sub-page.
Broaden the Kacheln-verwalten disclosure to the full sub-page universe,
grouped by category (vitals, body, activity, sleep, cardiovascular,
hearing, environment, metabolic, mood, events) the same way the strip
groups them. Each group is its own drag context so a reorder never
crosses a group boundary; rows reuse the strip's labels so a manager row
reads like the nav pill it governs. The single tiles layout this writes
now drives the overview vitals grid, the tab-strip pills, and this
manager. Relabel the disclosure to Detailseiten verwalten / Manage
detail pages across the six locales.
The top insights navigation was driven purely by data availability: a
sub-page pill appeared whenever its metric had readings in the 90-day
window, with no connection to the per-tile visibility the customize
panel writes. Hiding a sub-page in Anpassen therefore never removed its
nav pill, and the two surfaces drifted apart.
Thread the resolved insights layout into the tab strip via the layout
shell. A pill now shows iff its metric has data AND its slug is visible
in the saved layout; data availability stays the floor, so a
layout-visible metric with no readings still gets no pill. Pills and
group children order by the saved layout order, falling back to the
natural slug order. While the layout query is in flight the strip keeps
the data-only gate, so first paint is unchanged and a slow or failed
layout fetch never suppresses a pill the user has data for.
Add a total slug to manager-group mapping in sub-page-metric so every
routed sub-page resolves to a category, a superset of the tab-strip nav
grouping that also covers the flat pills and the sleep / mood / events
categories the strip never collapses.
Both charts derived their height off the full card width via aspect-[3/2],
which ballooned them on a wide viewport and let them dominate the mood
subpage. Bound each to a fixed 150px band so they read as tidy sibling
cards, and align the deferred-load skeletons to the same band.
Soften the saturated full-bleed mood hues to a muted tint (fillOpacity
0.55) so the bars sit calmly over the card while the per-level hue still
distinguishes the bands. On the weekday chart, drop the bright-green 3.5
target guide to a quiet comment-grey dashed line. The shared mood-level
tokens used by the heatmap and legend are untouched.
The page-scoped selection bar sat sticky bottom-2, behind the fixed
64px mobile bottom-nav, so the destructive Delete (N) button was
unreachable on phones and ignored the iOS home-indicator inset. Lift
it to clear the nav band + safe area on mobile and drop back to
bottom-4 at sm+ where the nav is hidden.
The per-row select checkbox wrapped a Radix checkbox (a 16px button)
in a 44px label; a label does not forward taps to a button, so the
effective target failed WCAG 2.5.5. Make the 44px area a single
button that owns the toggle, with the checkbox a pointer-events-none
controlled visual, avoiding any double-toggle.
Bring the mood pagination buttons to the 44px target used by the
measurements list. Add (sort: Desc) to the two source-filter indexes
so the schema matches the DESC migration SQL; schema-fidelity only,
no new migration.
Extract the duplicated list chrome — SortableHead and the single-row
DeleteButton — into a shared src/components/data-list/ module and add a
page-scoped multi-select with a selection action bar, used by both the
measurements and mood management lists.
Filter bar: each list keeps its primary Select (type / mood level) and
gains a source Select (measurements sends sourceEq, mood sends source)
plus an optional date range wired to the existing from/to params. Every
filter resets pagination to page 1 and is baked into the query key via
the centralised factory (queryKeys.measurementsList / moodEntriesList)
so the cache stays correct per filter combination.
Multi-select is scoped to the current page: a leading checkbox column on
desktop, a checkbox on the mobile cards, and a select-all-on-page header
checkbox. When at least one row is selected an action bar shows the count,
a clear affordance, and a destructive Delete (N) behind an AlertDialog
confirm that POSTs to the bulk-delete route, then invalidates the same
dependent-key bundle the single delete busts and clears the selection.
Selection clears on any page / filter / sort change; the selection math
lives in a pure, unit-tested helper. The locked-type insights subpage
keeps its focused single-metric view (source filter + date range
suppressed).
New strings land in a dataList namespace plus per-list source-filter and
bulk-delete keys, translated across all six locales.
The score-anatomy detail page now renders the per-score AI assessment that
already rides GET /api/insights/derived (shipped to iOS in v1.13.2) — the web
type dropped the field, so the card was silently discarded. The assessment
mounts below the composition card via the same InsightStatusCard the metric
sub-pages use, so it reads identically for readiness, sleep, recovery, strain
and stress.
Each score detail page also gains an educational intro caption beneath the
heading, mirroring the metric sub-pages: new explainer bodies for the five
scores across all six locales (sleepScore kept distinct from the sleep
metric's own body), wired through SubPageShell's explainerMetric prop.
The explainer-key guard resolves the score page's closed lookup record
statically so the single dynamic prop stays verifiable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- carry the ring's per-metric tint onto the score-anatomy detail card so
the tap-through reads as the same surface deepened, not a plain card
- pad the hero ring so its bloom is no longer clipped top/bottom
- lighten the detail card toward the bottom with a subtle theme-aware
gradient
- drop the "keine klinische Bewertung" footer line; the non-clinical
framing stays behind the provenance explainer, the composition rows stay
- move the info explainer from the card foot to the top-right corner at
heading height; popover/sheet behaviour unchanged
Add a dedicated version tile to the admin overview showing the running
version, short build SHA, and build timestamp, alongside a best-effort
'update available' hint computed against the latest GitHub release tag. The
hint reuses the existing /api/version/check-updates proxy (server-side
safeFetch past the CSP) so no new route or OpenAPI schema is introduced; the
check is day-stale and never blocks the render. Cookie-admin only by virtue of
the admin layout.
Shorten the period-narrative section title to 'Rückblick' (and the idiomatic
short equivalent per locale) — the same i18n key the customizable-overview
edit-mode labels read, so the rename flows through automatically.
Drop the redundant 300px min-height floor on the Trends cards. The chart slot
is a fixed 180px and the caption is clamped to three lines, so the floor
padded dead space below every caption that read as an oversized Trends to
Rückblick seam. The grid's auto-rows-fr plus the fixed chart slot already pin
the cards to a single baseline, so the seam now matches the uniform rhythm.
- Layout PUT accepts version 1 as well as 2 (live iOS client still PUTs
version 1); the serializer normalises to the canonical v2 blob, so a v1
body never 422s. Mirrored in the OpenAPI schema + description; yaml regen.
- Add the Weight tile to the inline tile editor so it can be hidden /
reordered like every other Vitals grid tile.
- Replace the non-transitive Vitals tile-drag comparator with a pure,
total-order rebuild helper (reorder the Vitals subset, substitute it back
into the full list in original-order slots, densify).
- Gated-off section rows keep a disabled eye toggle in the same position
with the gate hint as a caption below the title, so the row never jumps.
- Cap the disclosed Vitals tile list height (max-h-[60vh] scroll,
overscroll-contain) so a tile drag does not fight the page scroll.
- Gate edit mode until the layout GET settles so a save can never flush
defaults over a real saved layout.
- Drop the duplicate top-right "Done" affordance: hide the customize toggle
while editing; the in-card Done / Reset own save and reset.
- Restore focus on enter (edit-card heading) and on close (customize toggle).
Tests: route regression for a version-1 PUT persisting a resolved v2 layout;
unit coverage for the new Vitals rebuild helper (order, non-Vitals stability,
per-tile visibility, no mutation).
Read the resolved insights layout on /insights and render the customizable
region from it. Sections render in saved order, hidden ones drop out, and
the Vitals grid honours the per-tile order + visibility. HeroStrip stays
anchored above the customizable region; every existing feature/data gate
(briefing flag, cycle-enabled, per-tile data) still applies on top, so the
default layout reproduces the prior page.
- useInsightsLayout() reads GET /api/insights/layout through the query-key
factory, defaulting to DEFAULT_INSIGHTS_LAYOUT while in flight.
- orderedVisibleSectionIds + resolveTileLayout: pure, unit-tested helpers.
- Widen DEFAULT_VISIBLE_TILE_IDS with the Vitals/Mobility grid tiles so a
fresh account keeps the tiles it shows today.
Render the user's profile photo in the mobile header user button when
one is set, falling back to the initials avatar otherwise — matching
the sidebar user section. Also surface the admin-console link in the
header user menu for admins, mirroring the desktop sidebar entry.
With no stored preference the app now resolves to dark instead of
tracking the OS. An explicit choice still wins: light/dark persist as
before, and "system" is now persisted verbatim rather than as a
cleared key, so an explicit OS-tracking choice stays distinguishable
from a never-chosen state and survives a reload. The first-paint
bootstrap script mirrors the same resolution so a fresh visitor sees
no flash to light.
Shorten the notification-area label and inbox title from the longer
"Notification Center" form to plain "Notifications" across all six
locales (nav entry, page title, breadcrumb). Refresh the stale inbox
references in the settings-shell and sections tests.
- Cycle ring: the fixed 120 px shrink-0 ring overflowed grid cells on
360 px Android widths. It now caps at its target size but is free to
shrink to fill its cell, so it never pushes the page wider than the
viewport.
- Cycle tab strip: the four long German labels overflowed the equal-width
flex-1 cells and forced a horizontal scroll at 375 px. Below sm the
triggers size to their content (text-xs, no flex-1) and scroll cleanly;
from sm up they reclaim the equal-width layout.
- Calendar day cells: bump the 40 px min-height to 44 px to clear the
mobile tap-target floor.
- Add a Pixel-5 spec asserting no horizontal scroll on /insights and
/cycle plus the calendar tap-target floor.
The cycle view also now feeds the profile's typical lengths into the
wheel so a low-data tracker gets the canonical four-phase ring.
When only a few cycle days are logged, the labelled forward run was short
enough that normalising it filled the whole ring with a single phase. For
a tracker below seven labelled days, derive the four-phase proportions
from the profile's typical lengths instead (period / luteal / cycle
length, textbook 28/5/14 defaults), so the dial shows the familiar
MENSTRUAL/FOLLICULAR/OVULATORY/LUTEAL arcs. The current phase and the
day-of-cycle marker stay driven by the observed labels.
The ring tile reads the shared, 5-min-cached cycle profile to source the
typical lengths (no extra request). Well-populated cycles keep the
observed-share spans unchanged.
The overview is a vertical stack on a single space-y-6 rhythm, but two
seams read wrong: Trends to period-in-review felt too wide while the
cycle summary to signals-of-the-day seam had no gap at all. The missing
gap traced to the cycle-summary card wrapping itself in a display:contents
box, which collapsed its margin so space-y skipped it.
- Give the cycle summary a real section wrapper (heading above the card,
matching every other section) so the inter-section margin applies.
- Document space-y-6 as the single gap, top to bottom; every section is
a real section or renders null, never a contents wrapper that strands
a phantom margin.
Every overview section now leads with a single SectionHeading above its
card: a Lucide icon in the foreground colour and an h2 title at one size
and weight. Previously the overview mixed four heading dialects — a
purple Sparkles glyph on the briefing, no icon on Trends, a white-icon
tile header on the score and vitals strips, and in-card titles buried
inside the period-in-review, cycle-summary and signals cards.
- Add SectionHeading (icon + title + optional trailing action).
- Briefing: purple icon to white; heading moves through SectionHeading.
- Trends: add a TrendingUp icon (was icon-less); subtitle becomes the
trailing action.
- Health scores + vitals: migrate the section headers off TileHeader.
- Period in review, signals of the day, device notifications: lift the
in-card title out to a heading above the card.
- Signals card min-height drops to match its slimmer body now that the
title row left the card.
Remove the per-row dose count (the '· 12 / 12' caption) from the
compliance bars — the percentage, bar and window-days label remain. The
auto-miss engine now makes the two windows' percentages genuinely
diverge, so the counts no longer earn the row noise; the engine still
carries the counts on the wire for other consumers.
Remove the injection-site display from the overview card, including the
last-injection-site shown on the last-intake line. Site tracking and
logging are unchanged elsewhere (the post-dose picker, the detail
history); only the card surface drops it.
Extract a single MedicationCardBody that owns the entire card shell,
header, status line, next/last slot, compliance bars, cycle line and
action row. Both the generic and the GLP-1 card render it and pass only
the value content of their next/last lines, so structure, section order,
spacing tokens and labels are identical by construction and cannot drift
again. This also pins the two cards to the same CardContent spacing
(flex h-full flex-col space-y-3.5) and the same mt-auto action wrapper,
removing the bottom-spacing mismatch between the two cards.
Drive the card's state from the new dose status: highlight the card green
when the dose is in its take-window, and show a top status line that
escalates from 'overdue' to 'very overdue' as a dose passes its miss
cutoff. Upcoming and taken states stay calm.
Two identical compliance percentages with no context read as a stuck
display, even when the math is correct (a rolling weekly med on cadence is
~100% on every window by construction; a daily med with full adherence
rounds to ~100% on both 7d and 30d). Render the taken-of-expected count
after each rate (e.g. 100% with 4 / 4 and 100% with 52 / 52) so the two rows
stay distinguishable and trustworthy. Falls back to the bare expected count
when only the denominator is on the wire, and to nothing when neither count
is available.
Thread a per-window taken count onto ComplianceDisplay (short.taken /
long.taken) alongside the existing expectedShort / expectedLong, wire both
through to the shared bars component, and regen the OpenAPI schema.
Add unit tests asserting the windows do not structurally collapse to one
value: a daily med with misses outside the 7-day window scores 7d above 30d,
and a rolling weekly med with a partial-adherence history yields a short rate
above the long rate. Add card-part tests for the count / fallback / no-count
render branches.