refactor(admin): optimize db tools with bento cockpit
Some checks failed
Staging Build / build (push) Failing after 32s

This commit is contained in:
DanielS
2026-08-06 17:49:42 +02:00
parent d02703af8b
commit b3ae720136

View File

@@ -1,16 +1,15 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import { motion } from "framer-motion";
getDatabaseIntegrity, import {
repairDatabaseSchema, getDatabaseIntegrity,
optimizeDatabaseIndices repairDatabaseSchema,
optimizeDatabaseIndices
} from "@/lib/actions/admin"; } from "@/lib/actions/admin";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Loader2, RefreshCw, Wrench, Database, AlertTriangle, CheckCircle2, XCircle, Terminal, ArrowLeft } from "lucide-react"; import { Loader2, RefreshCw, Wrench, Database, CheckCircle2, XCircle, Terminal } from "lucide-react";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client"; import { createClient } from "@/lib/supabase/client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -26,6 +25,19 @@ interface IntegrityData {
}; };
} }
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() { export default function AdminToolsPage() {
const [integrity, setIntegrity] = useState<IntegrityData | null>(null); const [integrity, setIntegrity] = useState<IntegrityData | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -88,218 +100,145 @@ export default function AdminToolsPage() {
useEffect(() => { useEffect(() => {
async function checkAccess() { async function checkAccess() {
const supabase = createClient(); try {
const { data: { user } } = await supabase.auth.getUser(); const supabase = createClient();
if (!user) { const { data: { user } } = await supabase.auth.getUser();
router.push('/auth/login'); if (!user) {
return; 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);
} }
const { data: userData } = await supabase
.from('users')
.select('role')
.eq('id', user.id)
.single();
if (!userData || userData.role === 'verwaltung') {
router.push('/admin');
return;
}
setLoading(false);
loadStatus();
} }
checkAccess(); checkAccess();
}, [router]); }, [router]);
if (loading) { if (loading) {
return <div className="min-h-screen bg-[#020617] text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>; return <div className="p-8 text-white flex justify-center items-center"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>;
} }
return ( return (
<div className="min-h-screen bg-[#020617] text-white px-4 py-12 relative overflow-hidden"> <div className="p-6 max-w-7xl mx-auto text-slate-900 dark:text-white space-y-6">
{/* Background decoration */} {/* Header */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[600px] bg-blue-600/10 blur-[120px] rounded-full -z-10 opacity-30" /> <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">
{/* Loading Overlay */} <Database className="w-8 h-8 text-primary" />
{isLoading && ( Datenbank Cockpit & Tools
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex flex-col items-center justify-center gap-4"> </h1>
<Loader2 className="w-12 h-12 text-primary animate-spin" /> <p className="text-slate-400 text-xs">
<p className="text-slate-200 text-lg font-medium"> Verwalten und reparieren Sie Systemtabellen, Sicherheitsrichtlinien (RLS) und Datenbank-Indizes.
{activeAction === "repair" </p>
? "Schema wird repariert..." </motion.div>
: activeAction === "optimize"
? "Datenbank wird indexiert..."
: "Verbindung zur Datenbank wird aufgebaut..."}
</p>
</div>
)}
<div className="max-w-5xl mx-auto space-y-8"> {/* Bento Grid layout */}
{/* Header */} <motion.div variants={containerVariants} initial="hidden" animate="visible" className="grid grid-cols-1 md:grid-cols-3 gap-5">
<div className="flex items-center gap-4"> {/* Bento Card 1: Integritätsscan */}
<Link href="/"> <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">
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-white"> <div className="space-y-3">
<ArrowLeft className="w-4 h-4 mr-2" /> Startseite <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">
</Button> <RefreshCw className="w-5 h-5" />
</Link> </div>
<div> <div>
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3"> <h3 className="font-bold text-base text-white">Integritätsscan</h3>
<Database className="w-8 h-8 text-primary" /> <p className="text-xs text-slate-400 mt-1 leading-relaxed">
Admin Datenbank-Cockpit
</h1>
<p className="text-slate-400 text-sm mt-1">
Verwalten und reparieren Sie Systemtabellen, Sicherheitsrichtlinien (RLS) und Datenbank-Indizes.
</p>
</div>
</div>
{/* Action Controls */}
<div className="grid md:grid-cols-3 gap-6">
<Card className="glass-dark border-white/10 flex flex-col justify-between">
<CardHeader>
<CardTitle className="text-white text-lg flex items-center gap-2">
<RefreshCw className="w-5 h-5 text-blue-400" />
Integritätsscan
</CardTitle>
<CardDescription className="text-slate-400">
Prüft die physische Existenz der Tabellen und zählt deren Zeilenanzahl. Prüft die physische Existenz der Tabellen und zählt deren Zeilenanzahl.
</CardDescription> </p>
</CardHeader>
<CardContent className="pt-0">
<Button
onClick={() => loadStatus()}
disabled={isLoading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Status aktualisieren
</Button>
</CardContent>
</Card>
<Card className="glass-dark border-white/10 flex flex-col justify-between">
<CardHeader>
<CardTitle className="text-white text-lg flex items-center gap-2">
<Wrench className="w-5 h-5 text-emerald-400" />
Schema reparieren
</CardTitle>
<CardDescription className="text-slate-400">
Erstellt fehlende Kerntabellen automatisch neu und konfiguriert die RLS-Rechte.
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<Button
onClick={handleRepair}
disabled={isLoading}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
>
Schema reparieren
</Button>
</CardContent>
</Card>
<Card className="glass-dark border-white/10 flex flex-col justify-between">
<CardHeader>
<CardTitle className="text-white text-lg flex items-center gap-2">
<Database className="w-5 h-5 text-amber-400" />
Index optimieren
</CardTitle>
<CardDescription className="text-slate-400">
Baut korrupte Suchindizes im Schema public mittels REINDEX neu auf.
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<Button
onClick={handleOptimize}
disabled={isLoading}
className="w-full bg-amber-600 hover:bg-amber-700 text-white"
>
Index optimieren
</Button>
</CardContent>
</Card>
</div>
{/* Tabellenstatus */}
<Card className="glass-dark border-white/10">
<CardHeader>
<CardTitle className="text-white text-xl">Tabellen-Status & Diagnose</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid sm:grid-cols-2 md:grid-cols-4 gap-4">
{integrity?.tables ? (
Object.entries(integrity.tables).map(([name, status]) => (
<div
key={name}
className="p-4 rounded-xl border bg-white/5 border-white/10 flex flex-col justify-between gap-2"
>
<span className="font-semibold text-slate-300 text-sm capitalize">
{name.replace("_", " ")}
</span>
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500">Zeilen: {status.count}</span>
{status.exists ? (
<Badge className="bg-emerald-500/10 text-emerald-400 border-emerald-500/20 gap-1">
<CheckCircle2 className="w-3.5 h-3.5" /> OK
</Badge>
) : (
<Badge className="bg-red-500/10 text-red-400 border-red-500/20 gap-1">
<XCircle className="w-3.5 h-3.5" /> Fehlt
</Badge>
)}
</div>
</div>
))
) : (
<div className="col-span-4 py-8 text-center text-slate-500 text-sm">
Lade Diagnosedaten...
</div>
)}
</div> </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>
{/* Foreign Key Errors */} {/* Bento Card 2: Schema Reparieren */}
{integrity && integrity.errors.foreign_keys > 0 && ( <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="p-4 rounded-xl bg-red-500/10 border border-red-500/20 flex gap-3 items-center"> <div className="space-y-3">
<AlertTriangle className="w-6 h-6 text-red-400 shrink-0" /> <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">
<div> <Wrench className="w-5 h-5" />
<p className="font-bold text-red-200">Fremdschlüssel-Fehler gefunden!</p> </div>
<p className="text-red-300/80 text-sm"> <div>
Es wurden {integrity.errors.foreign_keys} verwaiste Lizenzen ohne zugeordneten Endkunden in der Tabelle `licenses` detektiert. <h3 className="font-bold text-base text-white">Schema Reparieren</h3>
</p> <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> </div>
</div> ))
) : (
<p className="text-xs text-slate-500 col-span-3">Keine Diagnose-Daten geladen.</p>
)} )}
</CardContent> </div>
</Card> </motion.div>
{/* Console / Output logs */} {/* Bento Card 5: Live Konsole / Logs (1 Spalte) */}
<Card className="glass-dark border-white/10"> <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">
<CardHeader className="flex flex-row items-center justify-between"> <div className="space-y-2">
<CardTitle className="text-white text-lg flex items-center gap-2"> <h3 className="font-bold text-base text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<Terminal className="w-5 h-5 text-primary" /> <Terminal className="w-4 h-4 text-sky-400" /> System-Konsole
Diagnose-Konsole </h3>
</CardTitle> <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">
<Button
variant="ghost"
size="sm"
onClick={() => setLogs([])}
className="text-slate-400 hover:text-white text-xs"
>
Konsole leeren
</Button>
</CardHeader>
<CardContent className="pt-0">
<div className="font-mono text-xs bg-black/50 p-4 rounded-xl border border-white/5 h-64 overflow-y-auto space-y-1.5 scrollbar-thin">
{logs.length === 0 ? ( {logs.length === 0 ? (
<span className="text-slate-600 italic">Konsole bereit. Führen Sie einen Scan aus.</span> <span className="text-slate-600 italic">Warte auf Aktionen...</span>
) : ( ) : (
logs.map((log, index) => ( logs.map((log, i) => <div key={i} className="leading-tight">{log}</div>)
<div key={index} className={log.includes("FEHLER") ? "text-red-400" : log.includes("Erfolgreich") ? "text-emerald-400" : "text-slate-300"}>
{log}
</div>
))
)} )}
</div> </div>
</CardContent> </div>
</Card> </motion.div>
</div> </motion.div>
</div> </div>
); );
} }