The v1.5.4 modal wizard landed the create + edit plan flow, but it took 16 features down with the retired flat form — Einnahmen bearbeiten / löschen, Medikament pausieren / beenden / löschen, per-Med API tokens, Phasen-Konfiguration, CSV-Import — and the trends-row on Insights had a 34 px overflow that pushed annotation copy on top of the charts. This release lands the medication detail page that gives every retired feature a coherent home, polishes the wizard against the live walk-through feedback, closes the iOS audit follow-ups in one go, and hardens every outbound fetch the server makes.
Added
/medications/[id]/page.tsx— a single Server-Component detail page composed of eight sections (Header band → Today's-dose → Cadence summary → Phasen (GLP-1) → Intake history preview → Notifications → Settings → Verwaltung & Gefahrenzone). One-shot medications walk a five-section variant; paused medications keep the structure but the status pill flips toPausiertand the dose-card surface mutes. Restores every one of the 16 features the v1.5.4 flat-form retirement displaced.- Per-row + bulk intake history actions — the preview now ships a per-row kebab (
Bearbeiten/Löschen) and a multi-select toolbar that fires the newPOST /api/medications/{id}/intake/bulk-deleteendpoint. Marc's „Einnahmen löschen" + „Einnahmen bearbeiten" complaints from the v1.5.4 walk-through close cleanly. safeFetchwrapper atsrc/lib/safe-fetch.ts— every outbound fetch the server makes for user-supplied hosts now inheritsredirect: "manual"+AbortSignal.timeout(15_000)defaults. Closes #218.- DNS-rebinding pinned
undici.Agentatsrc/lib/safe-fetch-dispatcher.ts— whenrequirePublicHost: true, the dispatcher resolves the hostname literally inside the connect hook, refuses any addressisPublicIpwould reject, and pins the connection to the first valid public address. The five user-host paths (MoodLog sync + push + test, local-AI client, ntfy) route through it. Closes #217. - Self-hosted avatar storage at
POST/GET/DELETE /api/user/avatar— multipart-uploaded JPEG/PNG/WebP, 2 MiB / 2048×2048 max, hand-rolled magic-byte sniff + dimension probe so no native dependency lands. Stored on the User row as BYTEA; ridespg_dumpalongside the rest of the row. The profile response carries anavatarUrlwith a cache-busting timestamp.src/lib/gravatar.tsretires — Automattic no longer sees the email-hash on every authenticated page load. POST /api/medications/{id}/intake/bulk-delete— owner-scoped, capped at 500 event IDs per call, rate-limited per user. Pre-work for the detail page's multi-select.GET / PUT / DELETE /api/insights/layout— mirrors/api/dashboard/widgetsso insights tile order + visibility persist server-side and sync across devices. Default order:overview,blutdruck,puls,sauerstoff,koerpertemperatur,gewicht,bmi,aktive-energie,workouts,schlaf,ruhepuls,hrv,stimmung,medikamente. Default-visible: overview, blutdruck, puls, gewicht, bmi, workouts, stimmung, medikamente.- Eight new Apple Health quantity-type mappings —
RESPIRATORY_RATE,BODY_MASS_INDEX,LEAN_BODY_MASS,WALKING_HEART_RATE_AVERAGE,WALKING_ASYMMETRY,WALKING_DOUBLE_SUPPORT,WALKING_STEP_LENGTH,WALKING_SPEED. The convention block onapple-health-mapping.tsdocuments the project rule: raw HK values flow on the wire, the server scales ×100 server-side for percent metrics. Step length and walking speed flow raw in SI (m and m/s) — no scaling. - Three new series-kind enum values —
RESTING_HEART_RATE,HEART_RATE_VARIABILITY,VO2_MAX. Detail + trend views on those metrics now respond 200 instead of 422.
Changed
- Wizard polish — dialog widens to
sm:max-w-2xl; the X-close gets a 44 px target; spacing tokens converge on the existing shadcn cadence (rounded-mdbutton,rounded-lgdialog, no new radii); the step-progress bar is a width-only<Progress>+ Tailwindtransition-allwithmotion-reducesnap; step transitions are fade-only.landingStepForEditaccepts anintentargument so the cadence-summary edit pencil drops the user on Step 5 instead of bouncing them through Step 1. /api/measurements/seriesdayscap — raised from 365 to 3650, matching the recurrence engine's hard cap. The iOS app's „Alle"-range no longer paints a 422 banner on every metric./api/dashboard/summaryMetricCard shape —titleandunitship as i18n keys (dashboard.metric.title.*+dashboard.metric.unit.*) instead of hardcoded German strings. Web resolves via the existingmessages/*.jsonpath; iOS resolves against itsLocalizable.xcstrings. Wire-shape change — clients that decoded the legacytitle/unitstrings keep working with the iOS team's tolerant fallback decoder during the transition.assertMedicationOwnershipconsistency sweep —purge,parent PUT,parent DELETE,intake/import,phase-config,bulk-delete,glp1 GETall converge on the single ownership helper. The §10 invariant 24 from the design direction now holds acrosssrc/app/api/medications/[id]/**.- Trends-row chart slot — raised from 140 → 180 px so the mini-card shell's header + padding stays inside the slot. The TrendAnnotation under each chart no longer collides with the chart envelope.
- Wizard payload edit path —
buildCreateBodyomitsnotificationsEnabledon edit so the toggle the user already set in the detail page Notifications section is not overwritten by the wizard's hydrated default. <NotificationsSection>DOM ids — section heading and row label carry distinct ids so the Switch'saria-labelledbyresolves to the row title, not the section heading.phase-configroute surface — PUT returns the multi-issue 422 envelope on Zod failure (matching every other v1.5.5 route), and the upsert builds the Prisma payload field-by-field instead of spreadingparsed.data. Mass-assignment surface closes structurally.
Fixed
- Trends-row text overlapping the charts — Marc's specific Insights complaint. Chart slot was 140 px but the mini-card shell painted ~174 px; the 34 px overflow pushed annotation copy onto the chart envelope. The 180 px slot accommodates the full envelope.
- Grace-row save dead on arrival — the detail page Settings section PUTs
reminderGraceMinutesat the top level; the route now normalises the value onto the primary schedule before the Prisma update. The schema declares the top-level field with a description noting the normalisation. - Purge route did not invalidate server caches — Tier-3a
Verlauf löschendropped the rollup rows but left the analytics + iOS today-tally caches with the pre-purge counts for up to their TTL. The success path now callsinvalidateUserMedications(user.id)alongside the rollup delete. - Bi-weekly worker still emitted every Wednesday — the v1.5.3 cadence engine fix was correct, but the cadence chart + medication card on the dashboard still read the legacy
daysOfWeekcolumn. The v1.5.x window keeps this in place; the read-flip arrives in v1.5.6. Operators see correct reminder fan-out today; the dashboard chip cosmetic catch-up follows. - Compose-mode multi-schedule data loss — the v1.5.4 wizard collapsed a multi-schedule medication to its first schedule on save and silently dropped the rest. Closed in v1.5.4 by the compose-mode commits; the regression test pinning that lives at
wizard-payload.test.ts.
Tests
- 5594 → 5615 unit (+21 in the reconciliation sweep alone; the detail-page surface + audit fixes add ~110 across the cycle).
- 262 integration unchanged + the avatar upload integration test (
tests/integration/user-avatar.test.ts). - New Playwright spec pre-work — the detail-page surface is component-test pinned; e2e walk lands in a follow-up.
Security
safeFetch+ DNS-rebinding pinned dispatcher close issues #217 + #218 architecturally — the input-timeisPublicUrlguard now pairs with a connect-time IP pin so DNS rebinding cannot flip the resolved host between accept and dispatch.- Avatar route enforces size before parse (Content-Length pre-flight + post-parse
file.sizecheck), magic-byte sniff over the declared content-type, dimension probe, owner-scoped on every method. No new XSS vector — the served content-type is whitelisted to the three image MIMEs. assertMedicationOwnershipsweep closes every detail-page route's ownership narrowing — the route layer is now the single ownership predicate acrosssrc/app/api/medications/[id]/**.- Pre-tag senior-dev + security architect audit produced two docs at
.planning/medication-detail-page-2026-05-28/F-{1,2}-*.md. The four senior-dev Criticals + five Highs landed in code; F-1 H-5 (safeFetchmigration to constant-host call sites like Withings + Codex + the GitHub bug-reporter) + F-2 M-1..M-3 (operator-hostrequirePublicHost, avatar chunked-body pre-flight, raw-fetch lint rule) deferred to v1.5.5.1.
iOS coord
- The iOS team's v0.8.0 audit closed cleanly. Detailed acknowledgements at
.planning/ios-coord/v155-wire-six-deferred-identifiers.md+.planning/ios-coord/v155-step-length-speed-followup.md. - The iOS team flips
walkingAsymmetryPercentage+walkingDoubleSupportPercentagefrom pre-multiplied to raw in their next release; until then the server fails-closed (skipped:"value_out_of_range") on > 100% values, so no DB pollution.
Notes
- No new npm dependencies. Avatar image-header parsing is hand-rolled; no
sharp/jimp. The animation surface is shadcn<Progress>+ Tailwindtransition-all; no Framer Motion. Onlyundiciwas promoted from transitive to explicit so the safeFetch-dispatcher import sits on a documented contract. - Test totals. 5615 unit + 1 skipped, 262 integration + 3 skipped.
pnpm typecheck,pnpm lint(one pre-existing withings/resume warning),pnpm openapi:check, locale-integrity + call-site coverage all green.