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.
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)
Stock only decremented on the web card's take button — the status
toggle, the phone sync, the Telegram buttons and the token API all
recorded the dose and left the inventory untouched, which is why a
fresh pack kept reading full. Consumption now stamps the intake row
itself: every path that turns a row taken consumes through one module
— open container first, then earliest expiry with auto-open, spilling
into the next pack and flooring at zero — and every path that takes it
back (undo, delete, a take turned skip, a converged duplicate)
refunds exactly what the stamp recorded. Replays and pending sync
echoes can never double-charge, and the nightly convergence pass
refunds the rows it tombstones. A medication can declare units per
dose, so two 2 mg tablets book as one 4 mg dose and stock reads in
doses with the unit count alongside; containers carry a named type
including tablet packs, hold up to a thousand units for inhalers, and
the GLP-1 card reads the same stock as the inventory tab instead of a
separate ledger. The inventory endpoints join the documented API.
Server and first client render now agree everywhere query state used to
leak into the chrome, checkboxes no longer nest inside buttons, delete
confirmations admit the undo they offer, raw step counts drop their
decimals, the truncated unit label wraps instead, streaks know their
singular, the medication header speaks the user's date format, a failed
coach turn keeps the user's message and says what went wrong, settings
status notes follow a language switch, the locale only flips once its
bundle actually loaded, and the last raw api fetches in hooks move onto
the shared client.
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
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.
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.
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.
The first / last jump buttons rendered at 32px, below the minimum touch
target. Extend the hit area with an invisible inset pseudo-element so the
tappable region is 44px while the visual button stays 32px; the existing
aria-labels are unchanged.
On narrow viewports the dot stepper collapsed every per-dot label, so
the user faced an unlabelled row of bullets with no sense of position.
Add a compact caption below the dots that names where they stand ("Step
4 of 8 · How often") on small screens, and let the per-dot labels show
only from the small breakpoint up where there is room. The dots stay
tappable throughout.
`summariseCadence` and `summariseScheduleDraft` each re-spelled the
cadence phrase, its detail clause, and the interpolation count. Extract
a single `{mode, cadence, subControls}`-shaped helper both call so the
medication-wide and per-schedule summaries can't drift apart, and drop
the now-redundant `summaryKeyForCadence` wrapper. Behaviour is
identical; add coverage pinning the per-schedule summary to the
medication-wide summary's leading cadence segment.
The injection_site column on intake events was read across several
surfaces but never written, so site tracking was effectively dead. Wire
it end-to-end as a per-medication opt-in, gated on an INJECTION delivery
form and default off.
- Schema: add medications.track_injection_sites + allowed_injection_sites
and users.global_excluded_injection_sites (migrations 0099, 0100;
additive, defaulted, no backfill).
- Intake write path: accept an optional injectionSite on the per-med,
status-toggle, and bulk intake routes. Persist it only on a taken
injection with tracking enabled, validated against the medication's
effective allowed set (per-med allowed sites minus the user-level
global exclusion, deny wins) — a disallowed site returns 422 (the bulk
route skips the entry).
- Rotation: extend the least-recently-used recommender with an allowed
filter; the picker disables sites outside the effective set.
- UI: per-medication tracking toggle + allowed-sites editor in the dose
wizard step, a skippable post-dose site prompt on both medication
cards, and a Settings card for the user-level global exclusion.
- Contract: injectionSite is additive on the intake request; OpenAPI
regenerated. New GET/PATCH /api/auth/me/injection-site-prefs.
The site capture is always optional and never blocks logging a dose.
The schedule wizard's time-of-day step rendered the four suggested times
twice: once as the icon chips from the step itself, and again as the
labelled preset row built into the chip component below. Add an opt-out
on the chip component and pass it from the step so only the icon-based
suggestions remain. The period is still conveyed by the icon plus an
aria-label; the offered times and selection logic are unchanged. The
preset row stays on by default for every other caller.
Pure-history medication detail page, legacy step consolidation, and the
finish of the outbound-fetch migration.
- /medications/[id] becomes a read-only history surface; every setting
moves into a new advanced-settings sheet, and the edit trigger becomes
a two-option picker. The list page is unchanged.
- A pg-boss job folds pre-v1.5.0 granular step rows into one canonical
daily total per user per day and soft-deletes the originals; migration
0087 adds a partial index for the converging discovery scan.
- Every remaining server-side fetch routes through safeFetch (Withings,
Codex OAuth + SSE, bug-report, Umami, GlitchTip, Loki); a new
healthlog/safe-fetch-required ESLint rule guards the boundary.
- Fixes: intake-edit dialog seeded from real values, consolidation skips
a unique-constraint day instead of stranding it, avatar upload reads
the body once under a bounded cap, egress lint rejects absolute URLs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new `/medications/[id]/page.tsx` surfaces every action the v1.5.4
form retirement buried — log today's dose, edit the plan, manage
notifications, manage API tokens, import CSV, configure GLP-1 phases
in a focused Sheet, walk a three-tier destructive cascade. The page
reads as a clinical document from header to destructive footer; one
phone-scroll reaches every restored feature.
Section order (recurring variant — 8 sections):
1. Header band — drug name + dose + status pill + edit pencil
2. Today's dose card — two buttons (`[Genommen]` + `[Übersprungen]`)
3. Cadence summary — plain-language line + Bearbeiten → wizard
Step 5 via the new `landingIntent: "cadence"` route
4. Dose ladder / Phasen — GLP-1 only, reuses the v1.4 components
5. Intake history preview — last 14 grouped + footer link to
`/history`; CSV-import trigger lives in the header
6. Notifications — `<Switch>` in `<label>` row + read-only chip
strip; replaced by an "iPhone steuert die Erinnerungen" chip
when `clientManaged === true`
7. Settings — Externe Integration / CSV stub / Phasen
(button → Sheet) / Grace minutes
8. Verwaltung & Gefahrenzone — Card A (Pausieren + Beenden) and
Card B (Verlauf + Medikament löschen) under one heading
One-shot variant collapses to 5 sections per D-3 §6. Paused variant
keeps the same structure with the header pill flipped and the
today's-dose buttons muted.
Every mutation cascades through `medicationDependentKeys` so the
inline compliance chart, dashboard chart and achievement reads
converge in one tick. Tier 1 Pausieren PUTs `{ active }` only;
the server derives `pausedAt`. Destructive CTAs ship `font-semibold`
so the WCAG large-text contrast band catches the destructive-button
white-on-#ff5555. Status pill text is always rendered; the dot is
`aria-hidden` and reads from Dracula `--success` / `--warning` tokens
so the colour stays in lockstep with the theme.
i18n covers DE + EN natively; ES/FR/IT/PL inherit the EN strings as
the locale-integrity guard's fallback contract specifies. New keys
live under `medications.detail.*`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six changes the v1.5.5 detail page leans on:
- Desktop shell widens from `sm:max-w-md` (448 px) to `sm:max-w-[560px]`
so the strip + body stay readable on the wider section icons the v1.5
shell ships. Mobile bottom-sheet keeps the 40 dvh floor so short
screens never collapse the wizard to a stub.
- Outer spacing scales to `p-4 pr-12 sm:p-6 sm:pr-14` so the close-X
has room to breathe and the strip + body share one column. The
fade-only `animate-in fade-in-0 duration-200 motion-reduce:animate-none`
on the step body keeps motion to one vocabulary — the slide proposed
earlier was stripped.
- `<Progress>` gets the explicit `transition-all motion-reduce:transition-none`
so width-only progress reads as the one source of motion on the
strip.
- `landingStepForEdit` takes an optional `intent` arg (`"cadence"` /
`"name"` / `"summary"`); the detail page's cadence-summary row uses
`"cadence"` so the user lands on Step 5 instead of Step 1. One-shot
payloads fall back to Step 8 because the cadence picker is not on
their path.
- The step `<h2>` carries `tabIndex={-1}` and receives focus on
intent-routed edit-opens so the screen reader announces the step
the user actually landed on, not the first input on it.
- `buildCreateBody` takes `forMode: "create" | "edit"` and omits
`notificationsEnabled` on edit. The detail-page notifications
switch is the single source of truth post-create; the wizard now
refuses to round-trip its default back to the API on edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five new payload-helper cases pin the multi-schedule encoder, the
hydration that opens on the list view, the add and remove guards,
and the schedule-id preservation on edit. A new Playwright spec
walks a two-schedule create flow end-to-end and verifies both
summary cards land before save.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A medication can now hold multiple parallel schedules, each with its
own cadence and times. Steps 5-7 of the wizard become per-schedule;
Step 8 lists every configured schedule with edit and remove actions
and surfaces a "Weiteren Zeitplan hinzufügen" card at the bottom.
The hydrator reads every schedule on edit and lands the user
directly on the list view when more than one is present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Component tests pin the path table, the step encoding, the
treatment-class to category mapping, and the create-vs-edit prop
branching. POST /api/medications gains pin-point coverage for the
new DIABETES and ANTIBIOTIC clinical-category values. Six Playwright
specs walk the new dialog end-to-end via the German label surface,
one per cadence shape; helpers consolidate the dialog-open + step
indicator assertions so each cadence spec only carries its own
sub-control selectors. The OpenAPI yaml refreshes to surface the
two new enum values to the iOS client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each of the eight steps lands with its Lucide icon, title, subline
and a single focused field. The cadence radio rows carry the
plain-language description directly under each label (not in a
tooltip), and the rolling-cadence row gets the literal mental-model
line so the patient understands the counter resets on every log.
Step 2 surfaces ten taxonomy rows that each map onto a single
(treatmentClass, category) pair through a hard-coded table; Step 6
swaps its body on the cadence kind picked in Step 5; Step 7 wraps
the existing times-of-day chips primitive with a sunrise / sun /
sunset / moon preset row above it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new MedicationWizardDialog routes through a Dialog on desktop and
a bottom Sheet on mobile via the existing ResponsiveSheet primitive,
with the wizard's own header inside the body and the footer carrying
Back / Next / Save. The dialog tracks an eight-step state machine
whose visible counter follows the path the user actually walks —
one-shot collapses to 5 steps, daily to 7, every other cadence to 8.
The pure helpers (path table, validation gate, request-body builder,
summary formatter, edit-path hydration) live alongside the dialog
shell so the next commits can plug step content in without touching
the wiring. Step content is empty in this commit; the next commits
fill the eight steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>