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. Synchronous DAY-tier write hook on everyMoodEntrymutation; 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.dayis a user-timezone-anchoredYYYY-MM-DDstring. Hook fires on every intake-event mutation plus the reminder-worker mint path. Boot-time backfill queue (medication-compliance-full-backfill).measurement_rollups.sum_value— nullable cumulative-metric column populated alongsidemean / countin every rollup fold. Existing rows backfill on next reach via the extendedrollup-full-backfilldiscovery query.rollup-read-wmy.ts— WEEK / MONTH / YEAR reader helpers with an auto-router that picks the largest granularity resolving the requested window (90 d → DAY, 365 d → MONTH, 1 095 d → YEAR). Coverage-miss fall-through. Ready for the v1.5 multi-year trend card.rollup-read-cumulative.ts—readCumulativeDaySums/readCumulativeDaySumsBatch/resolveBucketSumhelpers with legacy-NULL fallback.
Performance
Expected on accounts with several hundred thousand measurements. Numbers anchored on the .planning/round-v1438-perf-analysis.md audit. Live perf-verify rides the post-deploy window.
/api/mood/analyticscold mount: 12.7 s → ~200 ms. Was an unboundedMoodEntry.findManywalk + JS aggregation; now a bounded rollup read. Live-fallback retained for coverage misses and pre-aggregates daily means beforesummarize()sosummary.mean / latest / min / max / avg7 / avg30 / slope30stay byte-identical between the two branches on multi-entry days./api/medications/intake?scope=compliancecold mount: 3.2 s → ~200 ms. Coverage probe counts rolled days vs days with intake events (partial-coverage cases route to the live fallback, not the rollup). Atomic upsert closes the read-aggregate-then-upsert window under concurrent reminder-worker and Telegram intake./api/dashboard/summarycumulative sparkline: ~500 ms → ~300 ms. Readssum_valuedirectly instead of recomputing viamean × count./api/measurements?groupBy=daycumulative path consumessum_valuedirectly; eliminates per-type JS aggregation onACTIVITY_STEPS / FLIGHTS_CLIMBED / WALKING_RUNNING_DISTANCE / TIME_IN_DAYLIGHT / ACTIVE_ENERGY./api/analyticslive-fallback row cap: ~347 k → ~5 k. Trailing 425-daysincecap on thefetchMeasurementSeriesChunkedper-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 preservessummary.avg30LastYear(year-ago baseline tile).
Fixed
- Mood + medication-compliance DAY recompute race closed via single atomic
INSERT … SELECT … ON CONFLICT DO UPDATE. - Mood rollup async worker enqueue no longer awaits the user response; WEEK / MONTH / YEAR enqueue is fire-and-forget.
- Partial-coverage zero-fill on
/api/medications/intake?scope=compliance. Probe now compares rolled-day count to days-with-events. - Coverage-miss backfill enqueue scoped to the caller's user. Cluster-wide discovery stays on the worker boot path.
dashboard-summarynested ternary in the sparkline branch flattened.
Operator notes
- Migrations
0070,0071,0072are additive only.0072adds a nullableDOUBLE PRECISIONcolumn — 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.