The third and final layer of the stale-tile bug: saving invalidated the
dashboard snapshot with the default refetchType 'active', but the dashboard
page is unmounted while the user sits in settings — the cell only went stale,
and the snapshot query's deliberate refetchOnMount: false then suppressed the
re-read on same-tab navigation back. The save, reset and ring mutations now
invalidate with refetchType 'all', refreshing the cell mounted or not
(proven live in one SPA session against the real query client).
The sortable settings editors (dashboard layout, medication order, modules,
mood tags x2) painted a second page scrollbar: the sr-only drag hint below
each list is position: absolute, no ancestor was positioned, so it resolved
against the initial containing block, escaped the shell's overflow clipping
and lengthened the DOCUMENT by its static offset. The list wrappers are now
relative; the e2e overscroll guard asserts the one-scroll-floor on these
routes and was negative-tested against the revert.
A one-time background pass re-reads each connected account's Google Health
sleep history so nights stored twice before the sleep fix collapse to their
true total (new sleep_repaired_at marker, migration 0238; watermark-safe,
enqueued once at boot per un-repaired connection).
The sleep night read reports when two writers disagree about the same night —
the served total is unchanged, the disagreement rides along as an annotation
(sourceDiscrepancy on the night DTO) so clients can mark the number instead of
silently picking a side.
A GLP-1 medication whose weight has plateaued on the current dose says so next
to the estimated drug level (efficacy tab + insights) — a factual note from the
existing detector, association only, verb-guard covered.
Google Health diagnostics: the sync log now separates a daily-totals read that
returned nothing from one the importer could not parse
(googleHealth.rollup.emptyResponse vs droppedAll), and the connection test's
structure probe reports the raw response envelope key names when a rollup type
parses to zero — a per-account naming drift becomes visible from one report.
The GLP-1 estimated active-substance curve (the modelled drug level from logged
doses) now also renders in the efficacy (Wirkung) tab and in the Insights
medication overview, not only under the injection tab. Both reuse the same
self-contained DrugLevelChart, gated on the GLP-1 treatment class, so the drug
level sits next to the efficacy readouts. Additive — no existing surface changes;
non-GLP-1 medications render nothing.
Documents vault card: date + size and an AI-read marker now sit directly under
the title instead of the meta line running off under the thumbnail.
Document detail view: a larger close control replaces the small X and the
redundant top-right shortcut; the inline read-with-AI row under the preview is
gone (the suggest / summarise / show-text actions stay); a button to ask about
the document sits next to share and download.
Mental-wellbeing cards no longer wash the whole tile on hover — only the title
is the open target, matching the medication cards.
Medication API settings render inline instead of behind an expander that never
had anything beside it.
Add the per-medication "Wirkung" tab (hidden for one-shot and no-target meds)
rendering the target series with a start marker, dose-change markers, shaded
pause bands and a faint reference band, an adherence lane, a neutral
before/after-start card with honest empty states, an optional level-shift note,
and a retarget chooser. Weave a compact, read-only efficacy summary into the
Insights medication surface that reads the same DTO and links through to the
tab. All copy is descriptive; add a banned-verdict-verb guard test over the
medications.efficacy namespace across all six locales.
Introduce a shared PageAuthGate (header + tile skeleton) and adopt it on the
top-level pages that flashed a lone centered spinner while auth resolved:
mood, measurements, achievements, illness, mental-wellbeing, checkups, cycle,
and the labs / medications / custom-metrics / illness detail routes. The
auth-resolving frame now reserves the page geometry instead of snapping in
after a spinner.
Port the six medication dialogs (log-intake, intake-edit, dose-history-add,
take-all-due, log-injection-site, intake-import) from the centred Dialog to
ResponsiveSheet with the sticky-footer slot, matching every other module's
add/edit form. Form-backed dialogs associate their footer submit button via a
useId form id so Enter-to-submit and the footer button share one form; the
nested slot-nudge confirm rides the same sheet. Primary actions now sit within
thumb reach on phones.
- CustomMetricList load error uses QueryErrorCard with retry; post-create
navigation switches from a full-page window.location.assign to router.push.
- CustomMetricDetail + medications list swap their hand-rolled error blocks
for the shared QueryErrorCard.
- Side-effect delete now uses the themed AlertDialog confirm instead of a
native window.confirm.
- Admin SettingsToggle: name the Radix switch via aria-label so it no longer
announces as a bare 'switch, on/off' (one shared component, six call sites).
- Replace the phantom text-destructive-foreground / text-warning-foreground
utilities with the vetted destructive pattern (bg-destructive text-white
dark:bg-destructive/60) and a readable foreground on the warning wash — no
more dead classes riding on cascade order.
- Drop the sub-AA opacity tiers: lab sparkline + titration marker off
muted/70, state icons (query-error, chart empty/error, med sort) off
muted/60 to the full token so they clear the light-theme floor.
- Chart range toggles expose aria-pressed (lab, custom-metric, health, mood,
compliance) so the active window is conveyed to assistive tech.
Theme provider now initializes to the SSR-stable default and reconciles
the stored preference in a mount effect instead of reading localStorage
in the useState initializer — closes the same React #418 hydration seam
the sidebar already fixed (the inline script still handles the pre-paint
class, so no FOUC). Add a process-wide Intl formatter cache and route the
per-row workout/medication list helpers through it so a formatter is
constructed once per (locale, options) rather than once per row per
render.
Every next/dynamic chart import used to point at its own module, so
Turbopack emitted a separate chunk group per boundary — the production
build carried EIGHT ~312 KB copies of the recharts module graph, and
/insights (the worst-LCP route) downloaded two of them (~92 KB gz of
pure duplication, one fully unused at load).
All chart loaders now resolve the single chart-runtime barrel
(src/components/charts/chart-runtime.ts), which owns the only path to
recharts: one shared chunk group, recharts exists exactly once in the
build (verified: 8 -> 1 recharts-fingerprinted chunks), cached across
routes. Charts stay recharts and stay pixel-identical; every call site
keeps its own matching skeleton and ssr:false flag.
Riders of the same rule:
- the mood-trio barrel (mood-charts.ts, the v1.16.7 precedent this
generalises) folds into the runtime and is removed
- the vitals sparkline body extracts to delta-sparkline.tsx and loads
through the runtime; the tile keeps the fixed 40 px band, so the
async gap is an identically-sized empty box
- /cycle's BBT chart (the last static recharts import on a live route)
defers through the runtime with a card-matching skeleton
H3
Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:
- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
update-available pill and offline-geo hint → warning; the admin
helpful-rate ramp → success/warning/destructive; the confidence
meter mirrors the coverage-meter precedent (medium + low share the
caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
classes (no generated utility) for border-border + bg-muted/50
Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
Add healthlog/spacing-scale (error): pt-*/pb-* on CardHeader/CardContent
fights the gap-based Card contract — the header→body distance comes from
the parent flex gap, so slot padding stacks on top of it or is a no-op.
This is the drift class the earlier slot-padding sweep cleaned; the rule
keeps it from reaccumulating. Density belongs on the Card itself
(gap-2 py-3 md:py-4). No allowlist — an exception, should one ever be
load-bearing, carries an inline disable with its reason.
Remove the two stragglers the rule caught: pb-1 on the device-score tile
and pb-4 md:pb-6 on the intake history list, both stacking dead padding
on top of the card gap. RuleTester coverage for the modifier-prefixed,
arbitrary-value, cn()/ternary/template and member-expression forms.
Eight data surfaces still rendered a read failure as a bare
text-destructive paragraph with no retry: the biomarker manager, the lab
values list, the three record managers (allergies, family history,
conditions), the illness episode detail, and the intake history table;
the lab biomarker detail carried an inline near-duplicate of the
primitive from before it existed. Replace all of them with
QueryErrorCard wired to the query's refetch, keeping each surface's own
error string. Inline role="alert" text remains the pattern for mutation
errors inside forms.
The Card has been a gap-based flex column since v1.4.33 (gap-4 md:gap-6;
CardHeader carries no bottom padding), but 29 CardHeader pb-0/1/2/2.5 and
8 CardContent pt-6 overrides from the padding-era shadcn card survived.
Under the gap model they invert their intent: pb-* renders gap PLUS
padding (looser, not tighter), pb-0 is a no-op, and pt-6 on a header-less
CardContent stacks on the Card's own py-4/6 and makes the card top-heavy
(40-48 px top vs 16-24 px bottom). Delete them all; a genuinely tighter
header-to-body distance is expressed on the Card itself (className="gap-2"),
the only knob that controls it. An ESLint guard for these slots is a
follow-up.
The muted-foreground token is AA-tuned in both themes; the /70 and /80
opacity modifiers silently undid that (2.9-4.2:1 measured). Every
informational text node drops the modifier for the full token: sleep
hypnogram captions, learning-gate caveat, integration-card setup hints,
module lock notes, glucose panel and token-footer meta, invite-token
hint, wizard hint, notification channel labels and disabled notes (the
disabled state reads through the note and the disabled switches, not
through dimming), plus the resting state of the timestamp, suggestion
chip and sidebar-collapse controls. User-authored mood notes move to
foreground text-sm — user data is never muted. Decorative non-text
strokes (sparkline grid, calendar dot, titration connector) keep their
opacity; icons keep the /60 graphics floor.
The injection/GLP-1 card showed an overdue dose twice: once on the
next-intake value line (the destructive escalation with the due time)
and again as the open-cycle status line below the compliance bars. The
cycle line's missed tier is retired; the due-today tier stays. The
reserved min-h slot in the shared card body is untouched, so card
heights stay symmetric across medication types.
Add the healthlog/no-raw-palette-color rule so UI code routes color
through the semantic tokens instead of Tailwind palette shades, and
sweep the remaining offenders (password-strength scale, injection-site
accents, redundant dark overrides). Add guard tests for width stability,
the single page-header vocabulary, and motion-reduce coverage on
indefinite animations.
Swap the remaining native date inputs (allergy onset, environment
travel/backfill ranges, inbound-document dates) for the app DateField
so they no longer render in the browser's locale order. Route the
medication caution amber and the document info banner through the
warning/info tokens so they adapt to the theme.
The per-dose "custom window" switch could never be turned on. Switching it on
seeded the range from the default band and emitted immediately, but the
serialiser drops a range equal to the default band, so the persisted value
round-tripped straight back to empty, the controlled `checked` recomputed
false, and the switch snapped off. The explicit range fields only appear once
the switch is on, so there was no way to edit the range into a non-default one
first — a deadlock.
Track the opened-switch intent in local editor state, decoupled from
serialisation. A toggled-on row renders its default band immediately and the
switch stays on; an explicit range persists only once the user edits it away
from the default. Toggling off drops any stored range back to the default
derivation.
The card header carried the dose on its own muted line under the name and the
category as an outline badge below that. Surface the dose as a tag sitting next
to the category badge instead — `[7,5 mg] [Sonstiges]` — so a blood-pressure
tablet and a GLP-1 injection present their strength the same way, in one tight
badge row directly under the name. Both cards share this header, so the generic
and GLP-1 variants pick it up together. State badges keep their own row below,
so the narrow-viewport two-line contract is unchanged.
The medication and GLP-1 cards print a per-schedule dose override as a
trailing accent ("— 5 mg"), straight from the stored string, so the same
raw unit token the main dose carried still showed through there. Run it
through formatDose so the per-schedule accent localises like the rest.
A medication's dose is stored as free text the wizard composes from an
amount and a unit token ("1 pieces", "2 tablets"), and the card rendered
that token verbatim glued onto the name — so the unit showed in English
on every locale and the title read "Test ABC 1 pieces".
Translate the dose at display time: a formatDose helper splits the stored
string and runs a known unit token through the existing unit messages, so
"1 pieces" reads "1 Stück" / "1 piece(s)" per locale while a custom
free-text dose passes through untouched. The card header now carries the
name alone on its title line with the dose on a muted line beneath it,
above the category badge; the detail view, table and intake dialogs
localise the same way. Vorsorge and lab rows pass an empty dose so they
render no extra line.
The native datetime-local / time picker renders its clock and AM/PM
toggle by the browser UI language, not the page lang or the app's
time-format preference — so an English-language browser showed AM/PM
even with 24-hour selected. Replace the native time control with an
app-controlled TimeField (text entry + a dependency-free popover of
hour/minute, plus AM/PM in 12-hour mode) whose hour cycle always tracks
the preference. DateTimeField now composes the native calendar (date)
with TimeField (time); the committed value stays a local
yyyy-MM-ddTHH:mm string, and min/max clamp the recombined value.
Also thread the time-format preference into the doctor-report PDF so its
generated-at timestamp honours it instead of the locale default.
The insights alert cards computed medication compliance with the naive
`schedules.length × days` denominator, so a fully-adherent weekly
injectable read ~14% and fired a false low-compliance alert (and a paused
medication mis-fired the same way). Route the alert path through the
canonical `calculateCompliance` engine like every other compliance
surface — pause eras, schedule revisions and dose windows honoured.
Document confirm: committing a numeric observation against an existing
biomarker silently replaced the stated unit with the catalog's, with no
conversion — a stated "6.1 mmol/L" against a mg/dL marker committed
"6.1 mg/dL". Reject on a unit mismatch (fail closed) so the user
reconciles on the review screen, the same posture as the unit-required
guard.
Wire grip strength, waist (with waist-to-height) and pain into the
briefing feature set and the grounding allow-set so the clinical-depth
signals reach the daily narrative; the mental-health screeners stay
excluded by construction.
Biomarker detail: fall back to a generic description for catalog-less
markers, and keep the heading + description with a retry on the error
state. Collapse the medication External-API and Lifecycle groups by
default. Re-enable the insights scroll-to-top regression and assert the
shipped main-content reset.
Align the EN haemoglobin/haematocrit display names to British spelling.
Drop the redundant `(userId, deletedAt)` index on inbound documents — a
strict prefix of the new three-column index. Trim the dead
MedicalDisclaimer variants to the one live `dataPosture`. Move the
crisis-resources card off the retired amber tint to a calm
destructive-subtle border. Add a registry invariant asserting every
biomarker signal key resolves to a real catalog slug.
`estimateDailyDoseCount` only special-cased FREQ=MONTHLY and FREQ=YEARLY.
A modern FREQ=WEEKLY rrule (the encoding the create path stores, with an
empty daysOfWeek) fell through to the legacy branch at seven days per week,
so a once-weekly injection counted as one dose per day. The detail card's
"lasts about N days" and the daily low-stock job — which share this math —
fired reorder alerts roughly seven times too early (fourteen for bi-weekly).
Parse FREQ=WEEKLY honouring INTERVAL and BYDAY count:
perDay += (times * bydayCount) / (7 * interval).
The three intake dialogs (log, edit, dose-history add) still used a raw
datetime-local input, so they ignored the user's date-format preference.
Route them through DateTimeField like every other date surface. Also
correct a stale diastolic-floor comment in the dashboard verdict.
Swap the four DateInput callsites (filter bar, export range, schedule
history, onboarding baseline) to DateField so the user's date-order
preference paints every date input. Adapt the onChange from a native
event to the direct ISO value DateField emits, and add an optional
autoComplete prop so the onboarding date-of-birth field keeps its bday
autofill hint.
Swap the medication scheduling, inventory, illness, vorsorge, lab-OCR
and profile-birthdate date inputs from the native field to the
DateField primitive, which paints the chosen DMY/MDY/YMD/AUTO order
over an ISO yyyy-MM-dd value. The committed value contract, min/max
bounds and aria wiring are unchanged.
The medication detail clients computed the supply headline in the browser
via summariseSupply, risking web/iOS drift and pulling the shared math
into the client bundle. The GET /api/medications/[id]/inventory route now
computes the canonical SupplySummary server-side through summariseSupply
(the single source of truth) and ships it in the DTO; the Bestand and
Übersicht surfaces render the ready figures. Adds the MedicationSupplySummary
schema to the inventory list response and regenerates the OpenAPI contract.
Drop the grey cadence pill from the card heading. The cadence now reads as
part of the metric line, styled like the metric label itself (no shaded
background), via a new MedicationCardHeader metricMeta slot. Nothing else
sits beside the heading.
Link a measurement-linked reminder's heading to the measurements history
pre-filtered to its type, reusing the existing kebab target. Free-text
reminders keep a plain heading.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Bring the medication card, scheduling, wizard, and detail component
files in line with the kebab-case file-naming convention. Component
export identifiers are unchanged; only filenames and the import paths
that point at them move.
Sweep the primary touch controls that sat below the 44px tap-target
floor on phones onto the canonical `min-h-11 sm:min-h-9` (text) /
`size-11 sm:size-9` (icon) pattern, condensing back at `sm:` upward:
- Coach composer mic / stop / send icon buttons (were `size-9`).
- Dashboard hero CTA, link and action variants (were `size="sm"`).
- Vorsorge list-view "mark done" button (was `min-h-9`) and its kebab,
which now uses `size="icon-lg"` so the rendered control is 44px
rather than the icon variant's 40px.
- Illness list-view "log day" button (was `min-h-9`).
- Medication wizard Step 3 injection-sites disclosure (was `h-9`).
The Vorsorge dashboard-card capture action was raised alongside the
ListRow migration.
The /medications card and table render the dose-derived stock
(stockUnitsRemaining / stockDosesRemaining) straight from the
GET /api/medications list payload, which reads through cachedSwr on the
60s-fresh + 10-min-stale medications bucket. Registering, adjusting, or
deleting an inventory container changes that stock, but the inventory
routes never invalidated any cache — so the list served the pre-write
stock for the rest of the stale window. On the client the add / adjust /
delete supply mutations only invalidated the per-medication inventory
query, never the medications-list query that the card reads, so a mounted
list never refetched even once the server caught up.
Hard-evict the per-user medications + compliance buckets from every
supply-affecting write (inventory POST, the per-item PATCH / DELETE, and
the deprecated glp1 POST dose-change + ledger branches) so the next read
reflects the change rather than a stale SWR value. On the client,
centralise the supply mutations on a shared invalidateSupplyQueries helper
that drops both the medications-list key and the per-medication inventory
key. The card's neutral styling is unchanged.
The target editor invalidated a bare ["insights"] array on save and reset.
That literal sat outside the centralised factory, so the broad insights
fan-out it intended never matched the reads, which key through
insightsTargets/insightsRoot. Route the editor's reads and invalidations
through userThresholds(), insightsRoot(), insightsTargets() and analytics()
so a saved target actually refreshes every insights surface.
Migrate the other call sites the lint rule does not reach onto the factory:
the Coach conversation rail + thread reads, the per-metric personal-records
badge, the per-day measurement drill-down, the dashboard intake
compliance-chart fan-out, the per-medication side-effects list, the
onboarding auth invalidations, the notification-preferences read, and the
PWA persister's auth-me lookup.
Add the matching factory keys: coachConversations, coachConversation,
personalRecordsByMetric, measurementDrilldown, complianceChartInline and
medicationSideEffects. Each new key reuses the prefix its reads already use
so no invalidation shape drifts.
Mirror the medication card's name + cadence + status layout:
- the cadence ("alle 7 Tage") renders as a chip directly to the right of the
metric name via a new MedicationCardHeader nameChip slot, instead of on the
state-badge row below the category;
- the due / overdue status renders as plain text, not a chip;
- the "zuletzt erledigt" line always renders, falling back to an em dash when
there is no last-done value, instead of being hidden.
The green action button is unchanged. Also drop the per-module tour trigger
from this surface (the tour is first-time-only now).
Bring interactive elements that fell below the 44px iOS/WCAG touch
floor up to min-h-11 on phones (relaxing to the denser sm density on
larger screens), and add visible focus rings where they were missing:
- TimesOfDayChips remove button + API-token copy buttons gain an
inset hit-area so the visual density stays tight while the tap
target clears the floor.
- Dose-history row-actions menu trigger and the insights suggested-
prompt chips raise to min-h-11 sm:min-h-9.
- Sortable table headers and the glucose advanced disclosure toggle
gain a focus-visible ring and a phone tap floor.
Restructure the medication detail advanced surface (Erweitert + API tabs)
into expand/collapse groups: SettingsGroup gains an opt-in collapsible
mode built on the same Radix Collapsible primitive SheetSection uses, so a
long settings wall reads as scannable sections. Lifecycle and external-API
groups default open; Data and the destructive zone default collapsed.
Add a titration timeline to the injection tab — completed and planned
dose-escalation steps rendered as a vertical timeline with a "you are
here" marker. Reads the existing MedicationDoseChange stream (future-dated
rows are planned steps), so no schema change is needed. Neutral palette,
no state-driven colour.
An inventory item's unit count comes from a non-null Decimal column, but a
corrupt or legacy row could hold a missing or non-finite value. The
serialiser ran `Number(item.unitsTotal)`, and `Number(null) === 0` (and
`Number("corrupt") === NaN`), so an unknown count went out on the wire as a
fabricated 0 that a client could decrement into negatives.
Serialise a genuinely-unknown count (null / non-finite) as `null` so clients
render "unknown" instead. A real tracked 0 stays 0. No model change — the
distinction is serialisation-only.
The supply tab now renders an unknown-units row as "—", pre-fills the
correction editor empty, drops the capacity ceiling when total is unknown,
and keeps such a row out of the pooled available-supply headline. The
low-stock alert already excludes non-usable rows, so it does not fire on
unknown stock. OpenAPI marks both unit fields nullable.
The compliance number + card already render neutral (no grade tint); only a
stale comment still described a green take-window highlight the implementation
does not do. Reword it to state status drives a discreet line/glyph, never a
card background/border tint.
Move the low-stock trigger from a bare runway threshold to the larger of that
threshold and the reorder lead time plus one dose interval, so a weekly
medication is flagged before its last dose rather than after — early enough to
reorder. The change only moves the alert earlier; no one's threshold shrinks.
Surface concrete dates (runs out on, reorder by) and a running-low vs last-dose
state through the existing badge and channel, never the card colour. A reorder
lead time is configurable globally with a per-medication override, and the glp1
card flag now runs the same runway decision as the daily check instead of a
stale dose count.
The low-stock alert fired when the projected runway dropped below the
user threshold (default 7 days). For a sparse cadence — a weekly
injection's 7-day floor is barely one dose-interval — the warning landed
with about one dose left, too late to reorder before the supply ran out.
Add an optional per-medication reorder lead time plus a user-level
default (0–60 days, default 10) and widen the alert trigger to
max(lowStockRunwayDays, leadDays + cadenceIntervalDays), so the warning
lands before the last dose for any cadence. Because the trigger is a
max() over the user floor it never shrinks anyone's current threshold; a
daily med with no lead keeps its prior boundary.
The push body and the card notice surface concrete dates — supply runs
out around <date>, reorder by <date> — with a last-dose informational
state and the days/units fallback when supply data is thin. Retire the
stale fixed 4-dose count threshold on the GLP-1 details endpoint and
route its card lowStock flag through the same runway evaluation so the
card and the daily cron agree. Surface the lead default in the low-stock
settings card with progressive disclosure; a single medication can
override it.
Additive schema: nullable Medication.reorderLeadDays (migration 0161, no
backfill) and notificationPrefs.medication.reorderLeadDays. OpenAPI
regenerated.
The intake write invalidates the dashboard snapshot, but the default active
refetch type only refetches mounted queries — and the snapshot is unmounted
while the user is on the medication card or detail page. On navigating back it
remounts under refetchOnMount:false, so the pre-write cache was served and the
due prompt lingered until a hard reload. Force the inactive snapshot to refetch
after every intake write so the dashboard clears as soon as the dose is taken.
The low-supply notice moves onto the adherence meta row beside the streak —
a slot every card already reserves in every state — so a card that is low
on stock no longer inserts a row that pushes its bars out of line with the
card beside it. The gap between the drug class and the first line tightens
at the same time.
The medications list, compliance summary and supply reads now refetch on
every mount. App Router unmounts the page on navigation away and remounts
it on return; inside the client staleTime that return served a cache that
could be minutes old, hiding a dose logged on another device or surface. A
Playwright spec guards the return-navigation refetch.
A dose can now consume a fraction of a unit — a half, third or quarter
tablet for a split pill. The wizard offers a curated fraction set beside
the whole numbers (the most error-resistant input); units-per-dose and the
per-container counts widen from Int to Decimal, so a 30-tablet pack dosed
at a half reads 29.5 after one dose and the days-left projection follows.
The consumption ledger stamps and refunds the exact fraction, and existing
whole-number doses are untouched. OpenAPI regenerated — the three unit
fields move from integer to number — and the units-per-dose hint is
updated across the six locales.
Also anchors a first-use date relative to now in an inventory route test
that had aged past its 30-day in-use window.
- The escalated card row keeps its time anchor: the red badge carries
the matched window or the overdue-slot value beside it, and the
inline row's range stays visible on mobile, where it was the only
clock on the card.
- Take-all-due collects what the cards offer: a dose escalated past its
catch-up band joins the set with exactly the slot the card's own
button would record; as-needed medications never join.
- The table's stock warning follows the user's runway threshold instead
of a leftover four-dose constant; threshold off means no amber, empty
stays red.
- The card itself shows the remaining runway in the warning tone once
it crosses the threshold — only then, the card stays calm above.
- The low-stock push deep-links straight into the supply tab and its
APNs payload keeps url, runwayDays and thresholdDays; supply tab and
threshold setting link to each other; the reminder-thresholds and
notification-prefs endpoints enter the OpenAPI contract.
A medication can declare itself as-needed: no schedules, never due,
never reminded, excluded from every compliance rate — but intakes log
ad-hoc, consume inventory exactly like scheduled doses, and the
history stays. The wizard offers it as a cadence choice and round-
trips it on edit; card, table and detail show a calm marker where the
next dose normally sits, the take-all dialog never collects one, and
the supply runway stays unknowable by construction. (#316)