Move backups into the settings pages

Admin settings gains a super-admin Backups page at
/admin/settings/backups, with restore nested at .../backups/restore.
Both use SettingsShell and SettingsGroup like the other settings sub
pages instead of the retired admin hero.
This commit is contained in:
2026-10-19 17:37:03 +01:00
parent 53cf054d70
commit b390306166
30 changed files with 183 additions and 165 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
/**
* The admin sections that are live. Any other `/admin/*` path redirects to
* `AdminHomePath`. People and backups exist in code but stay closed until
* they are added back here. Logs (Activity) is super admins only.
* `AdminHomePath`. People exists in code but stays closed until it is added
* back here. Logs (Activity) is super admins only.
*/
export const AdminSections = [
"/admin/bots",
+9
View File
@@ -2,6 +2,7 @@ import {
Bell,
Bot,
CpuAi,
DatabaseBackup,
KeyRound,
Layers,
Monitor,
@@ -32,6 +33,8 @@ export type SettingsPage = {
};
export const SettingsComputersPath = "/admin/settings/computers";
export const SettingsBackupsPath = "/admin/settings/backups";
export const SettingsBackupsRestorePath = `${SettingsBackupsPath}/restore`;
/**
* The `/admin/settings/*` sub pages, listed in the rail on every settings
@@ -77,6 +80,12 @@ export const SettingsPages: SettingsPage[] = [
Icon: Variable,
super_admin_only: true,
},
{
title: "Backups",
url: SettingsBackupsPath,
Icon: DatabaseBackup,
super_admin_only: true,
},
];
export const SettingsDefaultPath = "/admin/settings/general";
@@ -1,19 +0,0 @@
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
import { SiteData } from "@/src/data/site-data";
type Params = {
label: string;
description: string;
};
export default function backupsPageMeta({
label,
description,
}: Params): BunextPageModuleMetaFn {
return async () => {
return {
title: `${label} | ${SiteData["SiteName"]}`,
description,
};
};
}
@@ -1,27 +0,0 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import grabDbBackups from "@/src/functions/backend/backups/grab-db-backups";
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
type Params = Parameters<BunextPageServerFn<PagePropsType>>[0];
/**
* Shared server props for the `/admin/backups` sub-pages. Super admin only.
*/
export default async function grabBackupsServerProps({ req }: Params) {
const user = await grabSuperAdminUser({ req });
if (!user?.id) {
return {
redirect: {
destination: "/admin",
},
};
}
return {
props: {
backups: grabDbBackups(),
},
};
}
@@ -1,32 +0,0 @@
import EmptyContent from "@moduletrace/twui/elements/EmptyContent";
import Section from "@moduletrace/twui/layout/Section";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import BackupsTable, { type BackupsTableMode } from "../(partials)/backups-table";
type Props = {
mode: BackupsTableMode;
};
export default function BackupsListSection({ mode }: Props) {
const { pageProps } = useContext(AppContext);
const backups = pageProps?.backups || [];
return (
<Section>
<Stack className="w-full items-stretch">
<Span size="small" variant="faded">
{`${backups.length} backup${backups.length == 1 ? "" : "s"}`}
</Span>
{backups[0] ? (
<BackupsTable backups={backups} mode={mode} />
) : (
<EmptyContent title="No backups yet" />
)}
</Stack>
</Section>
);
}
@@ -1,12 +0,0 @@
import AdminHero from "@/src/components/general/admin-hero";
import CreateBackupButton from "../(partials)/create-backup-button";
export default function Hero() {
return (
<AdminHero
title="Backups"
description="Snapshots of the database. Older backups are pruned automatically."
buttons={<CreateBackupButton />}
/>
);
}
-17
View File
@@ -1,17 +0,0 @@
import Hero from "./(sections)/hero";
import BackupsListSection from "./(sections)/backups-list-section";
import backupsPageMeta from "./(functions)/backups-page-meta";
export default function AdminBackupsPage() {
return (
<>
<Hero />
<BackupsListSection mode="view" />
</>
);
}
export const meta = backupsPageMeta({
label: "Backups",
description: "Create and download database backups",
});
@@ -1,10 +0,0 @@
import AdminHero from "@/src/components/general/admin-hero";
export default function Hero() {
return (
<AdminHero
title="Restore"
description="Replace all current data with the contents of a saved or uploaded backup. This cannot be undone."
/>
);
}
@@ -1,25 +0,0 @@
import Paper from "@moduletrace/twui/elements/Paper";
import H3 from "@moduletrace/twui/layout/H3";
import Section from "@moduletrace/twui/layout/Section";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import UploadRestoreForm from "../(partials)/upload-restore-form";
export default function UploadRestoreSection() {
return (
<Section className="pb-0!">
<Paper>
<Stack className="w-full items-stretch gap-4">
<Stack className="gap-1!">
<H3>Restore from a file</H3>
<Span size="small" variant="faded">
Upload a SQLite database file. It is saved to the
backups list, then restored.
</Span>
</Stack>
<UploadRestoreForm />
</Stack>
</Paper>
</Section>
);
}
-19
View File
@@ -1,19 +0,0 @@
import Hero from "./(sections)/hero";
import UploadRestoreSection from "./(sections)/upload-restore-section";
import BackupsListSection from "../(sections)/backups-list-section";
import backupsPageMeta from "../(functions)/backups-page-meta";
export default function AdminBackupsRestorePage() {
return (
<>
<Hero />
<UploadRestoreSection />
<BackupsListSection mode="restore" />
</>
);
}
export const meta = backupsPageMeta({
label: "Restore Backup",
description: "Restore the database from a backup",
});
@@ -0,0 +1,35 @@
import grabSuperAdminUser from "@/src/functions/backend/backups/grab-super-admin-user";
import grabDbBackups from "@/src/functions/backend/backups/grab-db-backups";
import { SettingsDefaultPath } from "@/src/dict/settings-pages-dict";
import type { PagePropsType } from "@/src/types";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
import grabSettingsServerProps from "../../(functions)/grab-settings-server-props";
type Params = Parameters<BunextPageServerFn<PagePropsType>>[0];
/**
* Shared server props for the `/admin/settings/backups` sub-pages: the
* saved snapshots. Super admins only; everyone else lands on the general
* settings.
*/
export default async function grabBackupsServerProps(params: Params) {
const settings = await grabSettingsServerProps(params);
if (!settings.props) return settings;
if (!(await grabSuperAdminUser({ req: params.req }))) {
return {
redirect: {
destination: SettingsDefaultPath,
permanent: false,
},
};
}
return {
props: {
...settings.props,
backups: grabDbBackups(),
},
};
}
@@ -0,0 +1,46 @@
import { useContext } from "react";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import SettingsGroup from "../../(partials)/settings-group";
import { AppContext } from "@/src/pages/__root";
import CreateBackupButton from "../(partials)/create-backup-button";
import BackupsTable, { type BackupsTableMode } from "../(partials)/backups-table";
type Props = {
mode: BackupsTableMode;
};
/**
* The database snapshots, newest first. On the restore page each row
* restores; otherwise each one downloads, and a new backup can be made.
*/
export default function BackupsListSection({ mode }: Props) {
const { pageProps } = useContext(AppContext);
const backups = pageProps?.backups || [];
const is_restore = mode == "restore";
return (
<SettingsGroup
title={is_restore ? "Saved snapshots" : "Snapshots"}
description={
is_restore
? "Restore any of these, or upload a file below. Create a fresh backup first if you might need the current data."
: "Older snapshots are pruned automatically. Download one to keep it."
}
>
<Row className="w-full items-center justify-between gap-2! flex-wrap">
<Span size="small" variant="faded">
{`${backups.length} backup${backups.length == 1 ? "" : "s"}`}
</Span>
{is_restore ? null : <CreateBackupButton />}
</Row>
{backups[0] ? (
<BackupsTable backups={backups} mode={mode} />
) : (
<Span size="small" variant="faded">
No backups yet.
</Span>
)}
</SettingsGroup>
);
}
@@ -0,0 +1,28 @@
import Button from "@moduletrace/twui/layout/Button";
import { ArchiveRestore } from "lucide-react";
import SettingsGroup from "../../(partials)/settings-group";
import { SettingsBackupsRestorePath } from "@/src/dict/settings-pages-dict";
/**
* The way into the restore page from the backups list.
*/
export default function RestoreBackupsSection() {
return (
<SettingsGroup
title="Restore"
description="Replace all current data with the contents of a saved or uploaded backup. This cannot be undone."
>
<Button
title="Restore backup"
href={SettingsBackupsRestorePath}
variant="outlined"
color="warning"
size="small"
beforeIcon={<ArchiveRestore size={20} />}
className="self-start"
>
Restore a backup
</Button>
</SettingsGroup>
);
}
@@ -0,0 +1,19 @@
import BackupsListSection from "./(sections)/backups-list-section";
import RestoreBackupsSection from "./(sections)/restore-backups-section";
import SettingsShell from "../(partials)/settings-shell";
import settingsPageMeta from "../(functions)/settings-page-meta";
export default function AdminSettingsBackupsPage() {
return (
<SettingsShell
title="Backups"
description="Snapshots of the database, and restoring one."
wide
>
<BackupsListSection mode="view" />
<RestoreBackupsSection />
</SettingsShell>
);
}
export const meta = settingsPageMeta({ label: "Backups" });
@@ -0,0 +1,16 @@
import SettingsGroup from "../../../(partials)/settings-group";
import UploadRestoreForm from "../(partials)/upload-restore-form";
/**
* Restoring from a file on this machine, for a backup kept elsewhere.
*/
export default function UploadRestoreSection() {
return (
<SettingsGroup
title="From a file"
description="Upload a SQLite database file. It is saved to the backups list, then restored."
>
<UploadRestoreForm />
</SettingsGroup>
);
}
@@ -0,0 +1,26 @@
import UploadRestoreSection from "./(sections)/upload-restore-section";
import BackupsListSection from "../(sections)/backups-list-section";
import SettingsShell from "../../(partials)/settings-shell";
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
import { SiteData } from "@/src/data/site-data";
export default function AdminSettingsRestoreBackupPage() {
return (
<SettingsShell
title="Restore backup"
description="Replace all current data with a saved or uploaded backup."
breadcrumb_title="Restore backup"
wide
>
<UploadRestoreSection />
<BackupsListSection mode="restore" />
</SettingsShell>
);
}
export const meta: BunextPageModuleMetaFn = async () => {
return {
title: `Restore backup | ${SiteData["SiteName"]}`,
description: `Restore a database backup on ${SiteData["SiteName"]}.`,
};
};