This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft, Server, Activity, Database, HardDrive,
|
||||
DownloadCloud, ShieldAlert, UploadCloud, Loader2, CheckCircle2
|
||||
} from "lucide-react";
|
||||
import { getSystemMetrics, exportDatabase, restoreDatabase } from "./actions";
|
||||
|
||||
export default function SystemHealth() {
|
||||
const [metrics, setMetrics] = useState<any>(null);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
// Restore State
|
||||
const [isRestoring, setIsRestoring] = useState(false);
|
||||
const [restoreError, setRestoreError] = useState("");
|
||||
const [restoreSuccess, setRestoreSuccess] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
const res = await getSystemMetrics();
|
||||
if (res.success) setMetrics(res);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMetrics();
|
||||
// Actualizar métricas cada 10 segundos
|
||||
const interval = setInterval(fetchMetrics, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const d = Math.floor(seconds / (3600 * 24));
|
||||
const h = Math.floor(seconds % (3600 * 24) / 3600);
|
||||
const m = Math.floor(seconds % 3600 / 60);
|
||||
return `${d}d ${h}h ${m}m`;
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
const res = await exportDatabase();
|
||||
|
||||
if (res.success && res.data) {
|
||||
// Crear un Blob y forzar la descarga del JSON
|
||||
const blob = new Blob([res.data], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `flux-db-snapshot-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
alert(res.error || "Export failed.");
|
||||
}
|
||||
setIsExporting(false);
|
||||
};
|
||||
|
||||
const handleRestore = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!selectedFile) return setRestoreError("Please select a JSON backup file.");
|
||||
|
||||
setIsRestoring(true);
|
||||
setRestoreError("");
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
const jsonString = event.target?.result as string;
|
||||
|
||||
const formData = new FormData(e.currentTarget as HTMLFormElement);
|
||||
formData.append("jsonString", jsonString);
|
||||
|
||||
const res = await restoreDatabase(formData);
|
||||
|
||||
if (res.error) {
|
||||
setRestoreError(res.error);
|
||||
} else {
|
||||
setRestoreSuccess(true);
|
||||
// Recargar para forzar la re-lectura de la BD
|
||||
setTimeout(() => window.location.href = "/hq-command/dashboard", 3000);
|
||||
}
|
||||
setIsRestoring(false);
|
||||
};
|
||||
reader.readAsText(selectedFile);
|
||||
} catch (error) {
|
||||
setRestoreError("Failed to read the file.");
|
||||
setIsRestoring(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-6 md:p-12 max-w-5xl mx-auto">
|
||||
{/* HEADER */}
|
||||
<div className="mb-10">
|
||||
<Link href="/hq-command/dashboard" className="inline-flex items-center gap-2 text-sm text-[#86868B] hover:text-[#00F0FF] transition-colors mb-6 group">
|
||||
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" /> Back to Command Center
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-light text-white flex items-center gap-3">
|
||||
<Server className="text-blue-400" /> System Health & Vault
|
||||
</h1>
|
||||
<p className="text-[#86868B] mt-2">Server telemetry, data snapshots, and disaster recovery protocols.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TELEMETRÍA (MÉTRICAS) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div className="bg-[#111] border border-white/5 p-6 rounded-3xl relative overflow-hidden">
|
||||
<div className="absolute -right-4 -bottom-4 opacity-5"><Activity size={100} /></div>
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Node.js Uptime</span>
|
||||
<p className="text-2xl font-mono text-white">
|
||||
{metrics ? formatUptime(metrics.uptime) : "Loading..."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#111] border border-white/5 p-6 rounded-3xl relative overflow-hidden">
|
||||
<div className="absolute -right-4 -bottom-4 opacity-5"><HardDrive size={100} /></div>
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Heap Memory Usage</span>
|
||||
<p className="text-2xl font-mono text-white">
|
||||
{metrics ? `${metrics.memory.used} MB ` : "0 MB "}
|
||||
<span className="text-sm text-[#86868B]">/ {metrics ? metrics.memory.total : 0} MB</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#111] border border-white/5 p-6 rounded-3xl relative overflow-hidden">
|
||||
<div className="absolute -right-4 -bottom-4 opacity-5"><Database size={100} /></div>
|
||||
<span className="text-[10px] uppercase tracking-widest text-[#86868B] block mb-2">Postgres Connection</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-3 w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
|
||||
</span>
|
||||
<p className="text-lg font-medium text-emerald-400">
|
||||
{metrics ? metrics.dbStatus : "Connecting..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
|
||||
{/* ZONA VERDE: EXPORTAR (BACKUP) */}
|
||||
<div className="bg-[#111] border border-white/5 rounded-3xl p-8 flex flex-col">
|
||||
<div className="flex items-center gap-3 text-emerald-400 mb-4">
|
||||
<DownloadCloud size={24} />
|
||||
<h3 className="text-xl font-medium">Data Snapshot</h3>
|
||||
</div>
|
||||
<p className="text-sm text-[#86868B] leading-relaxed mb-8 flex-1">
|
||||
Download a complete JSON snapshot of your PostgreSQL database. This file contains all configurations, users, and content needed to perfectly clone or restore your environment via Docker.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
className="w-full bg-white text-black py-4 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-gray-200 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isExporting ? <><Loader2 size={18} className="animate-spin" /> Compiling Data...</> : "Download Secure Backup"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ZONA ROJA: RESTAURAR (DANGER ZONE) */}
|
||||
<div className="bg-rose-500/5 border border-rose-500/20 rounded-3xl p-8 relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-rose-500 to-transparent"></div>
|
||||
|
||||
{restoreSuccess ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center animate-in fade-in zoom-in duration-500">
|
||||
<CheckCircle2 size={48} className="text-emerald-400 mb-4" />
|
||||
<h3 className="text-2xl font-light text-white mb-2">Restoration Complete</h3>
|
||||
<p className="text-[#86868B]">The database has been successfully overwritten. Rebooting session...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-3 text-rose-500 mb-4">
|
||||
<ShieldAlert size={24} />
|
||||
<h3 className="text-xl font-medium">Disaster Recovery</h3>
|
||||
</div>
|
||||
<p className="text-xs text-rose-400/80 leading-relaxed mb-6">
|
||||
<strong>WARNING:</strong> Uploading a snapshot will instantaneously erase all current database records and overwrite them. This process is irreversible.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleRestore} className="space-y-4">
|
||||
<div className="bg-black/40 border border-white/5 p-3 rounded-xl flex items-center justify-between">
|
||||
<span className="text-xs text-[#86868B] truncate max-w-[200px]">
|
||||
{selectedFile ? selectedFile.name : "No backup selected"}
|
||||
</span>
|
||||
<button type="button" onClick={() => fileInputRef.current?.click()} className="text-xs bg-white/10 hover:bg-white/20 text-white px-3 py-1.5 rounded-lg transition-colors">
|
||||
<UploadCloud size={14} className="inline mr-1" /> Select JSON
|
||||
</button>
|
||||
<input type="file" accept=".json" ref={fileInputRef} onChange={e => setSelectedFile(e.target.files?.[0] || null)} className="hidden" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input required name="username" placeholder="Admin Username" className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-sm outline-none focus:border-rose-500" />
|
||||
<input required name="password" type="password" placeholder="Admin Password" className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-sm outline-none focus:border-rose-500" />
|
||||
</div>
|
||||
|
||||
<input required name="confirm" placeholder="Type CONFIRM-RESTORE" className="w-full bg-black/60 border border-rose-500/30 rounded-xl px-4 py-3 text-rose-400 font-mono text-sm text-center outline-none focus:border-rose-500" />
|
||||
|
||||
{restoreError && <div className="p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg text-rose-400 text-xs text-center">{restoreError}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isRestoring || !selectedFile}
|
||||
className="w-full bg-rose-600 text-white py-4 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-rose-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isRestoring ? <><Loader2 size={18} className="animate-spin" /> Overwriting Database...</> : "Execute Destructive Restore"}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user