"use client"; import { useState, useEffect, useCallback } from "react"; import { useTranslations, useFormatter } from "next-intl"; import type { PiecedTenant, TenantRequest } from "@/types"; import { StatusBadge } from "@/components/ui/status-badge"; import { formatDateTime, formatRelative } from "@/lib/format"; import Link from "next/link"; type Tab = "requests" | "tenants" | "health"; type RequestFilter = "all" | "pending" | "provisioning" | "approved" | "rejected"; interface HealthData { tenants: { total: number; phases: Record }; spend: { global: number; perTenant: Record }; services: { litellm: { healthy: boolean; details?: any }; vllm: { healthy: boolean; details?: any }; }; } interface AdminPanelProps { initialTenants: PiecedTenant[]; } export function AdminPanel({ initialTenants }: AdminPanelProps) { const t = useTranslations("admin"); const f = useFormatter(); const [tab, setTab] = useState("requests"); // Requests state const [requests, setRequests] = useState([]); const [filter, setFilter] = useState("all"); const [loadingRequests, setLoadingRequests] = useState(true); const [actionLoading, setActionLoading] = useState(null); const [rejectModal, setRejectModal] = useState(null); const [rejectNotes, setRejectNotes] = useState(""); // Tenants state const [tenants, setTenants] = useState(initialTenants); const [loadingTenants, setLoadingTenants] = useState(false); const [deleteModal, setDeleteModal] = useState(null); // Health state const [health, setHealth] = useState(null); const [loadingHealth, setLoadingHealth] = useState(false); // Shared const [error, setError] = useState(""); // ─── Requests fetching ─── const fetchRequests = useCallback(async () => { try { const url = filter === "all" ? "/api/admin/requests" : `/api/admin/requests?status=${filter}`; const res = await fetch(url); if (!res.ok) throw new Error("Failed to fetch"); const data = await res.json(); setRequests(data); } catch (e: any) { setError(e.message); } finally { setLoadingRequests(false); } }, [filter]); useEffect(() => { if (tab === "requests") { setLoadingRequests(true); fetchRequests(); } }, [tab, filter, fetchRequests]); // ─── Tenants fetching ─── const fetchTenants = useCallback(async () => { setLoadingTenants(true); try { const res = await fetch("/api/tenants"); if (!res.ok) throw new Error("Failed to fetch tenants"); const data = await res.json(); setTenants(data); } catch (e: any) { setError(e.message); } finally { setLoadingTenants(false); } }, []); useEffect(() => { if (tab === "tenants") { fetchTenants(); } }, [tab, fetchTenants]); // ─── Health fetching ─── const fetchHealth = useCallback(async () => { setLoadingHealth(true); try { const res = await fetch("/api/admin/health"); if (!res.ok) throw new Error("Failed to fetch health"); const data = await res.json(); setHealth(data); } catch (e: any) { setError(e.message); } finally { setLoadingHealth(false); } }, []); useEffect(() => { if (tab === "health") { fetchHealth(); } }, [tab, fetchHealth]); // Also fetch health for spend data when on tenants tab useEffect(() => { if (tab === "tenants" && !health) { fetchHealth(); } }, [tab, health, fetchHealth]); // ─── Request actions ─── const handleApprove = async (id: string) => { setActionLoading(id); setError(""); try { const res = await fetch(`/api/admin/requests/${id}/approve`, { method: "POST", }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Approve failed"); } await fetchRequests(); } catch (e: any) { setError(e.message); } finally { setActionLoading(null); } }; const handleReject = async (id: string) => { setActionLoading(id); setError(""); try { const res = await fetch(`/api/admin/requests/${id}/reject`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ adminNotes: rejectNotes || undefined }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Reject failed"); } setRejectModal(null); setRejectNotes(""); await fetchRequests(); } catch (e: any) { setError(e.message); } finally { setActionLoading(null); } }; // ─── Tenant actions ─── const handleSuspend = async (name: string, suspend: boolean) => { setActionLoading(name); setError(""); try { const res = await fetch(`/api/admin/tenants/${name}/suspend`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ suspend }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Suspend failed"); } await fetchTenants(); } catch (e: any) { setError(e.message); } finally { setActionLoading(null); } }; const handleDelete = async (name: string) => { setActionLoading(name); setError(""); try { const res = await fetch(`/api/admin/tenants/${name}/delete`, { method: "POST", }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Delete failed"); } setDeleteModal(null); // Bug 32: K8s deletion is asynchronous — the resource enters a // Terminating phase with a deletionTimestamp set, finalizers run, // then the resource is fully removed. fetchTenants() right // after the API call would race the K8s store and often still // include the just-deleted row. Two complementary fixes: // 1. Optimistically drop the row from local state so the UI // reflects the user's intent immediately. // 2. Schedule a delayed refetch (1.5s) to pick up any side // effects (cascaded request rows, freshly-released names). // The immediate fetchTenants() is kept as a "best chance" — if // K8s does report the deletion synchronously (rare), we get the // freshest data. If it doesn't, the optimistic update has us // covered until the delayed refetch lands. setTenants((prev) => prev.filter((t) => t.metadata.name !== name)); fetchTenants(); setTimeout(() => fetchTenants(), 1500); } catch (e: any) { setError(e.message); } finally { setActionLoading(null); } }; const FILTERS: RequestFilter[] = [ "all", "pending", "provisioning", "approved", "rejected", ]; const pendingCount = requests.filter((r) => r.status === "pending").length; return ( <> {/* Tab bar */}
{/* Error banner */} {error && (
{error}
)} {/* ───── REQUESTS TAB ───── */} {tab === "requests" && ( <>
{FILTERS.map((f) => ( ))}
{loadingRequests ? (

{t("loadingRequests")}

) : requests.length === 0 ? (

{t("noRequests")}

) : (
{requests.map((req) => ( ))}
{t("company")} {t("contact")} {t("agentName")} {t("packages")} {t("status")} {t("submitted")} {t("actions")}
{req.companyName}
{req.contactName}
{req.contactEmail}
{req.agentName} {req.packages?.length ? req.packages.join(", ") : "—"}
{formatDateTime(req.createdAt, f)}
{formatRelative(req.createdAt, f)}
{req.status === "pending" && ( <> )} {(req.status === "provisioning" || req.status === "approved" || req.status === "active") && req.tenantName && ( {t("viewTenant")} )} {req.status === "rejected" && ( )}
{req.adminNotes && (

{req.adminNotes}

)}
)} )} {/* ───── TENANTS TAB ───── */} {tab === "tenants" && ( <> {/* Summary cards */}
t.status?.phase === "Running" || t.status?.phase === "Ready" ).length } color="text-emerald-400" /> t.spec.suspend).length} color="text-amber-400" /> t.status?.phase === "Error").length } color="text-red-400" />
{loadingTenants ? (

{t("loadingTenants")}

) : tenants.length === 0 ? (

{t("noTenants")}

) : (
{tenants.map((tenant) => { const tenantSpend = health?.spend?.perTenant?.[tenant.metadata.name]; return ( ); })}
{t("name")} {t("displayName")} {t("phase")} {t("packages")} {t("spendChf")} {t("created")} {t("actions")}
{tenant.metadata.name} {tenant.spec.displayName} {tenant.spec.suspend && ( {t("suspendedBadge")} )} {tenant.spec.packages?.join(", ") || "—"} {tenantSpend !== undefined ? `CHF ${tenantSpend.toFixed(2)}` : "—"}
{formatDateTime( tenant.metadata.creationTimestamp, f )}
{formatRelative( tenant.metadata.creationTimestamp, f )}
{t("manage")}
)} )} {/* ───── HEALTH TAB ───── */} {tab === "health" && ( <> {loadingHealth ? (

{t("loadingHealth")}

) : health ? (
{/* Service health indicators */}

{t("serviceHealth")}

{/* Tenant overview */}

{t("tenantOverview")}

{/* Spend overview */}

{t("spendOverview")}

{t("globalSpend")}

CHF {health.spend.global.toFixed(2)}

{t("activeTenants")}

{Object.keys(health.spend.perTenant).length}

{t("tenantsWithSpend")}

{/* Refresh button */}
) : (

{t("healthUnavailable")}

)} )} {/* ───── REJECT MODAL ───── */} {rejectModal && (

{t("rejectTitle")}