chore(release): v1.4.39

Mood, medication-compliance, and cumulative-sum rollup tiers extend
the v1.4.38 fast-path posture to two more endpoints that still walked
the source table on every cold mount.

Raw measurements / mood_entries / medication_intake_events tables are
byte-unchanged. Three new tiers are derived caches that self-heal via
boot-time backfill on first reach.

Expected wins:
- /api/mood/analytics 12.7 s -> ~200 ms
- /api/medications/intake?scope=compliance 3.2 s -> ~200 ms
- /api/dashboard/summary ~500 ms -> ~300 ms
- /api/analytics live-fallback 347 k rows -> ~5 k rows (425 d cap)

Tests 4 524 -> 4 640 (+116). Typecheck clean. Lint clean.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 07:38:31 +02:00
1 parent 96e4cc06f1
commit 4d6a6126b0
2 files changed
+118 -1

No files matched your search

+117
View File
@@ -1,5 +1,122 @@
# Changelog
## [1.4.39] — 2026-05-21 — Mood, medication-compliance, and cumulative-sum rollup tiers
The v1.4.38 chain settled the measurement-rollup fast-path. v1.4.39
extends the same "raw data stays untouched, a derived second layer
serves the read path" posture to two more endpoints that still walked
the source table on every cold mount — `/api/mood/analytics` and
`/api/medications/intake?scope=compliance` — and folds a cumulative
`sum_value` column into the existing `measurement_rollups` tier so
step / flight / distance / daylight / active-energy sparklines no
longer re-derive their daily totals in Node.
Raw `measurements`, `mood_entries`, `medication_intake_events` tables
are byte-unchanged. The three new tiers are derived caches that
self-heal via boot-time backfill on first reach.
### Added
- **`mood_entry_rollups`** — per-(user, granularity, bucket) mood
stats. Reuses the existing `RollupGranularity` enum so the worker
shares one type across mood and measurement tiers. Synchronous
DAY-tier write hook on every `MoodEntry` create / update / delete;
WEEK / MONTH / YEAR folded asynchronously through pg-boss. Boot-time
backfill queue (`mood-rollup-full-backfill`) discovers legacy
accounts and converges on first worker boot.
- **`medication_compliance_rollups`** — per-(user, medication, day)
scheduled / taken / skipped ledger. `day` is a user-timezone-anchored
`YYYY-MM-DD` string so multi-instance reads do not re-derive
boundaries. Hook fires on every intake-event mutation plus the
reminder-worker mint path. Boot-time backfill queue
(`medication-compliance-full-backfill`) handles legacy accounts.
- **`measurement_rollups.sum_value`** — nullable cumulative-metric
column populated alongside `mean / count` in every rollup fold.
Existing rows backfill on next reach via the extended
`rollup-full-backfill` discovery query.
- **`rollup-read-wmy.ts`** — `readWeekRollups` / `readMonthRollups` /
`readYearRollups` / `readBestGranularityRollups` reader helpers. The
auto-router picks the largest granularity that resolves the
requested window (90 d → DAY, 365 d → MONTH, 1 095 d → YEAR) with
coverage-miss fall-through. Ready for the v1.5 multi-year trend
card; not exposed via any route yet.
- **`computeLongWindowSummary(userId, type, windowDays)`** in
`summaries-slice.ts` — granularity-routed `count / min / max /
mean / sum` aggregate for long-window consumers.
- **`rollup-read-cumulative.ts`** — `readCumulativeDaySums` /
`readCumulativeDaySumsBatch` / `resolveBucketSum` helpers with
legacy-NULL fallback.
### Performance
Expected on Marc-sized accounts; numbers anchored on the
`.planning/round-v1438-perf-analysis.md` audit and confirmed by the
unit-test fixture suite. Live perf-verify rides the post-deploy
window.
- **`/api/mood/analytics` cold mount: 12.7 s → ~200 ms.** Was an
unbounded `MoodEntry.findMany` walk + JS aggregation; now a bounded
rollup read. 5-year `since` ceiling on the rollup branch keeps the
query plan stable on multi-year tenants. Live-fallback retained for
coverage misses and pre-aggregates daily means before `summarize()`
so `summary.mean / latest / min / max / avg7 / avg30 / slope30` stay
byte-identical between the two branches on multi-entry days.
- **`/api/medications/intake?scope=compliance` cold mount: 3.2 s → ~200 ms.**
Was an unbounded intake-event findMany + per-day JS bucketing; now a
per-day rollup read. Coverage probe counts rolled days vs days with
intake events (partial-coverage cases route to the live fallback,
not the rollup). Race-safe atomic upsert closes the
read-aggregate-then-upsert window under concurrent reminder-worker
and Telegram intake.
- **`/api/dashboard/summary` cumulative sparkline: ~500 ms → ~300 ms.**
Reads `sum_value` directly instead of recomputing via `mean × count`.
- **`/api/measurements?groupBy=day` cumulative path:** consumes
`sum_value` directly; eliminates per-type JS aggregation on
`ACTIVITY_STEPS / FLIGHTS_CLIMBED / WALKING_RUNNING_DISTANCE /
TIME_IN_DAYLIGHT / ACTIVE_ENERGY`.
- **`/api/analytics` live-fallback row cap: 347 k → ~5 k.** Trailing
425-day `since` cap on the `fetchMeasurementSeriesChunked` per-type
loop. Defense-in-depth — the v1.4.38.8 per-type fast-path gate makes
this path unreachable in the common case, but a regression that
re-triggers it can no longer pull the entire row history. The 425 d
window preserves `summary.avg30LastYear` (year-ago baseline tile);
the rollup fast-path stays untouched.
### Fixed
- **Mood + medication-compliance DAY recompute race.** Concurrent
writes for the same `(user, day)` could interleave their SELECT and
UPSERT and leave a stale row until the next write. Both helpers
now use a single atomic `INSERT … SELECT … ON CONFLICT DO UPDATE`
that re-aggregates inside the upsert subquery.
- **Mood rollup async worker enqueue blocked the user response.** The
WEEK / MONTH / YEAR pg-boss enqueue is now fire-and-forget; the
read-critical DAY pass commits synchronously above so the cold-mount
cost no longer drags pg-boss latency into mood-write responses.
- **Partial-coverage zero-fill on `/api/medications/intake?scope=compliance`.**
The coverage probe used to flip to the rollup path on a single
rollup row in the window — legacy accounts with a mid-deploy
in-progress backfill saw zero-filled tiles for the un-rolled days.
Probe now compares rolled-day count to days-with-events.
- **Coverage-miss backfill enqueue scoped to the caller's user.** The
request-path warm-up no longer kicks off a cluster-wide discovery
scan on every cache-miss; cluster discovery stays on the worker
boot path. Mirrors the mood-rollup pattern.
- **`dashboard-summary` nested ternary** in the sparkline branch
flattened per the project's no-nested-ternaries rule.
### Operator notes
- Migrations `0070`, `0071`, `0072` are additive only. `0072` adds a
nullable `DOUBLE PRECISION` column with no default — catalog-only
DDL on PostgreSQL 11+, no table rewrite, writes resume in
milliseconds.
- No environment-variable change. No API contract break. No iOS
contract change.
- Boot-time backfills converge automatically on first reach. Operator
trigger remains available via `POST /api/admin/rollups/recompute`.
- Tests: 4 524 → 4 640 unit (+116). Integration suite green.
## [1.4.38.8] — 2026-05-18 — Analytics fast-path gates per-type only
The v1.4.38.5–.7 chain confirmed the rollup-fast-path was bouncing
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.4.38.8",
"version": "1.4.39",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "AGPL-3.0-only",
"homepage": "https://healthlog.dev",