mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat(admin): say what a queued restore is waiting for
A restore never runs beside a backup pass or another large job: the heavy lane puts its delivery back in the queue for a minute at a time. The job's own row says queued for the whole wait, and the console said the restore starts "as soon as the background worker picks it up", which read as a stuck worker while a backup ran. The status routes now ask the queue. A delivery the lane put back sits in created with a start_after ahead of now, and the active row of a heavy queue is what holds the lane. The job view carries waiting: "backup" | "busy" | null, and the console says it waits for the running backup, or for another large job, and since when it has been queued, in all nine locales. A queue that cannot be asked answers no wait and is reported at warn. The job view and its reads move to backup-restore-view.ts, which keeps the worker module under its size baseline.
This commit is contained in:
1 parent
6a87810dbf
commit
7936ab9a57
28 files changed
+544
-199
No files matched your search
@@ -26,7 +26,7 @@ import {
|
||||
RestoreJobStatus,
|
||||
visibleRestoreJobs,
|
||||
} from "@/components/admin/backup-restore-status";
|
||||
import type { BackupRestoreJobView } from "@/lib/jobs/backup-restore";
|
||||
import type { BackupRestoreJobView } from "@/lib/jobs/backup-restore-view";
|
||||
import { primeLocales } from "@/__tests__/helpers/prime-locales";
|
||||
|
||||
primeLocales("de");
|
||||
@@ -54,6 +54,7 @@ function job(overrides: Partial<BackupRestoreJobView>): BackupRestoreJobView {
|
||||
},
|
||||
result: null,
|
||||
failure: null,
|
||||
waiting: null,
|
||||
attempts: 1,
|
||||
createdAt: "2026-09-25T08:00:00.000Z",
|
||||
startedAt: "2026-09-25T08:00:01.000Z",
|
||||
@@ -85,9 +86,29 @@ describe("RestoreJobStatus while it runs", () => {
|
||||
expect(html).not.toContain('data-slot="restore-job-progress"');
|
||||
});
|
||||
|
||||
it("says a queued restore is waiting", () => {
|
||||
it("says a queued restore is waiting, and since when", () => {
|
||||
const html = render(job({ status: "queued", phase: null, progress: null }));
|
||||
expect(html).toContain("waiting to start");
|
||||
expect(html).toContain("background worker picks it up");
|
||||
expect(html).toContain("Queued at");
|
||||
expect(html).not.toContain("data-waiting");
|
||||
});
|
||||
|
||||
it("says it waits for the running backup, not for a stuck worker", () => {
|
||||
const html = render(
|
||||
job({ status: "queued", phase: null, progress: null, waiting: "backup" }),
|
||||
);
|
||||
expect(html).toContain('data-waiting="backup"');
|
||||
expect(html).toContain("Waiting for the backup");
|
||||
expect(html).not.toContain("background worker picks it up");
|
||||
});
|
||||
|
||||
it("says it waits for another large job when that holds the server", () => {
|
||||
const html = render(
|
||||
job({ status: "queued", phase: null, progress: null, waiting: "busy" }),
|
||||
);
|
||||
expect(html).toContain('data-waiting="busy"');
|
||||
expect(html).toContain("large job");
|
||||
});
|
||||
|
||||
it("says when it was started again after a restart", () => {
|
||||
@@ -179,4 +200,12 @@ describe("RestoreJobStatus in another language", () => {
|
||||
expect(html).toContain("alex wird wiederhergestellt");
|
||||
expect(html).toContain("Messwerte werden geschrieben");
|
||||
});
|
||||
|
||||
it("says in German that it waits for the backup", () => {
|
||||
const html = render(
|
||||
job({ status: "queued", phase: null, progress: null, waiting: "backup" }),
|
||||
"de",
|
||||
);
|
||||
expect(html).toContain("Wartet auf das Backup");
|
||||
});
|
||||
});
|
||||
@@ -28,10 +28,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import type { RestoreFailureCause } from "@/lib/export/restore-failure-cause";
|
||||
import type {
|
||||
BackupRestoreFailureCode,
|
||||
BackupRestoreJobView,
|
||||
} from "@/lib/jobs/backup-restore";
|
||||
import type { BackupRestoreFailureCode } from "@/lib/jobs/backup-restore";
|
||||
import type { BackupRestoreJobView } from "@/lib/jobs/backup-restore-view";
|
||||
|
||||
/** Whether a job still holds its account's one restore slot. */
|
||||
export function isActiveRestore(job: BackupRestoreJobView): boolean {
|
||||
@@ -147,6 +145,30 @@ function transactionFailureText(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What a queued job is waiting for. A restore never runs beside a backup or
|
||||
* another large job (the heavy lane puts it back for a minute at a time), and
|
||||
* a wait that said only "until the worker picks it up" read as a stuck
|
||||
* worker for as long as a backup ran.
|
||||
*/
|
||||
function queuedText(
|
||||
job: BackupRestoreJobView,
|
||||
t: ReturnType<typeof useTranslations>["t"],
|
||||
): string {
|
||||
switch (job.waiting) {
|
||||
case "backup":
|
||||
return t("admin.section.backups.restoreJobWaitingBackupNote");
|
||||
case "busy":
|
||||
return t("admin.section.backups.restoreJobWaitingBusyNote");
|
||||
case null:
|
||||
return t("admin.section.backups.restoreJobQueuedNote");
|
||||
default: {
|
||||
const unhandled: never = job.waiting;
|
||||
return unhandled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** What a running job is doing, and the share of it done when that is known. */
|
||||
function runningLine(
|
||||
job: BackupRestoreJobView,
|
||||
@@ -213,16 +235,14 @@ export function RestoreJobStatus({
|
||||
const line =
|
||||
job.status === "running"
|
||||
? runningLine(job, t, fmt)
|
||||
: {
|
||||
text: t("admin.section.backups.restoreJobQueuedNote"),
|
||||
percent: null,
|
||||
};
|
||||
: { text: queuedText(job, t), percent: null };
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
data-slot="restore-job-status"
|
||||
data-status={job.status}
|
||||
data-waiting={job.waiting ?? undefined}
|
||||
className="border-border bg-muted/30 space-y-2 rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -248,6 +268,13 @@ export function RestoreJobStatus({
|
||||
data-slot="restore-job-progress"
|
||||
/>
|
||||
) : null}
|
||||
{job.status === "queued" ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("admin.section.backups.restoreJobQueuedSince", {
|
||||
when: fmt.dateTime(job.createdAt),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
{job.attempts > 1 ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("admin.section.backups.restoreJobResumed")}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
MissingBackupSection,
|
||||
RestoreSkipSummary,
|
||||
} from "@/lib/export/restore-skips";
|
||||
import type { BackupRestoreJobView } from "@/lib/jobs/backup-restore";
|
||||
import type { BackupRestoreJobView } from "@/lib/jobs/backup-restore-view";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { randomId } from "@/lib/random-id";
|
||||
|
||||
Reference in new issue
Block a user