Move database backup policy and scheduling to shared global configuration. - Split project memory backups from cluster-wide database backup settings. - Migrate legacy backup values and routines safely into central global storage. - Schedule and dispatch one shared PostgreSQL backup routine across project engines. Files changed: .changeset/fn-8129-backup-settings-scope-split.md | 7 + docs/dashboard-guide.md | 2 + docs/settings-reference.md | 10 +- packages/cli/src/commands/backup.ts | 3 +- .../__tests__/backup-settings-migration.test.ts | 50 ++++++ .../src/__tests__/backup-settings-scope.test.ts | 27 +++ packages/core/src/backup-settings-migration.ts | 188 +++++++++++++++++++++ packages/core/src/backup.ts | 77 +++++---- packages/core/src/global-routine-store.ts | 104 ++++++++++++ packages/core/src/index.gate.ts | 6 +- packages/core/src/index.ts | 6 +- .../core/src/postgres/migrations/0000_initial.sql | 19 +++ .../postgres/migrations/0015_global_routines.sql | 19 +++ packages/core/src/postgres/schema-applier.ts | 19 ++- packages/core/src/postgres/schema/central.ts | 21 ++- packages/core/src/postgres/startup-factory.ts | 11 ++ packages/core/src/settings-schema.ts | 14 +- packages/core/src/types.ts | 31 +++- .../dashboard/app/components/SettingsModal.tsx | 10 +- .../settings/__tests__/section-keys.test.ts | 1 + .../app/components/settings/save-split.ts | 2 + .../search/__tests__/settings-search-index.test.ts | 1 + .../settings/search/entries.ts | 2 + .../app/components/settings/section-keys.ts | 4 - .../settings/sections/BackupsSection.search.ts | 40 ----- .../settings/sections/BackupsSection.tsx | 112 +----------- .../sections/DatabaseBackupsSection.search.ts | 51 ++++++ .../settings/sections/DatabaseBackupsSection.tsx | 142 ++++++++++++++++ .../settings-default-descriptions.test.tsx | 1 + packages/dashboard/src/routes.ts | 12 +- .../src/routes/register-settings-memory-routes.ts | 41 ++--- .../engine/src/__tests__/routine-scheduler.test.ts | 55 +++++- packages/engine/src/cron-runner.ts | 4 +- packages/engine/src/routine-runner.ts | 67 +++++--- packages/engine/src/routine-scheduler.ts | 35 +++- 35 files changed, 929 insertions(+), 265 deletions(-) Fusion-Task-Id: FN-8129 Fusion-Task-Lineage: af17f39a-7f1c-40ff-8a4a-cd63895cd532 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
90 lines
4.8 KiB
TypeScript
90 lines
4.8 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { SettingsToggleRow } from "../SettingsToggleRow";
|
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
|
import { SettingsNumberRow } from "../SettingsNumberRow";
|
|
import { SettingsTextRow } from "../SettingsTextRow";
|
|
import type { SectionBaseProps } from "./context";
|
|
/*
|
|
FNXC:SettingsBackups 2026-07-16-14:35:
|
|
Memory backup files are project-owned, so only memory settings remain in this project-scoped section after database backup policy moved to the global cluster section.
|
|
*/
|
|
export function BackupsSection({ form, setForm }: SectionBaseProps) {
|
|
const { t } = useTranslation("app");
|
|
return (<>
|
|
<h4 className="settings-section-heading">{t("settings.backups.memoryBackups", "Memory Backups")}</h4>
|
|
<SettingsToggleRow
|
|
descriptor={{
|
|
key: "memoryBackupEnabled",
|
|
label: t("settings.backups.enableAutomaticMemoryBackups", " Enable automatic memory backups "),
|
|
help: t("settings.backups.whenEnabledProjectAndAgentMemoryFilesAre", "When enabled, project and agent memory files are backed up automatically on a schedule. Default: disabled."),
|
|
scope: "project",
|
|
}}
|
|
value={form.memoryBackupEnabled || false}
|
|
onChange={(v) => setForm((f) => ({ ...f, memoryBackupEnabled: v === true }))}
|
|
/>
|
|
<SettingsTextRow
|
|
descriptor={{
|
|
key: "memoryBackupSchedule",
|
|
label: t("settings.backups.memoryBackupScheduleCron", "Memory Backup Schedule (Cron)"),
|
|
help: t("settings.backups.cronExpressionForMemoryBackupTimingDefault0", "Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM)."),
|
|
scope: "project",
|
|
placeholder: t("settings.backups.03", "0 3 * * *"),
|
|
disabled: !form.memoryBackupEnabled,
|
|
}}
|
|
value={form.memoryBackupSchedule || "0 3 * * *"}
|
|
onChange={(v) => setForm((f) => ({ ...f, memoryBackupSchedule: v ?? "" }))}
|
|
error={form.memoryBackupSchedule && !/^[\s\d*,/-]+$/.test(form.memoryBackupSchedule)
|
|
? t("settings.backups.invalidCronExpressionFormat", "Invalid cron expression format")
|
|
: undefined}
|
|
/>
|
|
<SettingsNumberRow
|
|
descriptor={{
|
|
key: "memoryBackupRetention",
|
|
label: t("settings.backups.memoryRetentionCount", "Memory Retention Count"),
|
|
help: t("settings.backups.numberOfMemoryBackupsToKeepOldestAre", "Number of memory backups to keep (oldest are deleted first). Range: 1-100. Default: 14."),
|
|
scope: "project",
|
|
min: 1,
|
|
max: 100,
|
|
disabled: !form.memoryBackupEnabled,
|
|
}}
|
|
value={form.memoryBackupRetention ?? null}
|
|
onChange={(v) => setForm((f) => ({ ...f, memoryBackupRetention: v ?? undefined }))}
|
|
error={form.memoryBackupRetention !== undefined && (form.memoryBackupRetention < 1 || form.memoryBackupRetention > 100)
|
|
? t("settings.backups.mustBeBetween1And100", "Must be between 1 and 100")
|
|
: undefined}
|
|
/>
|
|
<SettingsTextRow
|
|
descriptor={{
|
|
key: "memoryBackupDir",
|
|
label: t("settings.backups.memoryBackupDirectory", "Memory Backup Directory"),
|
|
help: t("settings.backups.directoryForMemoryBackupsRelativeToProjectRoot", "Directory for memory backups, relative to project root. Default: .fusion/backups/memory."),
|
|
scope: "project",
|
|
placeholder: t("settings.backups.fusionBackupsMemory", ".fusion/backups/memory"),
|
|
disabled: !form.memoryBackupEnabled,
|
|
}}
|
|
value={form.memoryBackupDir || ".fusion/backups/memory"}
|
|
onChange={(v) => setForm((f) => ({ ...f, memoryBackupDir: v ?? "" }))}
|
|
error={form.memoryBackupDir && form.memoryBackupDir.includes("..")
|
|
? t("settings.backups.pathCannotContainParentDirectoryTraversal", "Path cannot contain parent directory traversal (..)")
|
|
: undefined}
|
|
/>
|
|
<SettingsSelectRow
|
|
descriptor={{
|
|
key: "memoryBackupScope",
|
|
label: t("settings.backups.memoryBackupScope", "Memory Backup Scope"),
|
|
help: t("settings.backups.memoryBackupScopeHint", "Default: all (project + agents)."),
|
|
scope: "project",
|
|
disabled: !form.memoryBackupEnabled,
|
|
options: [
|
|
{ value: "all", label: t("settings.backups.allProjectAgents", "All (project + agents)") },
|
|
{ value: "project", label: t("settings.backups.projectOnlyFusionMemory", "Project only (.fusion/memory)") },
|
|
{ value: "agents", label: t("settings.backups.agentsOnlyFusionAgentMemory", "Agents only (.fusion/agent-memory)") },
|
|
],
|
|
}}
|
|
value={form.memoryBackupScope || "all"}
|
|
onChange={(v) => setForm((f) => ({ ...f, memoryBackupScope: (v ?? "all") as "project" | "agents" | "all" }))}
|
|
/>
|
|
</>);
|
|
}
|
|
export default BackupsSection;
|