Add next backup countdown and download action to backup toasts

- Show "Next backup in X" in scheduled backups description
- Add download button to manual backup success toast
- Remove redundant schedule description from header
- Reorder day-of-week picker above time picker for weekly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 16:28:50 -05:00
co-authored by Claude Opus 4.6
parent 3511a76508
commit 0794bca301
@@ -82,25 +82,65 @@ const DAYS_OF_WEEK = [
"Saturday", "Saturday",
] as const; ] as const;
function describeSchedule( function getNextBackupDate(
frequency: BackupFrequency, frequency: BackupFrequency,
time: string, time: string,
_dayOfWeek = 0, dayOfWeek: number,
): string { ): Date {
const now = new Date();
const [h, m] = time.split(":").map(Number); const [h, m] = time.split(":").map(Number);
const d = new Date(2000, 0, 1, h, m);
const timeStr = format(d, "h:mm a");
switch (frequency) { if (frequency === "6h") {
case "6h": const next = new Date(now);
return "Every 6 hours"; const currentHour = next.getHours();
case "12h": const nextHour = Math.ceil((currentHour + 1) / 6) * 6;
return `Every 12 hours starting ${timeStr}`; next.setHours(nextHour, m, 0, 0);
case "1d": if (next <= now) next.setHours(next.getHours() + 6);
return `Daily at ${timeStr}`; return next;
case "7d":
return "Weekly";
} }
if (frequency === "12h") {
const next = new Date(now);
const h2 = (h + 12) % 24;
const candidates = [h, h2].sort((a, b) => a - b);
for (const candidate of candidates) {
next.setHours(candidate, m, 0, 0);
if (next > now) return next;
}
next.setDate(next.getDate() + 1);
next.setHours(candidates[0], m, 0, 0);
return next;
}
if (frequency === "7d") {
const next = new Date(now);
const daysUntil = (dayOfWeek - next.getDay() + 7) % 7;
if (daysUntil === 0) {
next.setHours(h, m, 0, 0);
if (next > now) return next;
next.setDate(next.getDate() + 7);
next.setHours(h, m, 0, 0);
return next;
}
next.setDate(next.getDate() + daysUntil);
next.setHours(h, m, 0, 0);
return next;
}
// 1d
const next = new Date(now);
next.setHours(h, m, 0, 0);
if (next <= now) next.setDate(next.getDate() + 1);
return next;
}
function formatNextBackup(
frequency: BackupFrequency,
time: string,
dayOfWeek: number,
): string {
const next = getNextBackupDate(frequency, time, dayOfWeek);
return `Next backup ${formatDistanceToNow(next, { addSuffix: true })}`;
} }
export function BackupSection({ export function BackupSection({
@@ -140,7 +180,17 @@ export function BackupSection({
try { try {
const backup = await createBackupAction(); const backup = await createBackupAction();
setBackups((prev) => [backup, ...prev]); setBackups((prev) => [backup, ...prev]);
toast.success("Backup created"); toast.success("Backup created", {
action: {
label: "Download",
onClick: () => {
const a = document.createElement("a");
a.href = `/api/backup/${backup.filename}`;
a.download = backup.filename;
a.click();
},
},
});
} catch { } catch {
toast.error("Failed to create backup"); toast.error("Failed to create backup");
} finally { } finally {
@@ -229,9 +279,7 @@ export function BackupSection({
setSavingSchedule(true); setSavingSchedule(true);
try { try {
await setBackupScheduleAction(newFrequency, newTime, newDow); await setBackupScheduleAction(newFrequency, newTime, newDow);
toast.success( toast.success("Schedule updated");
`Schedule updated: ${describeSchedule(newFrequency, newTime, newDow)}`,
);
} catch { } catch {
setFrequency(prevFreq); setFrequency(prevFreq);
setTime(prevTime); setTime(prevTime);
@@ -411,7 +459,10 @@ export function BackupSection({
<CardTitle>Scheduled</CardTitle> <CardTitle>Scheduled</CardTitle>
<CardDescription> <CardDescription>
{scheduledEnabled ? ( {scheduledEnabled ? (
<span className="inline-flex items-baseline gap-1"> <span className="inline-flex flex-wrap items-baseline gap-1">
<span suppressHydrationWarning>
{formatNextBackup(frequency, time, dow)}.
</span>{" "}
Keeping last{" "} Keeping last{" "}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger className="inline-flex cursor-pointer items-center gap-0.5 border-b border-dotted border-muted-foreground/50 transition-colors hover:text-foreground"> <DropdownMenuTrigger className="inline-flex cursor-pointer items-center gap-0.5 border-b border-dotted border-muted-foreground/50 transition-colors hover:text-foreground">