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:
@@ -54,7 +54,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
|
|||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
- **Auth guard:** `src/pages/__root.server.ts` - server function that runs on every request.
|
- **Auth guard:** `src/pages/__root.server.ts` - server function that runs on every request.
|
||||||
- No public pages: anything outside the live admin sections (`AdminSections` in `src/dict/admin-sections-dict.ts`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`) and `/auth` redirects to `AdminHomePath` (`/admin/bots`). `/admin` itself only redirects there. People and backups exist in code but are closed until added back to `AdminSections`
|
- No public pages: anything outside the live admin sections (`AdminSections` in `src/dict/admin-sections-dict.ts`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`) and `/auth` redirects to `AdminHomePath` (`/admin/bots`). `/admin` itself only redirects there. People exists in code but is closed until added back to `AdminSections`
|
||||||
- No users at all: everything redirects to `/auth/signup` (first-run super admin setup, `grabHasUsers`); once a user exists `/auth/signup` redirects away
|
- No users at all: everything redirects to `/auth/signup` (first-run super admin setup, `grabHasUsers`); once a user exists `/auth/signup` redirects away
|
||||||
- Redirects unauthenticated `/admin/*` → `/auth/login`
|
- Redirects unauthenticated `/admin/*` → `/auth/login`
|
||||||
- Unknown `/admin/settings/*` pages (not in `SettingsPages`, `src/utils/is-unknown-settings-path.ts`) redirect to `/admin/settings/general`
|
- Unknown `/admin/settings/*` pages (not in `SettingsPages`, `src/utils/is-unknown-settings-path.ts`) redirect to `/admin/settings/general`
|
||||||
@@ -158,7 +158,7 @@ Live routes under `/admin` (see `AdminSections`): `/admin/bots`, `/admin/workspa
|
|||||||
- `/admin/logs` (live) - Activity, the audit log: super admins only (others are redirected; the popover's "Activity" item shows to super admins only). Rendered in `SettingsShell` with `wide`
|
- `/admin/logs` (live) - Activity, the audit log: super admins only (others are redirected; the popover's "Activity" item shows to super admins only). Rendered in `SettingsShell` with `wide`
|
||||||
- `/admin/help` (live) - familiar problems and their fixes, one `SettingsGroup` per topic (`(data)/help-topics.ts`, markdown rendered with the chat's `MessageMarkdownComponents`, the topic id as the group's anchor). First topic: signing in to Google and other strict sites with Cookie-Editor over the Desktop view (`HelpSignInTopicId` in `src/dict/help-dict.ts`; `ask_user_to_take_over` points bots to it)
|
- `/admin/help` (live) - familiar problems and their fixes, one `SettingsGroup` per topic (`(data)/help-topics.ts`, markdown rendered with the chat's `MessageMarkdownComponents`, the topic id as the group's anchor). First topic: signing in to Google and other strict sites with Cookie-Editor over the Desktop view (`HelpSignInTopicId` in `src/dict/help-dict.ts`; `ask_user_to_take_over` points bots to it)
|
||||||
- Routines, Activity and Help are the `RailPages` (`src/dict/rail-pages-dict.ts`), opened from the rail's user popover (which lists them, then Settings and Sign out). On them the rail shows its own aside (`(partials)/rail-pages-list.tsx`): a back link to the bots, then those pages, like settings
|
- Routines, Activity and Help are the `RailPages` (`src/dict/rail-pages-dict.ts`), opened from the rail's user popover (which lists them, then Settings and Sign out). On them the rail shows its own aside (`(partials)/rail-pages-list.tsx`): a back link to the bots, then those pages, like settings
|
||||||
- `/admin/backups` - super admin only: create/list/download database snapshots in `db/.backups`, plus `restore/` (restore a listed backup, or upload a SQLite database file which is saved to `.backups` then restored). Shells out to the `bun-sqlite` CLI (`backup`, and `import <file>` since `restore` is interactive) via `src/functions/backend/backups/`; API routes in `api/admin/backups/`
|
- `/admin/settings/backups` - super admin only (listed in `SettingsPages`): create/list/download database snapshots in `db/.backups`, plus `restore/` (restore a listed backup, or upload a SQLite database file which is saved to `.backups` then restored). Shells out to the `bun-sqlite` CLI (`backup`, and `import <file>` since `restore` is interactive) via `src/functions/backend/backups/`; API routes in `api/admin/backups/`
|
||||||
- `/admin/settings` (live) - redirects to `SettingsDefaultPath` (`/admin/settings/general`). Sub pages: `general/` (theme: System/Light/Dark per device via `useThemePreference`, stored in `localStorage[AppData.ThemeStorageKey]`, the same key `ThemeInitScript` and twui's `ColorSchemeSelector` read; applies instantly, no save bar), `profile/` (current user profile), `notifications/` (this device's Web Push switch, a test send, and every device the user subscribed), `change-password/` (set/change own password) `models/` (super admin only, `super_admin_only` in the dict hides it from the rail: model providers and which model each harness uses) and `environment/` (super admin only: app settings and the envs bots and computers use, per scope), each wrapped in `SettingsShell` (`(partials)/settings-shell.tsx`: 64px pane header like the conversation header, then one centered column). Content is built from `SettingsGroup` blocks (label + description left, controls right, hairline-separated, no cards) and ends with the sticky `SettingsSaveBar` (Save enabled only when dirty, Discard, one-line status). Saves don't reload the page. Keep save notices in component state: `useFormInit` clears `status` whenever the form changes. `bots/` (the crew: chat, edit with `BotForm`, models, delete a worker via `api/admin/bots/delete-bot`), `workspaces/` (open, create through the Add flow, delete), `computers/` (state and resources, live; super admins create, edit with `AddComputerForm`, retry, delete), `mcps/` and `tools/` (see MCPs and custom tools: super admins add, edit, refresh or try, delete; admins only set Access, `extension-access-modal.tsx`; add, edit, access and try open in modals (`AddModalShell`)), each row with a status line under it (`extension-status-line.tsx`: connected or not, last run worked or failed, when, and why), those five for `canManageCrew` only (`grab-crew-settings-server-props.ts`, `can_view` in the dict) and `users/` (for `canListUsers`: super admins and admins; the dict's `can_view` gates it in the rail; add with a password (`canCreateUser`), edit (`canUpdateUser`), both with the one `UserForm` (`admin/people/create/(partials)/user-form/`, `useFormInit`, photo included, saved in one request), revoke (adds `revoked`, `canRevokeUser`), delete (`canRevokeUser`'s rules, never yourself), pick their user types from `UserTypes` (`user-types-select.tsx`: only the ones `filterAssignableUserTypes` allows, never `UneditableUserTypes`; at least one, since login needs a type; `saveUser` checks each grant and removal with `userTypeAssignment`; the super admin's types are never shown or changed there, and they can't be revoked; the password is entered twice, `SaveUserRequestSchema` checks they match), delete with everything theirs; `api/admin/users/save-user`, `delete-user`). The lists share `SettingsList`, `SettingsListRow`, `SettingsRowAction` and `use-settings-list`. Sub pages are declared once in `src/dict/settings-pages-dict.ts`; on every `/admin/settings/*` page the rail swaps the crew for a back link plus that list. Add a sub page by adding its folder and a dict entry
|
- `/admin/settings` (live) - redirects to `SettingsDefaultPath` (`/admin/settings/general`). Sub pages: `general/` (theme: System/Light/Dark per device via `useThemePreference`, stored in `localStorage[AppData.ThemeStorageKey]`, the same key `ThemeInitScript` and twui's `ColorSchemeSelector` read; applies instantly, no save bar), `profile/` (current user profile), `notifications/` (this device's Web Push switch, a test send, and every device the user subscribed), `change-password/` (set/change own password) `models/` (super admin only, `super_admin_only` in the dict hides it from the rail: model providers and which model each harness uses) and `environment/` (super admin only: app settings and the envs bots and computers use, per scope), each wrapped in `SettingsShell` (`(partials)/settings-shell.tsx`: 64px pane header like the conversation header, then one centered column). Content is built from `SettingsGroup` blocks (label + description left, controls right, hairline-separated, no cards) and ends with the sticky `SettingsSaveBar` (Save enabled only when dirty, Discard, one-line status). Saves don't reload the page. Keep save notices in component state: `useFormInit` clears `status` whenever the form changes. `bots/` (the crew: chat, edit with `BotForm`, models, delete a worker via `api/admin/bots/delete-bot`), `workspaces/` (open, create through the Add flow, delete), `computers/` (state and resources, live; super admins create, edit with `AddComputerForm`, retry, delete), `mcps/` and `tools/` (see MCPs and custom tools: super admins add, edit, refresh or try, delete; admins only set Access, `extension-access-modal.tsx`; add, edit, access and try open in modals (`AddModalShell`)), each row with a status line under it (`extension-status-line.tsx`: connected or not, last run worked or failed, when, and why), those five for `canManageCrew` only (`grab-crew-settings-server-props.ts`, `can_view` in the dict) and `users/` (for `canListUsers`: super admins and admins; the dict's `can_view` gates it in the rail; add with a password (`canCreateUser`), edit (`canUpdateUser`), both with the one `UserForm` (`admin/people/create/(partials)/user-form/`, `useFormInit`, photo included, saved in one request), revoke (adds `revoked`, `canRevokeUser`), delete (`canRevokeUser`'s rules, never yourself), pick their user types from `UserTypes` (`user-types-select.tsx`: only the ones `filterAssignableUserTypes` allows, never `UneditableUserTypes`; at least one, since login needs a type; `saveUser` checks each grant and removal with `userTypeAssignment`; the super admin's types are never shown or changed there, and they can't be revoked; the password is entered twice, `SaveUserRequestSchema` checks they match), delete with everything theirs; `api/admin/users/save-user`, `delete-user`). The lists share `SettingsList`, `SettingsListRow`, `SettingsRowAction` and `use-settings-list`. Sub pages are declared once in `src/dict/settings-pages-dict.ts`; on every `/admin/settings/*` page the rail swaps the crew for a back link plus that list. Add a sub page by adding its folder and a dict entry
|
||||||
|
|
||||||
Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout or aside. The rail header's Add icon (super admins and admins only, like the rail's "+" buttons) opens the Add flow (`(partials)/add-flow.tsx`, `AddFlowContext`): a bot, a workspace or (super admins) a computer, each form in a modal stacked over the choice, so closing it goes back there (the Bots and Workspaces labels have a "+" that opens the form directly); on a workspace's pages new bots and computers join it. The rail shows the crew (`(partials)/rail-crew-list.tsx`, needs `bots` and `workspaces` in page props): the coordinator, the latest `AppData.RailRecentBots` workers (plus any open, unread or needing you, `pick-rail-bots`), "See all bots" with a few stones opening every bot in a modal, then Workspaces with a "+". On `/admin/workspaces/*` it shows the workspace's crew instead (`(partials)/rail-workspace-list.tsx`: back link, the workspace, then only the bots attached to it, "Workspace bots"; the coordinator stays in the main crew rail, and on a workspace bot's page the back link returns to the workspace page), except on `/admin/settings/*` where it shows "Back to bots" plus the settings sub pages (`(partials)/rail-settings-list.tsx`). Page heroes use the shared `AdminHero` component (`src/components/general/admin-hero.tsx`).
|
Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout or aside. The rail header's Add icon (super admins and admins only, like the rail's "+" buttons) opens the Add flow (`(partials)/add-flow.tsx`, `AddFlowContext`): a bot, a workspace or (super admins) a computer, each form in a modal stacked over the choice, so closing it goes back there (the Bots and Workspaces labels have a "+" that opens the form directly); on a workspace's pages new bots and computers join it. The rail shows the crew (`(partials)/rail-crew-list.tsx`, needs `bots` and `workspaces` in page props): the coordinator, the latest `AppData.RailRecentBots` workers (plus any open, unread or needing you, `pick-rail-bots`), "See all bots" with a few stones opening every bot in a modal, then Workspaces with a "+". On `/admin/workspaces/*` it shows the workspace's crew instead (`(partials)/rail-workspace-list.tsx`: back link, the workspace, then only the bots attached to it, "Workspace bots"; the coordinator stays in the main crew rail, and on a workspace bot's page the back link returns to the workspace page), except on `/admin/settings/*` where it shows "Back to bots" plus the settings sub pages (`(partials)/rail-settings-list.tsx`). Page heroes use the shared `AdminHero` component (`src/components/general/admin-hero.tsx`).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* The admin sections that are live. Any other `/admin/*` path redirects to
|
* The admin sections that are live. Any other `/admin/*` path redirects to
|
||||||
* `AdminHomePath`. People and backups exist in code but stay closed until
|
* `AdminHomePath`. People exists in code but stays closed until it is added
|
||||||
* they are added back here. Logs (Activity) is super admins only.
|
* back here. Logs (Activity) is super admins only.
|
||||||
*/
|
*/
|
||||||
export const AdminSections = [
|
export const AdminSections = [
|
||||||
"/admin/bots",
|
"/admin/bots",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Bell,
|
Bell,
|
||||||
Bot,
|
Bot,
|
||||||
CpuAi,
|
CpuAi,
|
||||||
|
DatabaseBackup,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Layers,
|
Layers,
|
||||||
Monitor,
|
Monitor,
|
||||||
@@ -32,6 +33,8 @@ export type SettingsPage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsComputersPath = "/admin/settings/computers";
|
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
|
* The `/admin/settings/*` sub pages, listed in the rail on every settings
|
||||||
@@ -77,6 +80,12 @@ export const SettingsPages: SettingsPage[] = [
|
|||||||
Icon: Variable,
|
Icon: Variable,
|
||||||
super_admin_only: true,
|
super_admin_only: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Backups",
|
||||||
|
url: SettingsBackupsPath,
|
||||||
|
Icon: DatabaseBackup,
|
||||||
|
super_admin_only: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SettingsDefaultPath = "/admin/settings/general";
|
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 />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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"]}.`,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user