import React, { useEffect, useMemo, useState } from "react"; import ReactDOM from "react-dom/client"; import { createClient, type Session } from "@supabase/supabase-js"; import "./styles.css"; type BackendPrincipal = { Subject: string; Email: string; Role: string; }; type ViewKey = "overview" | "projects" | "vms" | "ssh" | "audit" | "console"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL ?? ""; const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY ?? ""; const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080"; const supabase = supabaseUrl && supabaseAnonKey ? createClient(supabaseUrl, supabaseAnonKey) : undefined; const navItems: Array<{ key: ViewKey; label: string }> = [ { key: "overview", label: "Uebersicht" }, { key: "projects", label: "Projekte" }, { key: "vms", label: "VMs" }, { key: "ssh", label: "SSH-Keys" }, { key: "audit", label: "Audit" }, { key: "console", label: "Konsole" }, ]; function App() { const [session, setSession] = useState(null); const [loadingSession, setLoadingSession] = useState(true); const [activeView, setActiveView] = useState("overview"); const [backendPrincipal, setBackendPrincipal] = useState(null); const [backendStatus, setBackendStatus] = useState< "idle" | "loading" | "ok" | "error" >("idle"); const [backendError, setBackendError] = useState(""); useEffect(() => { if (!supabase) { setLoadingSession(false); return; } supabase.auth.getSession().then(({ data }) => { setSession(data.session); setLoadingSession(false); }); const { data } = supabase.auth.onAuthStateChange((_event, nextSession) => { setSession(nextSession); setBackendPrincipal(null); setBackendStatus("idle"); setBackendError(""); }); return () => data.subscription.unsubscribe(); }, []); const connected = Boolean(session && backendPrincipal); async function refreshBackendPrincipal() { if (!session) { return; } setBackendStatus("loading"); setBackendError(""); try { const response = await fetch(`${apiBaseUrl}/me`, { headers: { Authorization: `Bearer ${session.access_token}`, }, }); if (!response.ok) { throw new Error(`Backend antwortet mit ${response.status}`); } const principal = (await response.json()) as BackendPrincipal; setBackendPrincipal(principal); setBackendStatus("ok"); } catch (error) { setBackendStatus("error"); setBackendError(error instanceof Error ? error.message : "Unbekannter Fehler"); } } if (loadingSession) { return (
); } if (!supabase) { return ; } if (!session) { return ; } return (

ProxUI

Proxmox Multi-Tenant Console

{session.user.email} {connected ? "Backend verbunden" : "Session aktiv"}
); } function MissingConfig() { return (

Konfiguration

Frontend Env fehlt

`VITE_SUPABASE_URL` und `VITE_SUPABASE_ANON_KEY` muessen gesetzt sein.

); } function AuthScreen() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [mode, setMode] = useState<"signin" | "signup">("signin"); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); async function submit(event: React.FormEvent) { event.preventDefault(); setBusy(true); setMessage(""); const result = mode === "signin" ? await supabase!.auth.signInWithPassword({ email, password }) : await supabase!.auth.signUp({ email, password }); if (result.error) { setMessage(result.error.message); } setBusy(false); } return (

ProxUI

Anmelden

{message ?

{message}

: null}
); } function BackendPanel({ backendError, backendPrincipal, backendStatus, onRefresh, }: { backendError: string; backendPrincipal: BackendPrincipal | null; backendStatus: "idle" | "loading" | "ok" | "error"; onRefresh: () => void; }) { return (

Backend

Profil-Sync

{backendError ?

{backendError}

: null}
); } function ViewPanel({ activeView, connected, }: { activeView: ViewKey; connected: boolean; }) { const content = useMemo(() => viewContent(activeView), [activeView]); return (

{content.state}

{content.title}

{content.badge}
{content.rows.map((row) => (
{row.label} {row.value}
))}
); } function Metric({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } function statusLabel(status: "idle" | "loading" | "ok" | "error") { if (status === "loading") return "Pruefung laeuft"; if (status === "ok") return "Verbunden"; if (status === "error") return "Fehler"; return "Nicht geprueft"; } function viewContent(activeView: ViewKey) { const values: Record< ViewKey, { badge: string; rows: Array<{ label: string; value: string }>; state: string; title: string; } > = { overview: { badge: "Live", state: "Status", title: "Arbeitsbereich", rows: [ { label: "Auth", value: "angebunden" }, { label: "Profil-Sync", value: "angebunden" }, { label: "RLS", value: "aktiv" }, ], }, projects: { badge: "Platzhalter", state: "E2-T03", title: "Projekte", rows: [ { label: "Schema", value: "projects" }, { label: "Quotas", value: "project_quotas" }, { label: "API", value: "offen" }, ], }, vms: { badge: "Platzhalter", state: "E2-T05", title: "Virtuelle Maschinen", rows: [ { label: "Schema", value: "vms" }, { label: "VMID-Allokator", value: "bereit" }, { label: "Proxmox Client", value: "offen" }, ], }, ssh: { badge: "Platzhalter", state: "E2-T05", title: "SSH-Keys", rows: [ { label: "Schema", value: "ssh_keys" }, { label: "RLS", value: "aktiv" }, { label: "Upload/API", value: "offen" }, ], }, audit: { badge: "Platzhalter", state: "E2-T07", title: "Audit Log", rows: [ { label: "Schema", value: "audit_log" }, { label: "Append-only", value: "aktiv" }, { label: "Anzeige", value: "offen" }, ], }, console: { badge: "Platzhalter", state: "E8", title: "Web-Konsole", rows: [ { label: "Proxy", value: "Grundgeruest" }, { label: "Tickets", value: "offen" }, { label: "noVNC/xterm", value: "offen" }, ], }, }; return values[activeView]; } ReactDOM.createRoot(document.getElementById("root")!).render( );