"use client"; /** * `` — admin view of the weekly `DataBackup` snapshots. * * Lists every snapshot row (one per user × backup-type) with size and age, * plus a "Run backup now" CTA that enqueues the pg-boss `data-backup` job. * The encrypted payload itself is never shipped to the browser; only the * size in bytes is surfaced. */ import { useQuery } from "@tanstack/react-query"; import { BookOpen, Database, Loader2, PlayCircle } from "lucide-react"; import { BackupPassActivityNotice } from "@/components/admin/backup-pass-activity"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useRecentProof } from "@/components/settings/security-section/use-recent-proof"; import { SettingsCard } from "@/components/settings/settings-card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { QueryErrorRow } from "@/components/ui/query-error-row"; import { apiGet } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import type { BackupRow, BackupsList } from "@/types/backups"; import { RestoreJobStatus, visibleRestoreJobs } from "./backup-restore-status"; import { BackupRowsCards, BackupRowsTable } from "./backups/backup-rows"; import { DatabaseDumpRow } from "./backups/database-dump-row"; import { BackupUpload } from "./backups/backup-upload-card"; import { useBackupUpload } from "./backups/use-backup-upload"; import { RestoreRefusalNotice, RestoreSkipReport, ScheduleHealthNotice, } from "./backups/restore-notices"; import { useBackupDownload, useRunBackup } from "./backups/use-backup-actions"; import { useBackupRestores } from "./backups/use-backup-restores"; export function BackupsSection() { const { t } = useTranslations(); const { data, isLoading, isError, error, refetch } = useQuery({ queryKey: queryKeys.adminBackups(), queryFn: async () => { return apiGet("/api/admin/backups"); }, }); const runBackup = useRunBackup(); const rows: BackupRow[] = data?.rows ?? []; // Download, upload and restore reach every account's data, so each asks for // a fresh proof unless this session signed in or re-proved within minutes. const recentProof = useRecentProof(); const { downloadingId, handleDownload } = useBackupDownload(recentProof); const uploadFlow = useBackupUpload(recentProof); const { restoreJobs, dismissedRestores, dismissRestore, restore, restorePending, skipped, setSkipped, refusedSections, setRefusedSections, } = useBackupRestores(recentProof); const rowActions = { downloadingId, onDownload: (row: BackupRow) => void handleDownload(row), restorePending, onRestore: ( row: BackupRow, options: { restoreInstanceSettings: boolean }, ) => restore.mutate({ row, ...options }), }; return ( {recentProof.dialog} {/* The snapshot count leads (numbers first) and the "Run now" button follows. */} {rows.length} ) : null } description={

{t("admin.section.backups.description")}{" "} {/* External docs link — `noopener noreferrer` because this leaves the admin shell. */}

} /> {data?.database ? ( ) : null} {data?.schedule ? ( ) : null} {data?.scheduleActivity ? ( ) : null} {visibleRestoreJobs(restoreJobs, dismissedRestores).map((job) => ( dismissRestore(job.id)} /> ))} setRefusedSections([])} /> {skipped ? ( setSkipped(null)} /> ) : null} {isLoading ? (
{t("admin.section.backups.loading")}
) : isError ? ( void refetch()} /> ) : rows.length === 0 ? ( // Replace bare text with the EmptyState // primitive. The header already exposes "Backup now" but a // brand-new admin lands inside the card and benefits from a // duplicate CTA right next to the explanation. // // Its own test id, because both buttons are in the tree while the list // is empty and they do the same thing: one id on two elements forces // every caller to pick one blind, and a third button would join the // pick without anything failing.
} title={t("admin.section.backups.emptyTitle")} description={t("admin.section.backups.emptyDescription")} action={ } />
) : (

{data!.preWipeCopyDays > 0 ? t("admin.section.backups.retentionNote", { days: data!.preWipeCopyDays, }) : t("admin.section.backups.retentionNoteNoGrace")}

)}
); }