perf(mood-analytics): consume rollup tier instead of unbounded findMany

The /api/mood/analytics cold mount walked every MoodEntry the user
had ever written into Node only to bucket-average in JS. Reroutes
the read through readMoodDayRollups which returns one row per
calendar day from the persisted tier, and wires the per-write hook
into every mood mutation site so the rollup stays current:

  - POST/PUT/DELETE /api/mood-entries — inline recompute
  - POST /api/mood-entries/bulk — collapsed to per-day fan-out
  - POST /api/integrations/moodlog/webhook — inline recompute
  - syncMoodLogEntries — bounded re-fold after batch sync
  - DELETE /api/settings/moodlog + DELETE /api/settings/data —
    drop the user's rollup partition alongside the entries
  - admin restore + import — re-fold after the bulk write

Coverage-fallback mirrors summaries-slice: when the rollup count is
zero but raw mood entries exist (legacy account before the boot
backfill catches up) the route falls through to the legacy live walk
once and the warm-up helper fires asynchronously so the next request
lands on the fast path.
This commit is contained in:
Marc-André Bombeck committed 2026-05-18 12:22:38 +02:00
1 parent 823764a914
commit 266e45ffa4
10 files changed
+291

No files matched your search

@@ -43,6 +43,7 @@ import {
summarizeBackup,
type BackupSummary,
} from "@/lib/validations/backup";
import { recomputeUserMoodRollups } from "@/lib/mood/rollups";
export const dynamic = "force-dynamic";
@@ -281,6 +282,11 @@ const handler = apiHandler(
const moods = await tx.moodEntry.deleteMany({
where: { userId: ownerId },
});
// v1.4.39 W-MOOD — wipe the persisted mood rollup partition
// for this owner so the next analytics read doesn't surface
// pre-restore daily means. The fold below mints fresh rows
// from the restored mood entries.
await tx.moodEntryRollup.deleteMany({ where: { userId: ownerId } });
const channels = await tx.notificationChannel.deleteMany({
where: { userId: ownerId },
});
@@ -410,6 +416,52 @@ const handler = apiHandler(
return apiError("Restore failed", 500);
}
// 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
// hiccup doesn't undo the restore. The boot-time backfill is the
// safety net — if this fails the next worker boot mints the rows.
if (payload.moodEntries.length > 0) {
try {
await recomputeUserMoodRollups(ownerId, { granularities: ["DAY"] });
} catch (err) {
annotate({
meta: {
mood_rollup_restore_failed: true,
mood_rollup_restore_error:
err instanceof Error ? err.message : String(err),
},
});
}
}
// v1.4.39 W-MED — re-fold the medication-compliance rollup tier
// from the restored intake events. The medication delete inside the
// transaction already cascaded the existing rollup partition for
// this owner via the FK, so the fold mints fresh rows. Boot-time
// backfill is the safety net if this best-effort call fails.
if (payload.intakeEvents.length > 0) {
try {
const restoreUser = await prisma.user.findUnique({
where: { id: ownerId },
select: { timezone: true },
});
await recomputeUserMedicationCompliance(
ownerId,
MEDICATION_COMPLIANCE_BACKFILL_DAYS,
restoreUser?.timezone ?? null,
);
} catch (err) {
annotate({
meta: {
medication_compliance_rollup_restore_failed: true,
medication_compliance_rollup_restore_error:
err instanceof Error ? err.message : String(err),
},
});
}
}
const summary = summarizeBackup(payload);
await auditLog("admin.backups.restore", {