mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
fix(rollups): fold measurement tier on /api/import + admin backup restore
Both ingest paths wrote into `measurements` without firing the rollup write hook, leaving the dashboard chart's `source=rollup` fast-path blind to the imported days. The mood / medication-compliance rollup tiers already had post-batch hooks on these routes; the measurement tier had been the gap. - `/api/import`: tracks every touched `(type, measuredAt)` written during the batch, collapses to distinct `(type, day)` via `collapseToTypeDayKeys`, and folds once at the end. A 10 000-row CSV restore now pays at most ~N (type, day) recomputes rather than 10 000 per-row hooks. - Admin backup restore: the transaction wipes the user's `measurement_rollups` partition alongside the existing `mood_entry_rollups` wipe, and the post-transaction step kicks a `recomputeUserRollups(ownerId)` so the restored dataset paints fresh tiles instead of carrying the previous owner's daily means forward. - Best-effort posture preserved in both paths: a populator hiccup surfaces via `annotate` but never undoes the restore / import. Test coverage extends the existing `/api/import` route suite with a regression that pins the per-`(type, day)` collapse and a duplicate- write case (`P2002`) that proves the hook is skipped when nothing was actually written.
This commit is contained in:
1 parent
ad22fa2e9d
commit
a2b0ab19e5
3 files changed
+193
-1
No files matched your search
@@ -48,6 +48,7 @@ import {
|
||||
recomputeUserMedicationCompliance,
|
||||
MEDICATION_COMPLIANCE_BACKFILL_DAYS,
|
||||
} from "@/lib/medications/compliance-rollups";
|
||||
import { recomputeUserRollups } from "@/lib/measurements/rollups";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -291,6 +292,14 @@ const handler = apiHandler(
|
||||
// pre-restore daily means. The fold below mints fresh rows
|
||||
// from the restored mood entries.
|
||||
await tx.moodEntryRollup.deleteMany({ where: { userId: ownerId } });
|
||||
// v1.4.39.1 — wipe the persisted measurement rollup partition
|
||||
// for the same reason. Pre-fix the partition kept the previous
|
||||
// owner's daily means even after the underlying measurements
|
||||
// were replaced — the chart's `source=rollup` fast-path could
|
||||
// surface a stale 30-day mean built from rows that no longer
|
||||
// existed. The fold below mints fresh rows from the restored
|
||||
// measurement set.
|
||||
await tx.measurementRollup.deleteMany({ where: { userId: ownerId } });
|
||||
const channels = await tx.notificationChannel.deleteMany({
|
||||
where: { userId: ownerId },
|
||||
});
|
||||
@@ -420,6 +429,29 @@ const handler = apiHandler(
|
||||
return apiError("Restore failed", 500);
|
||||
}
|
||||
|
||||
// v1.4.39.1 — re-fold the persistent measurement rollup tier from
|
||||
// the just-restored measurements. Pre-fix the restore left the
|
||||
// rollup table empty for the owner, so the dashboard chart's
|
||||
// `source=rollup` fast-path silently returned zero buckets until
|
||||
// the next worker boot ran the backfill discovery — multi-hour
|
||||
// window of empty charts for the operator-restored account. Runs
|
||||
// outside the transaction (the 5-year fold would otherwise hold a
|
||||
// long write lock) and is best-effort so a populator hiccup never
|
||||
// undoes the restore. The boot-time backfill is the safety net.
|
||||
if (payload.measurements.length > 0) {
|
||||
try {
|
||||
await recomputeUserRollups(ownerId);
|
||||
} catch (err) {
|
||||
annotate({
|
||||
meta: {
|
||||
measurement_rollup_restore_failed: true,
|
||||
measurement_rollup_restore_error:
|
||||
err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// v1.4.39 W-MOOD — re-fold the mood rollup tier from the just-
|
||||
// restored entries. Runs outside the transaction so the (5-year)
|
||||
// fold can't hold a long write lock; best-effort so a populator
|
||||
|
||||
Reference in new issue
Block a user