245 lines
11 KiB
TypeScript
245 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { motion } from "framer-motion";
|
|
import {
|
|
getDatabaseIntegrity,
|
|
repairDatabaseSchema,
|
|
optimizeDatabaseIndices
|
|
} from "@/lib/actions/admin";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Loader2, RefreshCw, Wrench, Database, CheckCircle2, XCircle, Terminal } from "lucide-react";
|
|
import { createClient } from "@/lib/supabase/client";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
interface TableStatus {
|
|
exists: boolean;
|
|
count: number;
|
|
}
|
|
|
|
interface IntegrityData {
|
|
tables: Record<string, TableStatus>;
|
|
errors: {
|
|
foreign_keys: number;
|
|
};
|
|
}
|
|
|
|
const containerVariants = {
|
|
hidden: { opacity: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
transition: { staggerChildren: 0.1 }
|
|
}
|
|
};
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 15 },
|
|
visible: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 80, damping: 15 } }
|
|
};
|
|
|
|
export default function AdminToolsPage() {
|
|
const [integrity, setIntegrity] = useState<IntegrityData | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
const [activeAction, setActiveAction] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const router = useRouter();
|
|
|
|
const addLog = (message: string) => {
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
setLogs((prev) => [...prev, `[${timestamp}] ${message}`]);
|
|
};
|
|
|
|
const loadStatus = async (quiet = false) => {
|
|
if (!quiet) setIsLoading(true);
|
|
addLog("Starte Integritätsscan der Datenbank...");
|
|
const res = await getDatabaseIntegrity();
|
|
if (res.success && res.data) {
|
|
setIntegrity(res.data as IntegrityData);
|
|
addLog("Integritätsscan erfolgreich abgeschlossen.");
|
|
} else {
|
|
addLog(`FEHLER beim Scannen: ${res.error}`);
|
|
}
|
|
if (!quiet) setIsLoading(false);
|
|
};
|
|
|
|
const handleRepair = async () => {
|
|
setIsLoading(true);
|
|
setActiveAction("repair");
|
|
addLog("Starte automatische Schemareparatur...");
|
|
const res = await repairDatabaseSchema();
|
|
if (res.success && res.data) {
|
|
const repaired = (res.data as any).repaired || [];
|
|
if (repaired.length > 0) {
|
|
addLog(`Erfolgreich reparierte Tabellen: ${repaired.join(", ")}`);
|
|
} else {
|
|
addLog("Keine fehlenden Tabellen gefunden. Schema intakt.");
|
|
}
|
|
await loadStatus(true);
|
|
} else {
|
|
addLog(`FEHLER bei Schemareparatur: ${res.error}`);
|
|
}
|
|
setIsLoading(false);
|
|
setActiveAction(null);
|
|
};
|
|
|
|
const handleOptimize = async () => {
|
|
setIsLoading(true);
|
|
setActiveAction("optimize");
|
|
addLog("Starte Reindexierung des Schemas public...");
|
|
const res = await optimizeDatabaseIndices();
|
|
if (res.success) {
|
|
addLog("Reindexierung erfolgreich abgeschlossen. Suchindizes neu aufgebaut.");
|
|
} else {
|
|
addLog(`FEHLER bei Reindexierung: ${res.error}`);
|
|
}
|
|
setIsLoading(false);
|
|
setActiveAction(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
async function checkAccess() {
|
|
try {
|
|
const supabase = createClient();
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) {
|
|
router.push('/auth/login');
|
|
return;
|
|
}
|
|
|
|
const [userRes] = await Promise.all([
|
|
supabase.from('users').select('role').eq('id', user.id).single(),
|
|
loadStatus(true)
|
|
]);
|
|
|
|
if (!userRes.data || userRes.data.role === 'verwaltung') {
|
|
router.push('/admin');
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.error("Fehler beim Laden von Admin Tools:", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
checkAccess();
|
|
}, [router]);
|
|
|
|
if (loading) {
|
|
return <div className="p-8 text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 max-w-7xl mx-auto text-slate-900 dark:text-white space-y-6">
|
|
{/* Header */}
|
|
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} className="space-y-1">
|
|
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
|
|
<Database className="w-8 h-8 text-primary" />
|
|
Datenbank Cockpit & Tools
|
|
</h1>
|
|
<p className="text-slate-400 text-xs">
|
|
Verwalten und reparieren Sie Systemtabellen, Sicherheitsrichtlinien (RLS) und Datenbank-Indizes.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Bento Grid layout */}
|
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
|
{/* Bento Card 1: Integritätsscan */}
|
|
<motion.div variants={itemVariants} whileHover={{ y: -3 }} className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md flex flex-col justify-between space-y-4 hover:border-blue-500/30 transition-all duration-300 shadow-md">
|
|
<div className="space-y-3">
|
|
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 text-blue-400 flex items-center justify-center">
|
|
<RefreshCw className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-base text-white">Integritätsscan</h3>
|
|
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Prüft die physische Existenz der Tabellen und zählt deren Zeilenanzahl.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button onClick={() => loadStatus()} disabled={isLoading} className="w-full bg-blue-600 hover:bg-blue-500 text-white rounded-xl h-10 text-xs font-bold">
|
|
{isLoading && activeAction === null ? <><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Prüft...</> : 'Status Scannen'}
|
|
</Button>
|
|
</motion.div>
|
|
|
|
{/* Bento Card 2: Schema Reparieren */}
|
|
<motion.div variants={itemVariants} whileHover={{ y: -3 }} className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md flex flex-col justify-between space-y-4 hover:border-emerald-500/30 transition-all duration-300 shadow-md">
|
|
<div className="space-y-3">
|
|
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center">
|
|
<Wrench className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-base text-white">Schema Reparieren</h3>
|
|
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Erstellt fehlende Kerntabellen automatisch neu und konfiguriert RLS-Rechte.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleRepair} disabled={isLoading} className="w-full bg-emerald-600 hover:bg-emerald-500 text-white rounded-xl h-10 text-xs font-bold">
|
|
{isLoading && activeAction === 'repair' ? <><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Repariert...</> : 'Schema Reparieren'}
|
|
</Button>
|
|
</motion.div>
|
|
|
|
{/* Bento Card 3: Index Optimieren */}
|
|
<motion.div variants={itemVariants} whileHover={{ y: -3 }} className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md flex flex-col justify-between space-y-4 hover:border-amber-500/30 transition-all duration-300 shadow-md">
|
|
<div className="space-y-3">
|
|
<div className="w-10 h-10 rounded-xl bg-amber-500/10 border border-amber-500/20 text-amber-400 flex items-center justify-center">
|
|
<Database className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-base text-white">Index Optimieren</h3>
|
|
<p className="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Baut Suchindizes im Schema public mittels REINDEX neu auf für maximale Performance.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleOptimize} disabled={isLoading} className="w-full bg-amber-600 hover:bg-amber-500 text-white rounded-xl h-10 text-xs font-bold">
|
|
{isLoading && activeAction === 'optimize' ? <><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Indexiert...</> : 'Index Optimieren'}
|
|
</Button>
|
|
</motion.div>
|
|
|
|
{/* Bento Card 4: Tabellen-Diagnose (2 Spalten) */}
|
|
<motion.div variants={itemVariants} className="md:col-span-2 p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md space-y-4 shadow-md">
|
|
<h3 className="font-bold text-base text-white border-b border-slate-800 pb-3">Tabellen-Status & Diagnose</h3>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
{integrity?.tables ? (
|
|
Object.entries(integrity.tables).map(([name, status]) => (
|
|
<div key={name} className="p-3 rounded-xl border bg-slate-950/60 border-slate-800 flex items-center justify-between">
|
|
<div>
|
|
<span className="font-bold text-xs text-slate-200 capitalize block">{name.replace("_", " ")}</span>
|
|
<span className="text-[10px] text-slate-400">Zeilen: {status.count}</span>
|
|
</div>
|
|
{status.exists ? (
|
|
<Badge className="bg-emerald-500/10 text-emerald-400 border-emerald-500/20 text-[10px] py-0 px-1.5">OK</Badge>
|
|
) : (
|
|
<Badge className="bg-red-500/10 text-red-400 border-red-500/20 text-[10px] py-0 px-1.5">Fehlt</Badge>
|
|
)}
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-xs text-slate-500 col-span-3">Keine Diagnose-Daten geladen.</p>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Bento Card 5: Live Konsole / Logs (1 Spalte) */}
|
|
<motion.div variants={itemVariants} className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 backdrop-blur-md space-y-3 shadow-md flex flex-col justify-between">
|
|
<div className="space-y-2">
|
|
<h3 className="font-bold text-base text-white flex items-center gap-2 border-b border-slate-800 pb-3">
|
|
<Terminal className="w-4 h-4 text-sky-400" /> System-Konsole
|
|
</h3>
|
|
<div className="bg-slate-950 p-3 rounded-xl border border-slate-800 font-mono text-[11px] text-slate-300 h-44 overflow-y-auto space-y-1 subpixel-antialiased">
|
|
{logs.length === 0 ? (
|
|
<span className="text-slate-600 italic">Warte auf Aktionen...</span>
|
|
) : (
|
|
logs.map((log, i) => <div key={i} className="leading-tight">{log}</div>)
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|