# Changelog ## [1.4.35.1] — 2026-05-17 — Auto-converging rollup backfill on worker boot Follow-on to v1.4.35. Removes the operator action that was implicit in the foundation release: instead of self-hosters having to run `pnpm tsx scripts/backfill-rollups.ts` after upgrading, the worker boot now discovers any account with measurements but no rollup coverage and enqueues a one-shot full fold via pg-boss. Idempotent across reboots (the discovery query only matches uncovered users) and zero reader-path impact (serial concurrency, runs in the background). ### Boot-time backfill - **New `rollup-full-backfill` queue** (`ROLLUP_FULL_BACKFILL_QUEUE`, concurrency = 1) carrying `{ userId, enqueuedAt }`. The handler invokes `recomputeUserRollups(userId)` with the default 5-year window across all four granularities — equivalent to the manual CLI run, but inside the worker process so no `tsx` install gymnastics in production images. - **`enqueueBootTimeRollupBackfill`** runs once at `startReminderWorker` boot after every `boss.work` subscription is in place. The discovery query finds users with at least one row in `measurements` and zero rows in `measurement_rollups`, then sends one job per uncovered account with `singletonKey: boot-backfill|{userId}` so a fast restart while a fold is queued doesn't pile up duplicates. - **Best-effort:** any error during discovery is captured in the return value and logged through `workerLog`; the worker boot never fails because the backfill missed. ### Tests - 5 new unit cases pin the helper's contract (silent no-op without boss, one job per uncovered user, singleton-key coalesce counted as `skipped`, error surfacing, zero-users path). 2 new integration cases verify against the real testcontainer: only the uncovered user lands on the queue when a third already-folded user co-exists; zero jobs enqueued when every account is already covered. - Unit suite 4280 → 4285. Integration suite 228 → 230. `pnpm typecheck` + `pnpm lint` clean. ## [1.4.35] — 2026-05-17 — Persistent measurement rollups + partial read-swap Foundation release for the persistent `measurement_rollups` cache tier. Adds a new additive table, populates it from every write path, and switches two reader surfaces (the comprehensive insights aggregator and the slim analytics summaries slice) onto it for the linearly-composable stats. Slope / R² / standard deviation / anomaly counts continue to run against live SQL — they don't compose across DAY buckets and stay on the canonical aggregate. A full read-swap across every analytics surface is planned for a follow-up. ### Schema - **New table `measurement_rollups`** keyed on `(user_id, type, granularity, bucket_start)` with `count / mean / min_value / max_value / sd / slope / r2 / computed_at`. Backed by a composite-descending index on `(user_id, type, granularity, bucket_start DESC)`. Additive only — no `ALTER` on existing tables. Migration `0067_v1434_measurement_rollups` is idempotent under re-run. - **New enum `measurement_rollup_granularity`** with `DAY`, `WEEK`, `MONTH`, `YEAR` granularities. The DAY grain is recomputed inline on every measurement write; the wider grains queue onto pg-boss. ### Write hooks - Every measurement create / update / delete endpoint now calls `recomputeBucketsForMeasurement` inside a `try / catch` after its successful commit. The DAY bucket is folded synchronously so the next read is correct; WEEK / MONTH / YEAR follow on the pg-boss `rollup-recompute` queue (concurrency = 2, singleton-keyed per bucket to coalesce burst writes onto a single worker run). - The Apple Health import worker calls `recomputeUserRollups` once at completion instead of per-row, scoped to the import's date range, so a 100k-sample backfill no longer spawns 100k write hooks. ### Reads - `comprehensive-aggregator` (the engine behind `/api/insights/comprehensive`) sources `count / min / max / mean` per type from the DAY buckets via `aggregateBuckets`, plus the `dailyByType` correlation feed straight from the bucket means. A defensive parity check against the live aggregate's `COUNT(*)` falls back to live SQL when divergence is detected — covers the cold-mount edge case where the rollup populator is mid-flight. - `summaries-slice` (the slim slice behind `?slice=summaries`) does the same. The all-time window degrades cleanly on accounts that never ran the explicit backfill: the rollup covers only what the warm-on-read populator pre-folded, the parity check diverges, and the live SQL values take over — no semantics break. - `ensureUserRollupsFresh` is a cheap watermark query that runs before every read; on a warm process it's a single indexed lookup, on cold mount it folds the trailing 90-day window into the DAY rollup before the bucket read fires. ### Backfill - New `scripts/backfill-rollups.ts` walks every user (or one account via `--user `) and folds their measurement history into all four granularities. Single-user serial so the Prisma pool stays out of contention. Idempotent — every run upserts under the composite primary key. ### Tests - Three new integration cases in `tests/integration/measurement-rollups.test.ts` pin the parity contract end-to-end against a real Postgres testcontainer: the aggregator's `count / min / max / mean` matches live SQL byte-for-byte, `dailyByType` matches a parallel `date_trunc('day', …)` GROUP BY query, and a freshly-written measurement is reflected on the next read. - Unit suite up by 31 cases (4249 → 4280); integration suite up by 6 cases (222 → 228). `pnpm typecheck` + `pnpm lint` clean. ## [1.4.34.5] — 2026-05-17 — Audit follow-on: critical-path tests + iOS textarea zoom Follow-on to v1.4.34.4. Two batches: the missing critical-path integration tests the test-coverage audit flagged, and the textarea viewport-zoom fix the mobile-deep audit flagged as F-5. ### Tests - **24 new integration tests across 5 files** closing the critical gaps from `.planning/round-audit-test-coverage.md`: - `tests/integration/auth-password-change.test.ts` pins the v1.4.34.3+ `destroyAllSessions` three-transport revocation contract end-to-end (Session.deleteMany, ApiToken.revoked, RefreshToken.revokedAt). - `tests/integration/admin-reset-password.test.ts` mirrors the same contract for the admin-driven reset. - `tests/integration/withings-oauth.test.ts` covers connect / callback / disconnect plus state-token replay rejection. - `tests/integration/passkey-register.test.ts` covers register-options + register-verify + tampered-attestation rejection + challenge single-use. - `tests/integration/cache-invalidation-coverage.test.ts` grids the v1.4.34 IW-G invalidation matrix across 5 write surfaces. - Integration suite goes from 197/50 to 222/55. tsc + lint clean. ### Mobile - **Five textareas no longer trigger iOS Safari zoom-on-focus.** The Coach composer, bug-report description, admin feedback note, medication-side-effects notes, and medication-intake JSON import all shipped `text-sm` (14 px). iOS Safari zooms the viewport whenever an input renders below 16 px. Switched each to `text-base sm:text-sm` so the mobile baseline clears the floor while desktop keeps the compact look. ## [1.4.34.4] — 2026-05-17 — Audit-driven hotfix bundle (security, UX, code-quality, docs) A consolidated hotfix landing the findings from a six-axis audit covering mobile security, UX + accessibility, code quality + docs, README + discoverability, web performance, and mobile-deep responsive behaviour. None of the changes break the public API or the existing data shape; every fix is additive and SAFE under the v1.4.34.x web freeze. The full audit reports live in `.planning/round-audit-*.md`. ### Security - **Password rotation now revokes every transport.** `destroyAllSessions` previously deleted only `Session` rows; long-lived `ApiToken` Bearer credentials and short-lived `RefreshToken` rotations survived a user-initiated password change or an admin reset. A stolen iOS Bearer kept working past the user's most obvious self-remediation. The helper now wraps a `$transaction` that revokes all three transports in one shot. Same rotation runs from `POST /api/auth/password`, `POST /api/admin/users/[id]/reset-password`, and `DELETE /api/settings/account`. - **`/.well-known/` proxy bypass tightened.** Replaced the prefix-match `startsWith("/.well-known/")` with an explicit-allowlist `Set` seeded only with `apple-app-site-association`. New IETF discovery endpoints must be added explicitly so a future `/.well-known/openid-configuration` doesn't silently inherit "no auth" status. Also dropped the stale `/api/auth/codex/callback` PUBLIC_PATHS entry (the route was renamed to the device-code family). - **HSTS gains `preload`.** The domain is now eligible for the Chromium preload list (submit to hstspreload.org after this lands). - **Withings host removed from the global CSP.** `wbsapi.withings.net` was in every page's `connect-src`; mirrored the AI-host gating pattern so the host enters CSP only on `/settings/integrations/withings` and `/api/withings/` paths. Closes a DOM-XSS exfil shape on every non-Withings page. - **`getClientIp` trust-violation signal.** When `TRUST_PROXY_HOPS` and the XFF chain length disagree, every anonymous caller collapsed to the `"unknown"` rate-limit bucket. The helper now warns once per process and exposes a tagged `getClientIpOrTrustWarning(request)` return so future callers can route to a tighter universal bucket. - **Service-worker URL trust.** `notificationclick` now rejects payload URLs whose origin doesn't match `self.location.origin` and falls back to `/`. Push payloads are VAPID-authenticated but the push-server is our own; a server-side bug should not be able to navigate the user's PWA window off-origin. - **PWA manifest hygiene.** Added `"scope": "/"`, `"id": "/?source=pwa"`, `"display_override": ["standalone"]` so sub-path deployments don't inherit root scope and install-prompt heuristics anchor on a stable id. ### UX + accessibility - **i18n leaks closed.** Dialog and Sheet close-X buttons no longer ship hardcoded `Close` — both read `common.close` in every locale. Same fix on the ChartSkeleton's loading announcement (new `charts.loadingLabel` key) and the notifications-section breadcrumb (`nav.breadcrumb`). All six locale files updated. - **Focus rings on mobile + tablet navigation.** The bottom-nav primary links, the More overflow trigger, and the mobile top-bar user menu trigger gained `focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-offset-2`. Keyboard and switch-control users can now see focus on every nav surface. - **Tap-target floor (WCAG 2.5.5).** Input, NativeSelect, Select, and DateTimeInput now ship `h-11 sm:h-10` so mobile clears the 44 px floor while desktop keeps the 40 px norm the Button primitive converged on in v1.4.33. - **Admin chip-strip auto-scroll-into-view.** Mirrored the v1.4.33 settings-shell pattern: the active admin chip now scrolls to the centre of the strip on route change. - **List-page subtitles on mobile.** Measurements, Mood, and Medications pages now keep their descriptive subtitle on `