fix(backup): notice a backup run the process died under, and take its account last

A weekly or nightly backup job whose process died (a restart, a
container out of memory) stayed active until its two- or four-hour
expiry, and its retry walked the accounts in the same order: the account
that killed it has the oldest copy, so it led again, and every account
after it went without a copy for as long as that record stayed too
large.

Both passes now send with a five-minute pg-boss heartbeat, so a dead run
fails within minutes and its retry picks it up. Each pass records when it
started and finished every account (backup_pass_attempts, migration
0373); an account whose last attempt never finished goes behind all the
others. The backups page shows when the run in progress started and names
an account a run died under, for the weekly and the off-host card, and a
heartbeat timeout reads as what it means.
This commit is contained in:
Marc-André Bombeck committed 2026-10-04 15:56:56 +02:00
1 parent d55e329c48
commit f18e6ed34b
31 files changed
+734 -19

No files matched your search

@@ -24,6 +24,7 @@ import { I18nProvider } from "@/lib/i18n/context";
import { OffhostBackupsSection } from "../offhost-backups-section";
const QUIET = {
activity: { runningSince: null, interrupted: [] },
pendingDeletions: { count: 0, oldestRequestedAt: null, lastFailure: null },
lifecycle: { state: "configured" as const, expirationDays: 30 },
};
@@ -126,6 +127,7 @@ describe("<OffhostBackupsSection>", () => {
configured: true,
periodHours: 24,
rows: [],
activity: { runningSince: null, interrupted: [] },
pendingDeletions: {
count: 2,
oldestRequestedAt: "2026-09-20T10:00:00.000Z",
@@ -149,4 +151,26 @@ describe("<OffhostBackupsSection>", () => {
expect(html).toContain("after 30 days");
expect(html).not.toContain('data-slot="offhost-pending-deletions"');
});
it("names an account a run died under, and says since when one runs", () => {
const html = render({
configured: true,
periodHours: 24,
rows: [],
...QUIET,
activity: {
runningSince: "2026-10-04T02:30:00.000Z",
interrupted: [
{
userId: "u1",
username: "account-one",
startedAt: "2026-10-03T02:31:00.000Z",
},
],
},
});
expect(html).toContain('data-slot="backup-pass-running"');
expect(html).toContain('data-interrupted-username="account-one"');
expect(html).toContain("never finished");
});
});