The avatar upload uses the accessible visually-hidden <input type="file">
pattern: the input is sr-only / zero-size and the visible 44px touch
target is the styled label/button that triggers it. The mobile
touch-target sweep now skips hidden/zero-size file inputs and keeps the
44px assertion for every genuinely visible input.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The medication-wizard e2e specs assert the German step counter, but the
seeded e2e user falls back to the English default locale; pin the
`healthlog-locale=de` cookie for the wizard flow and correct the
storage-state cookie allowlist to the hyphenated cookie name the i18n
layer actually reads.
The one-shot cadence integration fixture derived `startsOn`/`endsOn` from
the UTC calendar date but checked emission against the Europe/Berlin
today-window, so the assertion flipped when the suite ran in the UTC
evening. Derive the fixture dates from the same Berlin calendar day the
window uses.
Also refresh the OpenAPI spec version to 1.6.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Per-row Bearbeiten and Löschen actions sit behind a kebab on each
intake row; multi-select gates a bulk-delete toolbar that wires
through to the v1.5.5 bulk-delete endpoint. Closes the two
displaced features from the v1.5.4 flat-form retirement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the glp1 POST cap (30 calls per minute per user) so a caller
firing repeated 500-id deletes cannot pin the recompute worker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the parsed.data spread so a future schema extension cannot
silently land on the Prisma write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The phase-config PUT was the last v1.5.5 route still returning a
flat 400 on Zod failure. Returning the multi-issue envelope means
iOS gets per-field feedback the same way the bulk-delete and
intake routes do.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A bare ["medications", id, "api-endpoint"] tuple escaped the
factory rule via useMemo; the new medicationApiEndpoint key in
the factory restores the single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the two title ids so the Switch's aria-labelledby resolves
to the row label rather than the section heading. axe duplicate-id
gone; the announced name now matches the user's mental model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The destructive sweep in commit af224964 missed the glp1 GET
branch; the handler still hand-rolled findUnique + userId compare.
Lifting it to the same ownership helper closes the §10 invariant
24 gap and aligns the GET with the POST.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tier-3a Verlauf-löschen cascade dropped the rollup rows but
left the analytics / iOS today-tally caches with the pre-purge
counts for up to their TTL. Add invalidateUserMedications to the
success path so every downstream reader sees the fresh state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The detail page settings section PUTs reminderGraceMinutes at the
top level, but the schema only accepted it per-schedule, returning
422 on every save. The route now maps a top-level grace value onto
the primary schedule's reminderGraceMinutes before the Prisma
update; the schema declares the top-level field with a description
noting the normalisation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The list-page medication cards previously pointed their History icon
straight at `/medications/{id}/history`. v1.5.5 promotes the detail
page as the canonical umbrella surface for everything the v1.5.4 form
retirement displaced, so the icon now opens `/medications/{id}` and
the detail page's intake-history preview links onward to the bulk-
delete sub-route in one extra tap.
Two focused tests:
- `medication-detail-header.test.tsx` pins the status-pill contract
(label always rendered, dot rides Dracula `--success` / `--warning`
tokens, dot is `aria-hidden`, DOM order is heading-then-edit).
- `detail-page-cascade.test.tsx` pins the per-section query-key +
invalidation cascade against the central factory. The §9.4
phase-config key lives outside the bundle so the settings section
must invalidate it explicitly; the test documents that contract
so a future refactor cannot drop it silently.
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>
The iOS team had gated both client-side waiting on the server map.
Both flow raw SI on the wire (metres and metres per second
respectively) — the server-side ×100 scaling the percent gait
metrics use does NOT apply here. The convention block at the top of
apple-health-mapping.ts now spells the split out so a future
gait-metric addition picks the right path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1.5.5 detail-page intake-history preview offers a multi-select
that posts the selected eventIds in one round-trip instead of firing
N concurrent DELETEs from the client. The endpoint:
- Asserts ownership through the shared `assertMedicationOwnership`
helper so the 404 leak shape stays identical across `[id]/**`.
- Reads rows scoped by `(eventId IN, userId, medicationId)` so a
leaked id from another medication's history never deletes anything.
- Runs `deleteMany` scoped by the same predicate.
- Recomputes the compliance rollup once per unique dayKey (not once
per event), so a 14-row delete spanning two days closes in two
recompute trips and usually 1-3.
- Returns `{ deleted: number }` carrying the real `deleteMany` count
so the client toast announces the truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The list page was carrying two large dialogs (`IntakeImportDialog`
and `ApiEndpointDialog`) whose state slots had no list-card trigger
wired to them. The slots were only ever set from inside the dialogs
themselves, so they read as dead code on the list page even though
the dialog bodies themselves were healthy.
`IntakeImportDialog` lifts to `src/components/medications/intake-import-dialog.tsx`
as the canonical mount the detail-page intake-history header will
import. `ApiEndpointDialog` no longer mounts on the list page; its
final v1.5.5 home is the detail-page Settings sub-row (lands in a
later commit). The list page drops the two state slots, the inline
dialog bodies, and the now-unused imports.
`medicationDependentKeys` also gains the `compliance-chart-inline`
prefix. The per-medication inline compliance chart used to mount
under `queryKeys.medicationComplianceChart(id)` (which expands to
`["compliance-chart-inline", id]`) but the bundle did not match the
prefix, so an intake mutation left the chart up to 60 s stale. The
prefix now lands in the bundle so every detail-page mutation evicts
the tile in one tick.
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>
The intake purge endpoint, the parent medication PUT, the parent DELETE
and the CSV-import POST each hand-rolled the same `findUnique +
userId !== id ? 404` block instead of using the shared
`assertMedicationOwnership` helper that every other
`src/app/api/medications/[id]/**` handler already uses. The hand-rolled
checks read slightly different fields and skipped the helper's narrow
`select`, which leaked extra column data into routes that never needed
it. Routing each of the four through the helper keeps the 404 leak
shape on a single line, prepares the purge endpoint for promotion to a
user-discoverable surface on the new detail page, and removes one of
the open audit findings against the medications API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous /me payload carried a gravatarUrl built from
SHA-256(email) and pointed every authenticated page-load at
www.gravatar.com — Automattic could correlate the digest against
its known-email table on each request. This release moves the
avatar in-house: the user uploads through POST /api/user/avatar
(multipart, JPEG/PNG/WebP, 2 MiB / 2048×2048 max), the bytes ride
on the User row alongside a content-type and updatedAt timestamp,
and GET /api/user/avatar/{id} serves them from same-origin with
the cache-busting timestamp on the profile response.
The image-header parsing is hand-rolled in src/lib/avatar.ts so
the standalone image does not gain a native dep just to read width
and height. Validation rejects oversize / wrong content-type at
the multipart parse boundary; DELETE clears the row. The bytes
ride pg_dump alongside the rest of the row so the existing weekly
encrypted backup path already covers them.
The sidebar, the coach panel, the use-auth hook, and the disable-
coach card stop reading gravatarUrl; src/lib/gravatar.ts retires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /api/dashboard/summary endpoint returned per-metric `title` and
`unit` as translated German strings ("Gewicht", "Schritte", …), which
injected DE into clients with an English UI. The wire now carries i18n
keys (`titleKey` / `unitKey`) under a new `dashboard.metric.*`
namespace; the PWA's `useTranslations()` and the iOS client's bundled
`Localizable.xcstrings` each resolve against their own locale bundle.
Locale parity preserved across de, en, es, fr, it, pl (DE / EN native,
the rest matched against the existing measurement-type labels). The
locale narrowing in `buildContext` is widened to accept every shipped
locale rather than dropping fr / es / it / pl back to the default — the
greeting + streak label now resolve in the user's configured language.
The OpenAPI yaml drift was a pre-existing 1.5.3 → 1.5.4 version-string
gap; regenerating picked it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors /api/dashboard/widgets for the insights surface: GET returns
the user's saved layout (defaulting to project order when unset),
PUT writes the layout, the response is the canonical shape the
clients can render directly. Closes the cross-device drift where
insights ordering lived in iOS local state only.
Storage is a new `insights_layout_json` JSONB column on `users`,
parallel to `dashboard_widgets_json` so each surface's resolver owns
its tile-id allowlist. Migration 0083 adds the column idempotently;
default NULL resolves to `DEFAULT_INSIGHTS_LAYOUT`. PUT routes
through the shared multi-issue 422 envelope + payload-redaction +
audit-row dedup pattern the dashboard-widgets route established.
A 5-minute per-user cache mirrors the dashboard-widgets cache and
busts on every save / reset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The iOS app surfaces these as series-capable, but the kind enum on
/api/measurements/series did not accept them — opening the detail
or trend view returned 422. The underlying MeasurementType enum
already carries the values; this commit extends the route-side
enum so the wire shape matches the data shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 365-day cap on /api/measurements/series rejected the iOS app's
"Alle"-range request (days = 3650) with a 422, painting an error
banner on every metric tile. Ten years matches the recurrence
engine's hard cap and the medication course-window upper bound.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds RESPIRATORY_RATE, BODY_MASS_INDEX, and the four remaining
deferred quantity types to MeasurementType plus the Apple Health
mapping. The batch route already returns 200 with a per-entry skip
for unmappable identifiers; iOS reads that as success and advances
its sync anchor, so any sample with one of these identifiers used
to be dropped forever. The mapping now accepts them and the rows
land.
The walking-asymmetry and walking-double-support percent values
adopt the project's server-side scaling convention (raw HK reading
on the wire, server applies the ×100 at ingest), matching the
walking-steadiness path. iOS releases that still send the
pre-multiplied percent need a one-release shim and are tracked in
the iOS coord notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes issue 217. The isPublicUrl input-time guard catches every
RFC1918 / CGNAT / loopback / link-local alt-notation, but a low-TTL
DNS record can flip between accept and dispatch. The new
pinnedPublicDispatcher resolves the hostname literally inside
undici.Agent's connect hook, refuses any address isPublicIp would
reject, and pins the connection to the first valid public address.
The five outbound paths that accept a user-supplied host route
through the dispatcher via safeFetch's requirePublicHost flag —
moodlog sync, moodlog reverse-push, the moodlog credentials probe,
the local AI client (already gated in commit 1), and the ntfy
sender. The flag now buys both the input-time check and the
connect-time pin in one call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes issue 218. Flips the convention from "remember to opt in to
redirect: manual" to "remember to opt out". Every outbound fetch that
doesn't pin a hard-coded host now routes through safeFetch and inherits
AbortSignal.timeout(15_000) so a tar-pit upstream can no longer pin a
worker indefinitely. The local AI client also gates on
requirePublicHost: true since its host is user-supplied; operators
running self-hosted Ollama / LM Studio on an RFC1918 address keep their
escape hatch via ALLOW_LOCAL_AI_PRIVATE_HOSTS=true (also enforced at
write-time in /api/user/ai-provider).
14 outbound call sites migrated. The four that already pinned manual
redirect (the historic templates) keep their semantics; the wrapper
preserves an explicit init.redirect. Adds undici as an explicit
dependency so the connect-time pin landing alongside this can rely on
the documented API rather than a transitive hop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both are Dependabot bumps that have been waiting on the queue; landing
them directly so the lock files stay coherent with the rest of v1.5.5.
knip: minor; docker/metadata-action: clean major (workflow change is
exactly the @v5 → @v6 string in two places).
Closes#209Closes#183
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 140 px slot matched the mini-chart's internal painting area but
not the mini-card shell's header + padding above it. The roughly
34 px the shell adds overflowed the slot and pushed the chart
envelope down into the TrendAnnotation row below — visible as text
sitting on top of the chart. Raise the wrapper to 180 px so the
full envelope (header + chart band) lives inside the slot and the
three tiles share one annotation baseline.
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>
The legacy CreationWizard and the flat MedicationForm retire in
favour of MedicationWizardDialog. /medications/new keeps working
through a redirect that opens the dialog from the list page; the
"Bearbeiten" action on a card opens the same dialog with the
hydrated payload. The PhaseConfigDialog drops along with the flat
form — it was only reachable from the kebab menu and stays out of
the wizard's surface; a future release can resurface the per-phase
configuration once the wizard grows compose-mode for multi-schedule
medications.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Populates messages/{de,en}.json with every key the new dialog reads
under medications.wizard.*, plus categoryDiabetes and
categoryAntibiotic on the cards' badge namespace; the other four
locales ship the English fallback per project convention. The
retired medications.create.wizard.* namespace is removed from all
six files so the locale-integrity guard surfaces every dangling key
the moment the old surface stops being reachable.
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>