"use client"; import Link from "next/link"; import { useRef, useState, useTransition } from "react"; import { JobProgress } from "../job-progress"; import { PassphraseNotice } from "../passphrase-notice"; import { inspectRestoreAction, startRestoreAction, type RestorePreview } from "./actions"; interface BackupOption { key: string; slug: string; fileName: string; createdAt: string; kind: string; sizeBytes: number; } function formatBytes(bytes: number): string { if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`; } export function RestoreWizard({ backups, preselected, hasStoredPassphrase, maxUploadBytes, }: { backups: BackupOption[]; preselected: string | null; hasStoredPassphrase: boolean; maxUploadBytes: number; }) { const [mode, setMode] = useState<"stored" | "upload">(backups.length === 0 ? "upload" : "stored"); const [selected, setSelected] = useState(preselected ?? backups[0]?.key ?? ""); const [uploadKey, setUploadKey] = useState(null); const [uploadProgress, setUploadProgress] = useState(null); const [uploadName, setUploadName] = useState(null); const [passphrase, setPassphrase] = useState(""); const [preview, setPreview] = useState(null); const [error, setError] = useState(null); const [replace, setReplace] = useState(false); const [confirmSlug, setConfirmSlug] = useState(""); const [jobId, setJobId] = useState(null); const [restoredSlug, setRestoredSlug] = useState(null); const [newPassphrase, setNewPassphrase] = useState(null); const [isPending, startTransition] = useTransition(); const xhrRef = useRef(null); const sourceKey = mode === "upload" ? uploadKey : selected; const resetPreview = () => { setPreview(null); setError(null); }; function upload(file: File) { resetPreview(); setUploadKey(null); if (file.size > maxUploadBytes) { setError(`That file is ${formatBytes(file.size)}, over the ${formatBytes(maxUploadBytes)} upload limit. Copy it into the server's backups folder instead.`); return; } setUploadName(file.name); setUploadProgress(0); const xhr = new XMLHttpRequest(); xhrRef.current = xhr; xhr.open("POST", "/api/backups/upload"); xhr.setRequestHeader("Content-Type", "application/octet-stream"); xhr.upload.onprogress = (event) => { if (event.lengthComputable) setUploadProgress(Math.round((event.loaded / event.total) * 100)); }; xhr.onload = () => { setUploadProgress(null); try { const body = JSON.parse(xhr.responseText) as { uploadId?: string; error?: string }; if (xhr.status === 200 && body.uploadId) setUploadKey(`upload:${body.uploadId}`); else setError(body.error ?? "Upload failed."); } catch { setError("Upload failed."); } }; xhr.onerror = () => { setUploadProgress(null); setError("Upload failed — the connection dropped."); }; xhr.send(file); } function inspect() { if (!sourceKey) return; resetPreview(); startTransition(async () => { const result = await inspectRestoreAction(sourceKey, passphrase); if (result.error) setError(result.error); setPreview(result.preview); }); } function start() { if (!sourceKey || !preview) return; setError(null); startTransition(async () => { const result = await startRestoreAction(sourceKey, passphrase, replace, confirmSlug); if (result.error) { setError(result.error); return; } setNewPassphrase(result.newPassphrase); setRestoredSlug(preview.slug); setJobId(result.jobId); }); } const blocked = preview ? Boolean(preview.domainConflict || preview.schemaProblem) : false; const replaceConfirmed = !preview?.slugExists || (replace && confirmSlug === preview.slug); if (jobId) { return (
{newPassphrase && } {restoredSlug && ( Open {restoredSlug} )}
); } return (
1. Choose the backup
{mode === "stored" ? ( ) : (
{ const file = e.target.files?.[0]; if (file) upload(file); }} className="block w-full text-sm" />

Up to {formatBytes(maxUploadBytes)}. Streamed to the server, not held in memory.

{uploadProgress !== null && (
)} {uploadKey &&

Uploaded {uploadName}.

}
)}
{ setPassphrase(e.target.value); resetPreview(); }} placeholder={hasStoredPassphrase ? "Leave blank to use this server's stored passphrase" : "Required on a fresh server"} className="w-full rounded-md border border-line px-3 py-2 font-mono text-sm" />
{error && (

{error}

)} {preview && (

3. Review and confirm

Business
{preview.name ?? preview.slug}
Slug
{preview.slug}
Domain
{preview.domain}
Backup taken
{new Date(preview.createdAt).toLocaleString()}
Schema
{preview.lastMigration ?? "unknown"}
{preview.domainConflict && (

{preview.domainConflict}

)} {preview.schemaProblem && (

{preview.schemaProblem}

)} {preview.slugExists ? (

“{preview.slug}” already exists on this server. Restoring REPLACES its database, media and configuration.

A safety backup of the current state is taken first.

setConfirmSlug(e.target.value)} placeholder={`Type ${preview.slug} to confirm`} autoComplete="off" spellCheck={false} className="w-full rounded-md border border-line px-3 py-2 font-mono text-sm" />
) : (

No business named “{preview.slug}” exists here — it will be created from the backup.

)}
)}
); }