"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { useCan } from "@/lib/abilities"; import { OPS_KIND_LABELS, OPS_STATUS_LABELS, formatBytes, formatDateTime, } from "@/lib/labels"; import { backupDownloadUrl, deleteBackup, deleteIngest, getOpsJob, listBackups, listIngest, listOpsJobs, startOpsJob, uploadIngest, } from "@/lib/api"; import type { BackupFile, IngestFile, OpsJob, OpsJobKind, } from "@/lib/types"; const INGEST_MAX_BYTES = 2 * 1024 * 1024 * 1024; export default function OperacionesPage() { return ( ); } type ConfirmState = | { kind: "REIMPORT" } | { kind: "SYNC" } | { kind: "RESTORE"; file: string } | null; function Operaciones() { const allowed = useCan("db:manage"); const [ingest, setIngest] = useState(null); const [backups, setBackups] = useState(null); const [jobs, setJobs] = useState(null); const [activeJob, setActiveJob] = useState(null); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [confirm, setConfirm] = useState(null); const [confirmText, setConfirmText] = useState(""); const [uploading, setUploading] = useState(null); const [starting, setStarting] = useState(false); const fileInputs = useRef>({}); const refreshLists = useCallback(() => { listIngest().then(setIngest).catch(() => setIngest([])); listBackups().then(setBackups).catch(() => setBackups([])); listOpsJobs() .then((rows) => { setJobs(rows); const running = rows.find((j) => j.status === "RUNNING"); if (running) setActiveJob(running); }) .catch(() => setJobs([])); }, []); useEffect(() => { if (allowed) refreshLists(); }, [allowed, refreshLists]); // Poll the active job while it runs; refresh everything when it finishes. useEffect(() => { if (!activeJob || activeJob.status !== "RUNNING") return; const id = activeJob.id; const timer = setInterval(() => { getOpsJob(id) .then((job) => { setActiveJob(job); if (job.status !== "RUNNING") { clearInterval(timer); refreshLists(); setNotice( job.status === "SUCCESS" ? `${OPS_KIND_LABELS[job.kind]} completada.` : `${OPS_KIND_LABELS[job.kind]} terminó con error. Revise el registro.`, ); } }) .catch(() => { /* transient — keep polling */ }); }, 1500); return () => clearInterval(timer); }, [activeJob, refreshLists]); if (!allowed) { return (

Operaciones

No tiene permisos para administrar la base de datos.
); } const jobRunning = activeJob?.status === "RUNNING"; async function handleUpload(name: string, file: File | undefined) { if (!file) return; setError(null); setNotice(null); setUploading(name); try { await uploadIngest(name, file); setNotice(`${name} cargado.`); refreshLists(); } catch (e) { setError((e as Error)?.message ?? "No se pudo cargar el archivo."); } finally { setUploading(null); const input = fileInputs.current[name]; if (input) input.value = ""; } } async function handleDeleteIngest(name: string) { setError(null); try { await deleteIngest(name); refreshLists(); } catch (e) { setError((e as Error)?.message ?? "No se pudo eliminar."); } } async function handleDeleteBackup(name: string) { setError(null); try { await deleteBackup(name); refreshLists(); } catch (e) { setError((e as Error)?.message ?? "No se pudo eliminar el respaldo."); } } async function start(kind: OpsJobKind, file?: string) { setError(null); setNotice(null); setStarting(true); try { const job = await startOpsJob(kind, file); setActiveJob(job); setJobs((prev) => (prev ? [job, ...prev] : [job])); } catch (e) { setError((e as Error)?.message ?? "No se pudo iniciar la operación."); } finally { setStarting(false); } } function askConfirm(state: ConfirmState) { setConfirm(state); setConfirmText(""); setError(null); setNotice(null); } async function runConfirmed() { if (!confirm) return; const c = confirm; setConfirm(null); if (c.kind === "REIMPORT") await start("REIMPORT"); else if (c.kind === "SYNC") await start("SYNC"); else await start("RESTORE", c.file); } const ingestReady = (ingest ?? []).every((f) => f.present); return ( <>

Operaciones de base de datos

{error &&
{error}
} {notice &&
{notice}
} {/* Active / running job with live log */} {activeJob && (

{OPS_KIND_LABELS[activeJob.kind]}{" "} {jobRunning && } {OPS_STATUS_LABELS[activeJob.status]}

{!jobRunning && ( )}
{activeJob.log || "Iniciando…"}
)} {/* Ingest folder */}

Carpeta de ingesta

Los cuatro archivos originales de Access. La reimportación y la sincronización leen de aquí. Tamaño máximo por archivo: {formatBytes(INGEST_MAX_BYTES)}.

{(ingest ?? []).map((f) => ( ))}
Archivo Estado Tamaño Modificado Acciones
{f.name} {f.present ? "Presente" : "Falta"} {formatBytes(f.size)} {formatDateTime(f.modifiedAt)}
{ fileInputs.current[f.name] = el; }} type="file" style={{ display: "none" }} onChange={(e) => handleUpload(f.name, e.target.files?.[0])} /> {f.present && ( )}
{/* Operations */}

Operaciones

start("BACKUP")} /> askConfirm({ kind: "REIMPORT" })} /> askConfirm({ kind: "SYNC" })} />
{!ingestReady && (

La reimportación requiere que los cuatro archivos estén presentes.

)}
{/* Backups */}

Respaldos

Restaurar sobreescribe la base de datos completa con el respaldo elegido.

{backups === null ? ( ) : backups.length === 0 ? ( ) : ( backups.map((b) => ( )) )}
Archivo Tamaño Creado Acciones
Sin respaldos.
{b.name} {formatBytes(b.size)} {formatDateTime(b.createdAt)}
Descargar
{/* Recent jobs */}

Historial

{(jobs ?? []).map((j) => ( ))} {jobs && jobs.length === 0 && ( )}
Operación Estado Inicio Fin
{OPS_KIND_LABELS[j.kind]} {OPS_STATUS_LABELS[j.status]} {formatDateTime(j.startedAt)} {formatDateTime(j.finishedAt)}
Sin operaciones registradas.
{/* Destructive-op confirm */} {confirm && (

{confirm.kind === "REIMPORT" ? "Confirmar reimportación" : confirm.kind === "SYNC" ? "Confirmar sincronización" : "Confirmar restauración"}

{confirm.kind === "REIMPORT" ? "Esto BORRA todos los datos actuales (incluidos los capturados a mano) y reconstruye desde los archivos de ingesta. Se creará un respaldo previo automático." : confirm.kind === "SYNC" ? "Se creará un respaldo previo automático. Luego se importarán al sistema los registros nuevos del legado y se eliminarán los del legado que ya no aparezcan en los archivos de ingesta. Los datos capturados a mano NO se borran." : `Esto sobreescribe la base de datos completa con “${confirm.file}”. Se recomienda crear un respaldo antes.`}

)} ); } function OpTile({ title, desc, action, tone, disabled, onClick, }: { title: string; desc: string; action: string; tone: "primary" | "danger" | "muted"; disabled: boolean; onClick: () => void; }) { const btnClass = tone === "danger" ? "btn btn-danger" : tone === "muted" ? "btn btn-outline" : "btn btn-primary"; return (

{title}

{desc}

); }