mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
fix(web): settings toggles refresh cache, backup next-run from server, update toast once
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
17 files changed
+123
-56
No files matched your search
@@ -20,7 +20,13 @@ import { getSystemHealth } from "@sofa/core/system-health";
|
||||
import { isTelemetryEnabled } from "@sofa/core/telemetry";
|
||||
import { getCachedUpdateCheck, isUpdateCheckEnabled } from "@sofa/core/update-check";
|
||||
|
||||
import { pauseJobs, rescheduleBackup, resumeJobs, triggerJob as triggerCronJob } from "../../cron";
|
||||
import {
|
||||
getJobSchedules,
|
||||
pauseJobs,
|
||||
rescheduleBackup,
|
||||
resumeJobs,
|
||||
triggerJob as triggerCronJob,
|
||||
} from "../../cron";
|
||||
import { os } from "../context";
|
||||
import { admin } from "../middleware";
|
||||
|
||||
@@ -100,12 +106,17 @@ export const backupsRestore = os.admin.backups.restore
|
||||
});
|
||||
|
||||
export const backupsSchedule = os.admin.backups.schedule.use(admin).handler(() => {
|
||||
const enabled = getSetting("scheduledBackups") === "true";
|
||||
return {
|
||||
enabled: getSetting("scheduledBackups") === "true",
|
||||
enabled,
|
||||
maxRetention: Number.parseInt(getSetting("maxBackupRetention") ?? "7", 10),
|
||||
frequency: (getSetting("backupScheduleFrequency") ?? "1d") as "6h" | "12h" | "1d" | "7d",
|
||||
time: getSetting("backupScheduleTime") ?? "02:00",
|
||||
dayOfWeek: Number.parseInt(getSetting("backupScheduleDow") ?? "0", 10),
|
||||
nextRunAt: enabled
|
||||
? (getJobSchedules().find((j) => j.jobName === "scheduledBackup")?.nextRunAt ?? null)
|
||||
: null,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { IconCalendarWeek } from "@tabler/icons-react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -38,56 +38,9 @@ interface BackupScheduleState {
|
||||
dow: number;
|
||||
}
|
||||
|
||||
function getNextBackupDate(frequency: BackupFrequency, time: string, dayOfWeek: number): Date {
|
||||
const now = new Date();
|
||||
const [h, m] = time.split(":").map(Number);
|
||||
|
||||
if (frequency === "6h") {
|
||||
const next = new Date(now);
|
||||
const currentHour = next.getHours();
|
||||
const nextHour = Math.ceil((currentHour + 1) / 6) * 6;
|
||||
next.setHours(nextHour, m, 0, 0);
|
||||
if (next <= now) next.setHours(next.getHours() + 6);
|
||||
return next;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function BackupScheduleSection() {
|
||||
const { t } = useLingui();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
t`Sunday`,
|
||||
@@ -118,9 +71,12 @@ export function BackupScheduleSection() {
|
||||
|
||||
const { enabled, maxRetention, frequency, time, dow } = current;
|
||||
|
||||
function formatNextBackup(freq: BackupFrequency, timeOfDay: string, dayOfWeek: number): string {
|
||||
const next = getNextBackupDate(freq, timeOfDay, dayOfWeek);
|
||||
const distance = formatRelativeTime(next);
|
||||
const nextRunAt = scheduleData?.nextRunAt;
|
||||
const timeZone = scheduleData?.timeZone;
|
||||
|
||||
function formatNextBackup(): string | null {
|
||||
if (!nextRunAt) return null;
|
||||
const distance = formatRelativeTime(new Date(nextRunAt));
|
||||
return t`Next backup ${distance}`;
|
||||
}
|
||||
|
||||
@@ -151,6 +107,8 @@ export function BackupScheduleSection() {
|
||||
toast.error(t`Failed to update schedule`);
|
||||
}
|
||||
},
|
||||
onSettled: () =>
|
||||
queryClient.invalidateQueries({ queryKey: orpc.admin.backups.schedule.key() }),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -220,7 +178,8 @@ export function BackupScheduleSection() {
|
||||
<CardDescription>
|
||||
{enabled ? (
|
||||
<span className="inline-flex flex-wrap items-baseline" suppressHydrationWarning>
|
||||
{formatNextBackup(frequency, time, dow)}. <Trans>Keeping</Trans>{" "}
|
||||
{formatNextBackup() && <>{formatNextBackup()}. </>}
|
||||
<Trans>Keeping</Trans>{" "}
|
||||
<Select
|
||||
value={String(maxRetention)}
|
||||
onValueChange={(v) => v && changeMaxRetention(Number(v))}
|
||||
@@ -331,6 +290,12 @@ export function BackupScheduleSection() {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{timeZone && (
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
<Trans>Times use the server's time zone ({timeZone})</Trans>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Time selector — shown for 12h, 1d, 7d */}
|
||||
<AnimatePresence initial={false}>
|
||||
{frequency !== "6h" && (
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { formatFieldErrors } from "./fields";
|
||||
|
||||
describe("formatFieldErrors", () => {
|
||||
test("passes strings through", () => {
|
||||
expect(formatFieldErrors(["Required"])).toBe("Required");
|
||||
});
|
||||
|
||||
test("renders message from issue objects", () => {
|
||||
expect(formatFieldErrors([{ message: "x", path: ["a"] }])).toBe("x");
|
||||
});
|
||||
|
||||
test("joins mixed arrays with a comma", () => {
|
||||
expect(formatFieldErrors(["a", { message: "b" }])).toBe("a, b");
|
||||
});
|
||||
|
||||
test("returns empty string for no errors", () => {
|
||||
expect(formatFieldErrors([])).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -6422,6 +6422,21 @@
|
||||
"dayOfWeek": {
|
||||
"type": "number",
|
||||
"description": "Day of week for weekly backups (0 = Sunday)"
|
||||
},
|
||||
"nextRunAt": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "When the next scheduled backup will run (ISO 8601), or null when disabled"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"description": "IANA time zone the schedule's times are in (the server's)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6429,7 +6444,9 @@
|
||||
"maxRetention",
|
||||
"frequency",
|
||||
"time",
|
||||
"dayOfWeek"
|
||||
"dayOfWeek",
|
||||
"nextRunAt",
|
||||
"timeZone"
|
||||
],
|
||||
"description": "Automated backup schedule configuration"
|
||||
}
|
||||
|
||||
@@ -1062,6 +1062,11 @@ export const BackupScheduleOutput = z
|
||||
frequency: backupFrequency,
|
||||
time: z.string().describe("Scheduled time (HH:MM, 24-hour format)"),
|
||||
dayOfWeek: z.number().describe("Day of week for weekly backups (0 = Sunday)"),
|
||||
nextRunAt: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe("When the next scheduled backup will run (ISO 8601), or null when disabled"),
|
||||
timeZone: z.string().describe("IANA time zone the schedule's times are in (the server's)"),
|
||||
})
|
||||
.meta({ description: "Automated backup schedule configuration" });
|
||||
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "الوقت:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "العنوان"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "Uhrzeit:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr "Time (optional)"
|
||||
msgid "Time:"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr "Times use the server's time zone ({timeZone})"
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "Hora:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "Heure :"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "שעה:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "כותרת"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "Ora:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "時刻:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "タイトル"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "시간:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "제목"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "Tijd:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "Hora:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
@@ -3497,6 +3497,10 @@ msgstr ""
|
||||
msgid "Time:"
|
||||
msgstr "时间:"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Times use the server's time zone ({timeZone})"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/command-palette.tsx
|
||||
msgid "Title"
|
||||
msgstr "标题"
|
||||
|
||||
Reference in new issue
Block a user