Files
flux-srl/src/app/hq-command/dashboard/health/page.tsx
T
davidherran 59a146ef10
Deploy to VPS / deploy (push) Has been cancelled
feat: HQ-wide Toast + Confirm — no more browser alert()/confirm() popups
The Inbox panel got the polished Toast + Confirm primitives a few
commits back. This commit propagates them across every other panel in
HQ Command so the editor experience is uniformly on-brand. No more
1990s browser dialogs interrupting the dark CMS look.

NINE PANELS, ELEVEN CALL SITES UPGRADED
- health/page.tsx       — DB export errors → toast
- network/page.tsx      — Delete deployment → confirm + toast
- heritage/page.tsx     — Delete section → confirm + toast
- users/page.tsx        — Revoke architect → confirm + error toast
- news/page.tsx         — Delete article → confirm + toast
- applications/page.tsx — Save error toast + delete-app confirm
- parts/page.tsx        — Delete component → confirm + toast
- hero/page.tsx         — Delete slide → confirm + toast
- timeline/page.tsx     — Delete milestone → confirm + toast

Each destructive confirm now spells out what it does ('Permanently
remove this case from the global map. The asset folder on disk is
kept for safety') instead of a generic 'Delete?' prompt — much
clearer for editors who aren't sure whether files get nuked too.

Each success toast names the action ('Component deleted', 'Slide
deleted', 'Architect access revoked') so the editor sees exactly
what fired. Errors come in as red toasts with the actual error text.

NO BACKEND CHANGES. Pure UX layer on top of existing actions.
The HqUiProvider was already mounted in src/app/hq-command/layout.tsx,
so wiring up was just useHqUi() per page + the replacement calls.
2026-05-05 21:16:02 -05:00

221 lines
10 KiB
TypeScript

"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";
import { useHqUi } from "@/components/hq/Toast";
export default function SystemHealth() {
const ui = useHqUi();
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 {
ui.toast(res.error || "Export failed.", "error");
}
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>
);
}