Release v1.7.0 — health-record export, flexible schedules, first-paint snapshot, full HealthKit coverage - #219
Merged
Merged
Conversation
Add the EN keys for the v1.7.0 medication-detail rework — three-button header labels, the full-history view (back link, subtitle, planned suffix, drug-level disclosure) and the four advanced-settings group headings plus the real CSV/JSON import block — and propagate them to de/es/fr/it/pl. Drop the orphaned CSV-stub keys and the now-unused intake "view full history" link copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detail header gains a three-button row — Edit (pencil, labelled), History (clock, ghost) and Advanced (sliders, ghost). The History button routes straight to the full intake-history view; the redundant "view full history" footer link on the intake preview is removed. All buttons keep a 44px touch target on mobile. The history view is now intake-history only, defaulting to scheduledFor-desc so the order reads today -> yesterday -> … and skipped rows no longer float to the top; the date column falls back to the scheduled time with a muted "(planned)" marker. The side-effect, schedule and titration sections drop off the default surface; the estimated drug-level curve stays as a default-closed disclosure for GLP-1. Import is present but de-emphasised. Extract the shared editable table + edit/delete/bulk machine into <IntakeHistoryEditable> so the preview and the full view stay identical. The advanced-settings sheet widens to the 2xl token and regroups into four labelled sections — Data (real CSV/JSON import + API endpoint), Reminders (notifications + grace), Lifecycle (pause/end/phases) and Danger zone (purge/delete) — with consistent outline / destructive button styling. The CSV import is promoted from a stub to a real button. Split the section bodies out of their card wrappers so the groups can compose them directly. Pin NULLS LAST on the takenAt sort in the intake list route so the skipped-rows-first ordering is correct regardless of the client sort key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…page The active-chip positioner used Element.scrollIntoView(), which walks every scrollable ancestor and adjusts both axes. On mobile this nudged the whole document vertically on each route change, so tapping a settings sub-page fired a dizzy page-level auto-scroll. Scroll the strip's own scrollLeft instead, confining the motion to its horizontal axis. Reduced-motion handling is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add fullName + insurerName (plaintext) and insuranceNumberEncrypted (AES-256-GCM) to the User model (migration 0093), validate the German KVNR mod-10 check digit, and surface all three optional fields through the profile update path and the /api/auth/me + /api/user/profile reads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rofile Use object-cover on the avatar image so a non-square upload is centre-cropped instead of stretched. Add the optional full-name, insurer, and insurance-number inputs to the profile form, and land the i18n keys for the profile identity block, the enhanced report, and the health-record export across all six locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build a Bundle of type document (leading Composition + Patient + Observations + MedicationStatements) from the same aggregator the PDF consumes, so both describe identical numbers. Map measurement types to LOINC/UCUM, emit a blood-pressure panel with sys/dia components, keep mood opt-in, and carry the KVNR as a Patient identifier. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Print the new identity fields on the cover, add a deterministic clinical-summary block (reading counts, per-vital trend arrows, weighted adherence), embed jsPDF-native trend sparklines (no native canvas module), set document metadata for a PDF/A-leaning file, and add an opt-in, clearly-labelled AI-summary section that is off by default and never part of the structured export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add POST /api/export/health-record (apiHandler, requireAuth, shared export rate bucket, audit health-record.export) driven by a strict Zod selection schema with no userId field and multi-issue 422s. Emit PDF, FHIR JSON, or a fflate zip package bundling both plus a README. Wire the grouped selection panel into Settings and document the route in the OpenAPI registry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add GET /api/dashboard/snapshot — one apiHandler-wrapped, requireAuth
round-trip that assembles every above-the-fold tile field via a single
Promise.all over the existing rollup / mood / widget helpers plus a
read-only lift of the pre-generated daily briefing.
Two-phase shape: tiles (slim summaries + mood + resolved layout + user
profile) always arrive together; extras (BD-in-target + per-context
glucose) ride the slowest reads and return null on a rollup-coverage
miss so the strip never waits on the live-SQL fallback. Coverage is
probed once up front and the thick phase only runs when warm.
The briefing is lifted read-only from User.insightsCachedText and
reports ready / preparing / disabled — no provider chain is reachable
from the builder. Body read-through caches.analytics keyed
${userId}|dashboard-snapshot, 60s TTL; per-sub-query timings surface
under meta.snapshot.sub_*_ms. OpenAPI .meta() registered under a new
Dashboard tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…validators
Add queryKeys.dashboardSnapshot() to the centralised factory and a
dashboardSnapshotCacheKey helper so the cache key stays single-source.
Extend the per-user invalidators so a write drops the snapshot: the
measurement / mood / medication sweeps over the ${userId}| analytics
prefix already cover it, and the widget + insight paths — which do not
touch the analytics bucket — call invalidateUserDashboardSnapshot
explicitly. A fresh comprehensive-insight write now evicts the snapshot
so the next read re-embeds the new briefing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the insight-pregenerate pg-boss queue (registered in allQueues and scheduled 04:30 Europe/Berlin in reminder-worker) so the comprehensive insight — which carries the daily briefing — is pre-generated overnight instead of lazily on the first /insights mount after the 24h cache expires. Extract the generation pipeline into comprehensive-generate so the route and the cron share one path. Budget-gated: the master assistant kill-switch short-circuits the run, a per-user rate-limit bucket (1 / 20h, distinct from the route's bucket) caps re-generation, and a per-run batch cap bounds the fan-out. The discovery query selects only coach-enabled, stale-or-missing-cache users, oldest-first. Provider-less accounts cost one cheap chain-resolve and no LLM call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add useDashboardSnapshot (one un-gated client cell — server requireAuth is the real gate, so firing it without waiting on isAuthenticated kills the /api/auth/me round-trip from the cold critical path) and a build-time NEXT_PUBLIC_DASHBOARD_SNAPSHOT flag. Flag OFF (default) keeps the legacy slim / thick / mood / widget cells byte-identical to today. Flag ON hydrates the same downstream data / moodData / layoutData shapes from the single snapshot cell so no tile rendering changes; the legacy cells go enabled:false. extras null on a coverage miss leaves the BD-Zielbereich + glucose fields undefined so those tiles shimmer while the rest of the strip paints together. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the ten-cluster data taxonomy (cardio, body composition, activity, workouts, sleep, mood, glucose, medication, mobility & gait, environment) and the cluster->source expansion + degradation-priority helpers. Extend CoachScopeSource + the provenance metric union to cover every clustered metric. Persist selection in the existing User.coachPrefsJson as an optional dataClusters array (no migration). undefined is the back-compat sentinel: the snapshot builder expands DEFAULT_COACH_CLUSTERS (cardio + body + mood + medication, reproducing the legacy five domains) when the key is absent; an explicit empty array means everything-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t cap Expand the snapshot's source default from the hardcoded five to the user's resolved clusters. Map and emit every clustered metric: the full body-composition set, cardio composition/vascular series, activity, mobility & gait, environmental exposure, per-context glucose, per-night sleep with stage minutes, and the Workout model (capped recent list + per-sport rollup, never raw rows). Add the assembled-snapshot soft char cap (~24k) with progressive degradation by reverse cluster priority — drop per-day detail, then weekly buckets, then replace the block with a marker, lowest-signal clusters first, so the clinical core survives truncation. Cap the timeline window for additive clusters when many are active. Enabling every cluster at allTime now stays inside the prompt budget. New observability: coach.clusters.resolved, coach.snapshot.truncated, coach.cluster.empty_skipped. Bump PROMPT_VERSION to 4.26.0 and teach the system prompt that the snapshot is additive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat opt-out framing with clustered opt-in toggles in the Coach settings sheet: each cluster row carries a label, a member-preview hint, and a switch, with the per-metric exclude list kept below as the advanced fine-tune. Persist dataClusters through the coach-prefs route and surface clusterCount on the replied + put annotations. Derive the provenance chip label from the metric token so the new taxonomy renders without a hand-listed map. Add cluster labels + hints and the ~26 new metric labels across all six locales, and regenerate the OpenAPI contract for the dataClusters field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the server FHIR document Bundle byte-identical to the iOS client's
LOINC/UCUM mapping so a Bundle from either side is interchangeable.
LOINC/UCUM corrections:
- SpO2 2708-6 -> 59408-5 (Pulse oximetry display)
- VO2max 84478-5 -> 96402-2 (Oxygen consumption maximum during exercise)
- Steps unit /d -> {steps}
- Sleep min -> h; emit value in hours (stored minutes / 60); PDF unaffected
- Body water 73704-9; bone mass 73708-0 (were local-text fallbacks)
- Resting HR display -> Heart rate --resting
- Glucose by context: random/bedtime 2339-0, fasting 1558-6,
postprandial 1521-4 (was incorrectly 2339-0)
- BMI display -> Body mass index (BMI) [Ratio]; emitted once (computed
block; stored BODY_MASS_INDEX series skipped)
New standard metrics: active energy 41981-2 kcal, walking speed 41957-2
m/s (FHIR stays m/s, no km/h), walking asymmetry 91557-1, step length
41955-6. Six HK-placeholder codes emit the HealthKit identifier string in
the LOINC slot (double support, env/headphone audio, flights, distance,
daylight).
Structural: DiagnosticReport (85353-1 vital-signs panel) as the last
entry routing every Observation into result[]; Composition sections
renamed to Vital signs + Medications.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ivery columns Migration 0091 adds the per-medication iOS reminder flags (live_activity_enabled, critical_alarm_enabled) plus a per-device medication_delivery override column. Migration 0092 adds the medication_schedule_type enum (SCHEDULED / PRN / CYCLIC) and the cyclic on/off-week columns. All columns are additive with constant defaults, so the ALTERs are non-blocking metadata operations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PRN short-circuits occurrencesBetween to zero slots and
nextOccurrenceAfter to null — as-needed schedules are loggable but
never projected, reminded, or counted in compliance expected. CYCLIC
gates whichever inner cadence (rrule / legacy / rolling / one-shot) by
an N-on / M-off week phase anchored to startsOn ?? createdAt, mirroring
the existing intervalWeeks phase math; the two phase computations stay
independent so cyclic composes with BYDAY and the legacy stride.
The projector select widens to carry the new columns; the worker reads
them through include: { schedules: true } already.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SB-SCHED-2. expandScheduleSlots now delegates to occurrencesBetween when the caller threads a medication context and the schedule carries a canonical recurrence (rrule / rolling), a non-SCHEDULED type, or the medication is one-shot. This fixes the long-standing bug where an rrule = "FREQ=WEEKLY;BYDAY=MO" schedule expanded to daily-every-day in compliance because daysOfWeek = null read as "every day"; the denominator now counts only the days the schedule actually emits. calculateCompliance gains an optional medicationContext; when absent it behaves exactly as before (the legacy weekday walker), so existing callers and fixtures stay byte-stable. Fixing one expander fixes both the compliance denominator and the detail-page cadence chart. Golden-fixture parity tests pin the denominator per cadence type: legacy parity, weekly BYDAY, bi-weekly, rolling, one-shot, PRN, cyclic, and a DST spring-forward boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the engine-routed denominator into all eight surfaces that surface an adherence rate: the per-medication compliance endpoint, the two insights routes (targets, comprehensive), the Health-Score feature extractor + fast-path, the BP-status gate, and the medication-compliance pillar. Each resolves the user timezone and the latest non-skipped takenAt (rolling cadences re-anchor on it) via shared helpers, then passes the canonical-engine context. The Health-Score fast-path select widens to carry the canonical recurrence columns. The per-medication compliance payload also gains the additive per-day `due` / `expectedCount` fields (item 5): the day's expected count is the engine's actual due-slot count, so iOS history renders a missed mark only on days the schedule expected a dose — off-weeks, non-matching weekdays, and PRN days no longer paint a false miss. `expected` stays populated for existing web consumers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted nextDueAt Create / update medication accept liveActivityEnabled + criticalAlarmEnabled (defaulted booleans, field-by-field passthrough) and the per-schedule scheduleType (SCHEDULED / PRN / CYCLIC) plus the cyclic on/off-week fields, with Zod refines: CYCLIC requires both week counts, PRN forbids a cadence. The FREQ=DAILY recurring default no longer applies to PRN. GET list + detail expose a read-only nextDueAt computed from the canonical engine's earliest nextOccurrenceAfter across schedules (reusing the list's existing last-intake groupBy to avoid an N+1; a 60 s list-cache staleness is accepted as for todayEventCount). The OpenAPI registry gains scheduleType / cyclic fields on the schedule resource and liveActivityEnabled / criticalAlarmEnabled / nextDueAt on the medication resource; the request schemas pick the new fields up from the live Zod objects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The notificationPrefs medication sub-object grows a roaming
deliveryDefault ("server" / "client"); the resolver maps
deliveryDefault === "client" onto the established clientManaged cron
gate so the reminder worker keeps reading one boolean. The device
registration POST and a new PATCH /api/auth/me/devices/[id] store a
nullable per-device medicationDelivery override (null = inherit the
roaming default); resolveDeviceDelivery resolves device-over-user-over-
"server". The override is stored + echoed only — cron suppression stays
user-level in v1.7.0 (APNs fans out to all devices and iOS dedupes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SB-SCHED-4. The reminder worker keyed phase + dedup on the single windowStart, so a schedule with timesOfDay = ["08:00","20:00"] reminded once a day instead of twice. The dispatch loop now iterates every first-class time-of-day, each with its own window (anchored at the time, spanning the legacy windowEnd - windowStart duration), phase, RED-mint instant, and dedup key. Migration 0093 adds time_of_day to the reminder-message ledger and re-keys its unique index to include it. A schedule with no first-class timesOfDay emits one slot at windowStart with time_of_day = "", which dedupes against pre-v1.7 rows (backfilled to "") byte-for-byte, so single-window meds keep their exact prior behaviour. The client-managed suppression check wraps each per-slot dispatch unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The widgets PUT rejected the entire layout blob with a 422 when any widget id was unknown, surfacing the "Layout konnte nicht gespeichert werden" toast whenever iOS shipped a tile ahead of a server release. Filter unknown ids out before Zod — mirroring the read-side tolerance that already drops unknown ids on GET — so the server persists only ids it knows while iOS keeps its own local layout. The enum still validates the surviving ids, so a genuinely malformed entry (missing order, etc.) still 422s. Dropped ids are surfaced via a dashboard.widgets.unknown-id-dropped annotation so the drift is greppable without a 422 storm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0093 is taken by the profile-identity migration; renumber the reminder-message time-of-day migration so the sequence is unambiguous. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a per-type display transform keyed by MeasurementType + unit preference. Storage stays canonical SI; the transform applies only at the render boundary. WALKING_SPEED renders km/h (factor 3.6) and WALKING_RUNNING_DISTANCE renders km (factor 0.001) on the metric preference, with additive mph/mi imperial branches. HealthChart gains an optional valueScale prop (default 1) folded into the raw value at the single read boundary so bucketing, y-domain, baseline, trend, and tooltip all scale uniformly. Default 1 keeps every existing chart byte-identical; Recharts is untouched. The chart cache key trails with valueScale so a scaled chart never reads an unscaled sibling out of the cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… metrics Add HIGH_FREQUENCY_MEAN_TYPES (respiratory rate, walking speed/step length, environmental + headphone audio exposure) and a reducer-aware daily-mean drain modelled on the proven cumulative drain. Per user × type × completed day it UPSERTs one stats:<HK>:<day> row carrying the mean and soft-deletes the per-sample rows (tombstone, audit-trail preserving). Scoped to source=APPLE_HEALTH so manual + Withings spot rows survive; 36-hour grace window keeps today's in-flight syncs raw. The set is strictly disjoint from CUMULATIVE_HK_TYPES (asserted in a test) so SUM and MEAN never collide on a type. PULSE is deliberately excluded — correlation/scatter read raw PULSE rows; its display stays daily-averaged via the read-path AVG. Register the mean-consolidation pg-boss queue in allQueues with a boot-time converging discovery + per-user handler, mirroring the step-consolidation pattern. Source-grep guard asserts the registration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-user unit_preference column (additive migration 0094, idempotent guard) mirroring the glucose_unit single-purpose display precedent — there is no general settings JSON bag on users, so a dedicated scalar is the right shape over abusing a domain-specific JSON column. Default metric; canonical measurement storage stays SI. Surface the preference on /api/auth/me and a new PATCH /api/auth/me/unit-preference (Zod enum, 422 on bad input, 60/min rate limit, audit row). The display-transform registry reads this preference to pick the km/h-vs-mph and km-vs-mi branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire a dedicated insights sub-page for all 24 ingested-but-unplotted MeasurementType values (blood glucose, body-composition tail, audio exposure, mobility/gait, respiratory rate, daylight, cardiovascular risk markers). Each reuses the generic HealthKitMetricPage scaffold — no new chart code; the existing daily-aggregating read path serves them — plus a per-metric InsightMetric member, ChartOverlayKey slot, sub-page slug, tab-strip pill, and six-locale page copy. The new metric clusters (body, activity, cardiovascular, hearing, environment, metabolic) each collapse behind a parent pill so the strip stays scannable. WALKING_SPEED renders km/h (valueScale 3.6) and WALKING_RUNNING_DISTANCE renders km (valueScale 0.001) via the display-transform — raw storage stays SI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real-Postgres test pins the drain contracts the unit mocks cannot: per-sample rows collapse into one daily MEAN, originals soft-deleted, manual/Withings rows untouched, PULSE never drained, idempotent re-run. Confirms migration 0094 applies cleanly in sequence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both `use-dashboard-snapshot` and `use-analytics-query` hard-coded the same `120_000` refetch interval. Extract a single named `DASHBOARD_REFETCH_INTERVAL_MS` so the cadence has one source of truth the server-side snapshot cache TTL can also key off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dashboard-snapshot entry shared the analytics bucket's 60 s TTL
while the client polls every 120 s, so every scheduled refetch was a
guaranteed cache miss that re-ran the full builder. Add an optional
per-key TTL to the server cache and give the snapshot key a TTL longer
than the refetch interval, so an idle-tab poll lands on a warm entry.
The key still rides the analytics bucket, so the `${userId}|` eviction
sweep on a measurement / mood / medication write evicts it unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The doc claimed the insight-pregenerate cron calls invalidateUserInsights directly. It does not — the cron evicts through generateComprehensiveInsight, which invalidates after its cache write. The /api/insights/generate POST remains a direct caller (its own pipeline). Reword to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /api/measurements/[id] and the PUT existence check used findUnique without a deletedAt filter, so a soft-deleted (tombstoned) row was still returned in full on a direct GET and could be resurrect-edited via PUT. Switch both to findFirst with deletedAt: null so a tombstoned measurement 404s, matching the list / analytics / rollup read invariant. Pin the contract with a real-Postgres GET/PUT 404 case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/api/sync/changes keyset-walks measurements filtered on user_id ordered by (updated_at asc, id asc); the existing indexes cover (user_id, type, measured_at) and (external_id) only, so Postgres sorted the user's whole measurement set per page. The route allows 120 pulls/min/user, so a multi-device offline drain on a heavy Apple-Health tenant is per-page full-sort pressure on the shared pool. Add @@index([userId, updatedAt, id]) and ship the additive migration 0096 (plain CREATE INDEX IF NOT EXISTS, no backfill). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The insurance number (German statutory `kvnr`) is encrypted at rest and absent from every current wide-event excerpt, but the field name belongs on the central redaction denylists as defence-in-depth. Add insurance / insurer / kvnr to SENSITIVE_KEY_PATTERNS so a body routed through redactSensitiveFields redacts the value, and extend the query-string scrub in redactSecrets so a stray ?kvnr= / ?insuranceNumber= leak is caught at the egress boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unknown-id filter runs over the full incoming widgets array before Zod's .max(20) applies, so a single large request could push thousands of strings into one wide-event line. Slice the logged dropped_ids to 20 entries and keep the full dropped_count. The persisted layout was already bounded; only the log line was unbounded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveCanonicalUnit issued a findFirst per day with no deletedAt filter, so it could pick up a tombstoned row's unit (harmless today since units are homogeneous per type, but an unnecessary query that reads from soft-deleted rows). Carry unit in the PerSampleRow select and read dayRows[0].unit instead; drop the helper. The scan already filters deletedAt: null, so the unit can never come from a tombstone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion window The tombstone-cleanup job prunes on deletedAt and the changes feed gates cursorExpired on updatedAt, but both derive from TOMBSTONE_RETENTION_DAYS. Pin that a row whose deletedAt predates the window is pruned while a recent tombstone survives and stays reachable by a fresh cursor, so a deletion can't be pruned inside a client's reachable cursor window. The assertion trips if either horizon is re-keyed away from the shared retention constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…check The single-resource PUT existence check moved from findUnique to findFirst (deletedAt: null filter); update the duplicate-timestamp test's prisma mock to match so the 409 / 200 / re-throw cases keep exercising the existence lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unit-preference segmented control used min-h-9 (36px) with no mobile bump, breaking the release-wide min-h-11 sm:min-h-9 touch-target rule. The hand-rolled radio buttons also carried no visible focus indicator on their bg-muted track. Bump to 44px on mobile and add the shadcn focus-visible ring so keyboard focus is visible (WCAG 2.4.7). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Medikamente" toggle was bound only to the compliance flag, so the medication list shipped unconditionally even after unchecking it. Bind the single control to both medList and compliance so unchecking it excludes all medication data, matching the iOS export's five-section model (vitals/charts/medications/adherence/mood). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The coach metric label used "Doppelstützzeit" while the chart page used "Doppelstandphase" for the same HealthKit metric, so a user comparing the two surfaces saw a mismatch. Align the coach label to the chart page's clinically-standard "Doppelstandphase". German-only; the other locales were already internally consistent for this metric. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsolidation drains The cumulative, daily-mean, and legacy-step passes each re-implemented the same per-user-day skeleton: load the user set, resolve each user's timezone, scan live source rows inside an optional grace window, bucket them by local calendar day, reduce each day, then mint one canonical daily stats row and drop the source rows. They duplicated the bucketing helper, the user-load query, the cutoff calculation, and a separate 36-hour grace constant. Pull the shared machinery into consolidation-base.ts and parameterise it on the divergent parts: the type set, the reducer (sum vs mean), the grace window, the source-scope filter, the canonical-unit resolution, the mint source, the per-day write strategy (hard vs soft delete), and the optional existing-total skip + unique-constraint step-over. Each drain is now a thin call into runConsolidation that keeps its own public summary shape and log lines. No behaviour change: the cumulative drain stays sum + hard-delete over every source, the daily-mean drain stays mean + soft-delete + APPLE_HEALTH scope + PULSE-excluded, the legacy-step drain stays sum + soft-delete + existing-total fold + P2002 conflict skip. The shared grace constant seeds both DRAIN_CUMULATIVE_CUTOFF_HOURS and MEAN_CONSOLIDATION_CUTOFF_HOURS so they track in lockstep. All existing unit and testcontainers integration tests pass unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hand-rolled radiogroups built from role="radio" buttons were each an independent tab stop with no arrow-key handling, so keyboard users had to Tab through every option and could not move selection with the arrow keys an ARIA radiogroup is expected to honour. Add a shared useRovingRadioGroup hook: the group becomes a single tab stop (selected option tabindex=0, the rest tabindex=-1), Left/Up and Right/Down move and wrap selection, Home/End jump to the first/last enabled option, and focus follows the selection. Disabled options are skipped. Mouse and touch behaviour is unchanged; the hook only layers the keyboard interaction and per-item tabindex/ref wiring on top of the existing onClick. Wire it into the unit-preference segmented control, the health-record export format buttons, the mood-level pickers (entry + edit), and the side-effect entry/severity chip groups. The component test harness is SSR-only, so the navigation math is extracted into a pure rovingRadioNextIndex function and unit-tested directly (arrows, wrap, Home/End, disabled-skip, empty group); the tabindex/aria render shape is pinned in the unit-preference SSR contract test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alogue
Add two additive blocks to the dashboard snapshot so the native client can
adopt it as a cold-launch seed:
- metricStates: latest {value, measuredAt, unit} per chartable metric, keyed
by the iOS MetricKind raw value (including the non-obvious raws such as
oxygenSaturation, totalBodyWater, heartRateVariability, bodyMassIndex,
walkingAsymmetryPercentage, walkingDoubleSupportPercentage,
environmentalAudioExposure, headphoneAudioExposure, activeEnergyBurned).
Derived in-process from the slim summaries slice already fetched for the
tile strip, so it adds no query against the shared Prisma pool.
- layoutCatalogue: the full 27-id widget catalogue (16 server-known + 11
iOS-only) with per-widget visibility + order, so the layout round-trips in
one key. Server-known ids inherit the resolved layout; iOS-only ids append
default-invisible. The writable widgets PUT enum is untouched.
Both blocks are additive — the web-consumed fields keep their exact shape.
OpenAPI .meta() updated for the new response fields.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mig 0097) Extend the offline-sync reconciliation columns the measurement feed already carries to MoodEntry and MedicationIntakeEvent so the `/api/sync/changes` feed can cover both domains: - MoodEntry gains `sync_version` (LWW counter, default 0) and `deleted_at` (soft-delete tombstone). It already had `updated_at`. - MedicationIntakeEvent gains `updated_at` (it had none — the feed orders on it), `sync_version`, and `deleted_at`. An intake is an immutable fact, so its sync_version only bumps on the soft-delete write; a correction is tombstone + re-insert. Each model gains a `(user_id, updated_at, id)` keyset index matching the measurement feed's index from migration 0096. Every add is additive and order-safe: sync_version defaults to a constant 0, deleted_at is nullable, and the intake `updated_at` add carries a `DEFAULT now()` so existing rows backfill without a table rewrite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…from reads Convert the user-facing single-row DELETE on mood entries and medication intake events from a hard `delete` to a `deletedAt` write (+ bump `syncVersion`), mirroring the measurement soft-delete. A tombstoned row stays in place so the `/api/sync/changes` feed can surface the deletion to paired clients that were offline at delete time, keyed on the server `id`. Read-side: add `deletedAt: null` to every list / detail / today / compliance / analytics / insights / coach / export / rollup read on both domains so tombstoned rows never surface in normal reads. The rollup aggregates (mood + medication-compliance), their not-exists prune, the coverage probes, and the boot-backfill discovery queries all gain the filter so the cached and live tiers agree. Write-path probes that guard the `(userId, medicationId, scheduledFor, source)` unique slot (today-backfill existence check, the RED missed-dose count) intentionally keep counting tombstoned rows to avoid a P2002 collision; this is noted inline at each site. Mutations bump `syncVersion`: mood update + soft-delete, intake status toggle + per-event edit + soft-delete. Mood update/PUT and the intake edit/status-toggle also refuse to resurrect a tombstoned row (the lookup gains a `deletedAt: null` guard → 404). The one-shot lifecycle reconcile treats a tombstoned intake as no-longer-logged, so deleting the single dose reactivates the medication. Reminder + mood-nudge suppression no longer count a deleted row as a logged action. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… cursor Extend the delta feed beyond measurements. The opaque cursor becomes a multi-domain keyset: it carries an independent `(updatedAt, id)` watermark per domain (measurements / mood / intakes) so each table walks at its own rate without one domain's high-water mark starving another. The token stays fully opaque — `decodeCursor` returns null on garbage or a wrong-version envelope, so a stale or old-format cursor re-inits cleanly (no live consumer cursors exist yet, so the format change is free). Each domain block carries `upserts` + `tombstones`; tombstone identity follows the iOS §7.3 table — measurements key on `externalId`, mood + intakes on the server `id`. Tombstones precede upserts within each domain. `syncVersion` is echoed per row. `hasMore` is true when any served domain still has rows past its page; `cursorExpired` fires when any domain watermark predates the shared retention horizon. `/api/sync/state` now reports the mood + intake summaries (same lastUpdatedAt / liveCount / tombstonedCount shape as measurements). The tombstone-cleanup job prunes mood + intake tombstones on the same retention horizon as measurements. The OpenAPI registry gains the mood + intake upsert/tombstone schemas and the multi-domain response/state shapes (yaml regen batched centrally). Integration coverage (testcontainers): mood + intake delta pagination, tombstone surfacing after soft-delete (and absence from the list read), multi-domain single-page round-trip, hasMore across domains, and the garbage-cursor clean-init path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widen the widgets layout to persist and return all 27 widget ids (16 web-known + 11 iOS-only) so the native client can drop its local merge workarounds and seed its layout in one key. - PUT enum widens to the 27-id catalogue; widgets cap raised 20 -> 30. Accept-and-ignore now drops only ids outside the catalogue (a retired tile or typo), capped + annotated; the 11 iOS-only ids survive and persist. Malformed surviving entries still 422. - resolveDashboardLayout / serializeDashboardLayout retain the iOS-only ids in the stored and returned layout. The default layout stays the 16 web tiles; iOS-only ids appear only once a client sends them and are never auto-seeded for a web-only account. - Web render is robust to iOS-only ids: the dashboard looks up only its own 16 ids and skips the rest; the Settings tile list skips any id with no web component instead of painting a dead row; the first-paint skeleton counts web tiles only. - GET returns the full persisted layout, up to 27 ids. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the status, tech-stack, and roadmap version references from v1.5 to the current v1.7 line, and bump the local-dev Node prerequisite to 22 to match the Dockerfile base image. Add the headline v1.7.0 capabilities: health-record export (PDF + HL7 FHIR R4), the full recurrence-engine medication schedules with cadence-canonical compliance, route of administration, full HealthKit metric coverage with a metric/imperial display preference, the first-paint dashboard snapshot with nightly briefing pre-generation, Coach data clustering, and the offline sync delta feed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MOOD_REMINDER delivery was gated by a double opt-in: the visible moodReminderEnabled card plus a per-event preference that defaulted off. With no preference row the dispatcher hit the no-row default-off branch and silently dropped every mood-reminder push, so a user who enabled the card still received nothing. Make the card the single source of truth. For MOOD_REMINDER, derive the no-row default from User.moodReminderEnabled, so enabling the card alone delivers. A genuine explicit per-event opt-out (a preference row with enabled=false) is still honoured; only the no-row default changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a settable local-time hour (0-23) for the daily mood reminder, replacing the hardcoded 22:00 window so iOS and web share one source of truth instead of the native client running its own schedule. The hour persists in the existing notification-prefs JSON blob under a new mood.reminderHour key (no migration). Default 22 keeps an unset value identical to today. Expose it on GET/PATCH of the prefs route, Zod-validated as an int 0..23, merged field-by-field. The mood-reminder cron resolves the hour per user, keeping the 15-minute tick, the already-logged-today skip, and the ledger-after-delivery idempotency. Surface an hour picker in the mood-reminder settings card wired through the prefs mutation across the six locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mood-reminder hour is now per-user, so drop the literal 22:00 from description and statusOn across all six locales in favour of the user's chosen time. Values only; keys unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A broad polishing release across medications, dashboard, export, and HealthKit.
Highlights
nextDueAt, and cadence-canonical compliance so non-daily plans report adherence correctly on both the dashboard and the detail page (the detail page previously still used the legacy weekday walker).POST /api/export/health-recordproduces a selectable export: an enriched clinical PDF, an HL7 FHIR R4 document bundle, or both as one zip. The bundle reuses the PDF aggregator (identical numbers) and matches the iOS client's LOINC/UCUM conventions for interchangeable records. Patient identity (full name / insurer / KVNR, encrypted at rest) lands on the profile.GET /api/dashboard/snapshotassembles above-the-fold tiles in one round-trip, and a nightly job pre-generates the daily briefing so/insightsnever blocks on the model. Open dashboards refresh on a shared interval from a warm cache.GET /api/sync/changes) with soft-delete tombstones and a stable refresherrorCode.Schema
Migrations
0091–0096, all additive and defaulted: reminder flags + device delivery; schedule-type enum + cyclic columns; profile identity; unit preference; reminder-message time-of-day; measurement sync keyset index. No data loss;daysOfWeekretained for the legacy fallback.Tests
5628 → 5823 unit; 1 skipped.
docs/api/openapi.yamlregenerated and in sync. A six-dimension review (correctness, security, architecture, design/a11y, simplification, i18n) ran before this PR; every High and Medium finding was resolved in-branch.Deferred to v1.7.1 (technical debt, not features)
After merge
Tag
v1.7.0→ docker-publish → deploy → verify/api/versionreports1.7.0live → confirm/.well-known/apple-app-site-associationis published on the default host.