Commit Graph
918 Commits
Author SHA1 Message Date
dependabot[bot] b06205851b chore(deps-dev): Bump @types/node from 25.8.0 to 25.9.1 (#226)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.8.0 to 25.9.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:06:44 +00:00
dependabot[bot] 383eef8dc0 chore(deps-dev): Bump pg from 8.20.0 to 8.21.0 (#229)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.20.0 to 8.21.0.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.21.0/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:02:16 +00:00
dependabot[bot] ef534d5c32 chore(deps): Bump lucide-react from 1.16.0 to 1.17.0 (#228)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:02:02 +00:00
dependabot[bot] 869c96968e chore(deps-dev): Bump @testcontainers/postgresql from 11.14.0 to 12.0.1 (#224)
Bumps [@testcontainers/postgresql](https://github.com/testcontainers/testcontainers-node) from 11.14.0 to 12.0.1.
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v11.14.0...v12.0.1)

---
updated-dependencies:
- dependency-name: "@testcontainers/postgresql"
  dependency-version: 12.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:00:51 +00:00
dependabot[bot] c2c1e8ef3e chore(deps): Bump @hookform/resolvers from 5.2.2 to 5.4.0 (#223)
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.2.2 to 5.4.0.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.2.2...v5.4.0)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 15:59:08 +00:00
dependabot[bot] 62e001af17 chore(deps-dev): Bump shadcn from 4.7.0 to 4.9.0 (#225)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.7.0 to 4.9.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.9.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 15:57:59 +00:00
dependabot[bot] 9a9e5b77b6 chore(deps): Bump docker/setup-buildx-action from 3 to 4 (#220)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 15:54:39 +00:00
Marc-André BombeckandClaude Opus 4.8 05fdddabc5 chore(release): v1.7.1 — overview-card edit/history/advanced actions (incl. GLP-1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.7.1
2026-05-31 16:10:03 +02:00
Marc-André BombeckandClaude Opus 4.8 50e0a65228 chore(release): v1.7.0
v1.7.0 — health-record export (PDF + FHIR), flexible medication schedules, first-paint dashboard snapshot, full HealthKit coverage, offline sync delta feed, Coach data clustering. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.7.0
2026-05-31 14:36:17 +02:00
Marc-André BombeckandClaude Opus 4.7 33b912babd test(settings): exempt the hidden avatar file input from the 44px touch-target sweep
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>
2026-05-30 00:44:40 +02:00
Marc-André BombeckandClaude Opus 4.7 ac1d03f8ea test(meds): green the wizard e2e locale and one-shot timezone fixtures
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>
v1.6.0
2026-05-30 00:30:27 +02:00
Marc-André BombeckandClaude Opus 4.7 8ee7aaac44 chore(release): v1.6.0
Medication editor overhaul, route of administration, today-tile read-flip.

- Route-of-administration column (ORAL | INJECTION | OTHER) decoupled from
  treatmentClass; injection-site picker surfaces for any injection.
  Migration 0088 backfills ORAL via a constant column default.
- One-time injection (oneShot + deliveryForm = INJECTION) editor path.
- Profile-photo upload card on Settings → Account.
- Today-tile read-flip: /api/medications/intake?scope=today and
  /api/dashboard/summary gate through the canonical recurrence engine,
  matching the reminder worker for bi-weekly, rolling, RRULE, and one-time
  cadences.
- Measurement note cap 25 → 200; dashboard range colors on semantic tokens;
  admin sidebar icons + shared-helper consolidation; API-tokens multi-language
  snippets restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:10:12 +02:00
Marc-André Bombeck d01d1d1dc9 Update README.md 2026-05-29 22:11:05 +02:00
Marc-André Bombeck 189d162dff Update README.md 2026-05-29 22:08:53 +02:00
Marc-André Bombeck edfe43d391 Update README.md 2026-05-29 22:08:35 +02:00
Marc-André BombeckandClaude Opus 4.7 8528c3b4fe chore(release): v1.5.6
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>
v1.5.6
2026-05-29 22:07:10 +02:00
Marc-André BombeckandClaude Opus 4.7 d54addd61a chore(release): v1.5.5
Medication detail page, iOS coord wave, outbound-fetch hardening.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.5.5
2026-05-29 00:55:40 +02:00
Marc-André BombeckandClaude Opus 4.7 353c8d7fa0 feat(meds): row-level edit and bulk-delete on the intake history preview
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 167cc3afa2 feat(meds): rate-limit the bulk-delete intake endpoint
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 ff334ff10e fix(meds): phase-config upsert builds the Prisma payload field-by-field
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 e6907d4ea9 fix(meds): phase-config returns the 422 multi-issue envelope
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 56523bafb1 chore(meds): centralise the medication api-token query key
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 9532f14f3b fix(meds): notification section title id no longer duplicates the section heading
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 359b103e90 fix(meds): glp1 GET routes through assertMedicationOwnership
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 043c335ba3 fix(meds): purge route invalidates the user-medication caches
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 8adf1b55e8 fix(meds): grace-row save lands on the primary schedule
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>
2026-05-29 00:53:23 +02:00
Marc-André BombeckandClaude Opus 4.7 8c740fd146 feat(meds): list-card history icon routes to the new detail page + cascade pin tests
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>
2026-05-28 23:54:07 +02:00
Marc-André BombeckandClaude Opus 4.7 4ae8801c56 feat(meds): self-hosted medication detail page restoring every v1.5.4-displaced action
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>
2026-05-28 23:51:17 +02:00
Marc-André BombeckandClaude Opus 4.7 7f95a2a44f fix(measurements): wire walking step length and walking speed
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>
2026-05-28 23:38:21 +02:00
Marc-André BombeckandClaude Opus 4.7 8d5d039ed1 feat(meds): bulk-delete intake endpoint for the detail-page preview multi-select
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>
2026-05-28 23:36:56 +02:00
Marc-André BombeckandClaude Opus 4.7 ef21967d12 refactor(meds): lift IntakeImportDialog out of the list page and bundle the inline-compliance prefix
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>
2026-05-28 23:34:08 +02:00
Marc-André BombeckandClaude Opus 4.7 c97c4c3be3 feat(meds): wizard polish — 560 px shell, intent-aware landing, omit notifications on edit
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>
2026-05-28 23:30:35 +02:00
Marc-André BombeckandClaude Opus 4.7 af2249644b fix(meds): route every detail-page sub-route through assertMedicationOwnership
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>
2026-05-28 23:27:02 +02:00
Marc-André BombeckandClaude Opus 4.7 ca1c52ffcb feat(avatar): self-hosted avatar storage replacing the Gravatar leak
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>
2026-05-28 22:39:20 +02:00
Marc-André BombeckandClaude Opus 4.7 e93feaf8d1 feat(dashboard): emit i18n keys instead of hardcoded German strings
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>
2026-05-28 22:32:46 +02:00
Marc-André BombeckandClaude Opus 4.7 a75b96b2a9 feat(insights): server-persist insights tile order + visibility
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>
2026-05-28 22:32:40 +02:00
Marc-André BombeckandClaude Opus 4.7 19cfd69ff1 feat(measurements): accept restingHeartRate, hrv, vo2max in the series enum
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>
2026-05-28 22:32:18 +02:00
Marc-André BombeckandClaude Opus 4.7 26f13c5064 feat(measurements): raise the series days cap to ten years
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>
2026-05-28 22:32:18 +02:00
Marc-André BombeckandClaude Opus 4.7 befac0ec65 fix(measurements): wire the six deferred Apple Health identifiers
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>
2026-05-28 22:32:18 +02:00
Marc-André BombeckandClaude Opus 4.7 b43b57f657 feat(security): pin resolved IP on outbound fetches to defeat DNS rebinding
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>
2026-05-28 22:32:11 +02:00
Marc-André BombeckandClaude Opus 4.7 425503e095 feat(security): safeFetch wrapper with manual redirect and timeout defaults
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>
2026-05-28 22:32:11 +02:00
Marc-André BombeckandClaude Opus 4.7 bf6dfb8f6d chore(deps): bump knip 6.14.1 → 6.14.2 and docker/metadata-action v5 → v6
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 #209
Closes #183

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:00:19 +02:00
Marc-André BombeckandClaude Opus 4.7 6e204f1a25 fix(insights): raise the trends-row chart-slot wrapper so the annotation no longer overlaps
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>
2026-05-28 21:42:20 +02:00
Marc-André BombeckandClaude Opus 4.7 3b7c72e1d0 chore(release): v1.5.4
Medication wizard: modal dialog, compose-mode, ten-bucket taxonomy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.5.4
2026-05-28 19:55:55 +02:00
Marc-André BombeckandClaude Opus 4.7 dba844235c test(meds): coverage for compose-mode
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>
2026-05-28 19:52:39 +02:00
Marc-André BombeckandClaude Opus 4.7 8578924f14 feat(meds): compose-mode for the modal wizard
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>
2026-05-28 19:52:39 +02:00
Marc-André BombeckandClaude Opus 4.7 320970c97d test(meds): coverage for the modal wizard
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>
2026-05-28 19:33:11 +02:00
Marc-André BombeckandClaude Opus 4.7 105fcee242 refactor(meds): route /medications/new and the edit trigger through the new dialog
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>
2026-05-28 19:33:11 +02:00
Marc-André BombeckandClaude Opus 4.7 e686083bae i18n(meds): localise the modal-wizard surface
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>
2026-05-28 19:33:11 +02:00
Marc-André BombeckandClaude Opus 4.7 b1f9a4bf8f feat(meds): step content for the modal wizard
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>
2026-05-28 19:33:11 +02:00