311 lines
12 KiB
TypeScript
311 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
getDatabaseIntegrity,
|
|
repairDatabaseSchema,
|
|
optimizeDatabaseIndices
|
|
} from "@/lib/actions/admin";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Loader2, RefreshCw, Wrench, Database, AlertTriangle, CheckCircle2, XCircle, Terminal, ArrowLeft } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { createClient } from "@/lib/supabase/client";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
interface TableStatus {
|
|
exists: boolean;
|
|
count: number;
|
|
}
|
|
|
|
interface IntegrityData {
|
|
tables: {
|
|
profiles: TableStatus;
|
|
end_customers: TableStatus;
|
|
pos_modules: TableStatus;
|
|
licenses: TableStatus;
|
|
};
|
|
errors: {
|
|
foreign_keys: number;
|
|
};
|
|
}
|
|
|
|
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() {
|
|
const supabase = createClient();
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) {
|
|
router.push('/auth/login');
|
|
return;
|
|
}
|
|
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();
|
|
}, [router]);
|
|
|
|
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="min-h-screen bg-[#020617] text-white px-4 py-12 relative overflow-hidden">
|
|
{/* Background decoration */}
|
|
<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" />
|
|
|
|
{/* Loading Overlay */}
|
|
{isLoading && (
|
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex flex-col items-center justify-center gap-4">
|
|
<Loader2 className="w-12 h-12 text-primary animate-spin" />
|
|
<p className="text-slate-200 text-lg font-medium">
|
|
{activeAction === "repair"
|
|
? "Schema wird repariert..."
|
|
: activeAction === "optimize"
|
|
? "Datenbank wird indexiert..."
|
|
: "Verbindung zur Datenbank wird aufgebaut..."}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="max-w-5xl mx-auto space-y-8">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-4">
|
|
<Link href="/">
|
|
<Button variant="ghost" size="sm" className="text-slate-400 hover:text-white">
|
|
<ArrowLeft className="w-4 h-4 mr-2" /> Startseite
|
|
</Button>
|
|
</Link>
|
|
<div>
|
|
<h1 className="text-3xl font-extrabold tracking-tight flex items-center gap-3">
|
|
<Database className="w-8 h-8 text-primary" />
|
|
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.
|
|
</CardDescription>
|
|
</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>
|
|
|
|
{/* Foreign Key Errors */}
|
|
{integrity && integrity.errors.foreign_keys > 0 && (
|
|
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 flex gap-3 items-center">
|
|
<AlertTriangle className="w-6 h-6 text-red-400 shrink-0" />
|
|
<div>
|
|
<p className="font-bold text-red-200">Fremdschlüssel-Fehler gefunden!</p>
|
|
<p className="text-red-300/80 text-sm">
|
|
Es wurden {integrity.errors.foreign_keys} verwaiste Lizenzen ohne zugeordneten Endkunden in der Tabelle `licenses` detektiert.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Console / Output logs */}
|
|
<Card className="glass-dark border-white/10">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle className="text-white text-lg flex items-center gap-2">
|
|
<Terminal className="w-5 h-5 text-primary" />
|
|
Diagnose-Konsole
|
|
</CardTitle>
|
|
<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 ? (
|
|
<span className="text-slate-600 italic">Konsole bereit. Führen Sie einen Scan aus.</span>
|
|
) : (
|
|
logs.map((log, index) => (
|
|
<div key={index} className={log.includes("FEHLER") ? "text-red-400" : log.includes("Erfolgreich") ? "text-emerald-400" : "text-slate-300"}>
|
|
{log}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|