import { useCallback, useEffect, useState } from "react";
import { CircleAlert, CircleCheck, History, RefreshCw, TriangleAlert } from "lucide-react";
import { api } from "../api.js";
import { Badge, Button, cx, Empty, ErrorNote, fmtDateTime, Panel, Spinner, timeAgo } from "../ui.jsx";
const ICON = { ok: CircleCheck, warn: TriangleAlert, bad: CircleAlert };
const TONE = { ok: "text-ok", warn: "text-warn", bad: "text-bad" };
/** Admin → System health: everything that runs in the background, in one list. */
export function Health() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
const load = useCallback(() => { setBusy(true); return api("/api/admin/health").then(setData).catch(setError).finally(() => setBusy(false)); }, []);
useEffect(() => { load(); }, [load]);
const counts = data ? { bad: data.checks.filter((c) => c.status === "bad").length, warn: data.checks.filter((c) => c.status === "warn").length } : null;
return (
{!data ?
: (
{data.checks.map((c, i) => {
const Icon = ICON[c.status];
return (
{c.area} {c.title}
{c.detail &&
{c.detail}
}
);
})}
)}
);
}
/** Admin → Audit log: who did what, and what Quorla published. */
export function Audit() {
const [clients, setClients] = useState([]);
const [f, setF] = useState({ days: 30, client: "", user: "", q: "" });
const [items, setItems] = useState(null);
const [error, setError] = useState(null);
const [shown, setShown] = useState(100);
useEffect(() => { api("/api/overview").then((r) => setClients(r.clients)).catch(() => {}); }, []);
const load = useCallback(() => {
const q = new URLSearchParams({ days: String(f.days) });
for (const k of ["client", "user", "q"]) if (f[k]) q.set(k, f[k]);
setItems(null); setError(null);
return api(`/api/admin/audit?${q}`).then((r) => setItems(r.items)).catch(setError);
}, [f]);
useEffect(() => { const t = setTimeout(load, 250); return () => clearTimeout(t); }, [load]);
const users = [...new Set((items || []).map((i) => i.by))].sort();
const name = (slug) => clients.find((c) => c.slug === slug)?.name || slug;
return (
setF({ ...f, days: Number(e.target.value) })}>{[7, 30, 90].map((d) => Last {d} days )}
setF({ ...f, client: e.target.value })}>All clients {clients.map((c) => {c.name} )}
setF({ ...f, user: e.target.value })}>Everyone {[...new Set([f.user, ...users].filter(Boolean))].map((u) => {u} )}
setF({ ...f, q: e.target.value })} />
{!items ? : !items.length ? : (
{items.slice(0, shown).map((e, i) => (
{fmtDateTime(e.at)}
{e.by} {e.text}
{e.client &&
· {name(e.client)} }
{e.kind === "error" &&
error }
{e.detail &&
{e.detail}
}
))}
)}
{items && items.length > shown && setShown(shown + 100)}>Show more
}
);
}
const T_TONE = { active: "ok", provisioning: "brand", pending_payment: "neutral", past_due: "warn", suspended: "bad", cancelled: "neutral", removed: "neutral" };
const T_LABEL = { active: "Active", provisioning: "Setting up", pending_payment: "Awaiting payment", past_due: "Payment overdue", suspended: "Suspended", cancelled: "Cancelled", removed: "Removed" };
/** Admin → Agencies (control centre only): every hosted workspace, its subscription and container. */
export function Agencies() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [busy, setBusy] = useState(null);
const load = useCallback(() => api("/api/control/tenants").then(setData).catch(setError), []);
useEffect(() => { load(); }, [load]);
const act = async (slug, action, confirmText) => {
if (confirmText && !window.confirm(confirmText)) return;
setBusy(`${slug}-${action}`); setError(null);
try {
const r = await api(`/api/control/tenants/${slug}/${action}`, { method: "POST" });
if (r.url) window.open(r.url, "_blank", "noopener");
await load();
} catch (e) { setError(e); }
setBusy(null);
};
return (
{data && (!data.stripe || !data.provisioner) && (
{!data.stripe && "Stripe isn't set up: sign-ups wait for you to approve them here. "}
{!data.provisioner && "PROVISION_TOKEN isn't set: the server script can't create workspaces yet. "}
See docs/multi-tenant.md.
)}
{!data ? : !data.tenants.length ? When an agency signs up at /signup it appears here. : (
Agency Status Workspace Owner Signed up Actions
{data.tenants.map((t) => (
{t.agencyName}
{t.slug}{t.port ? ` · port ${t.port}` : ""}
{T_LABEL[t.status] || t.status}
container: {t.actual}{t.desired !== t.actual && t.desired !== "none" ? ` → ${t.desired}` : ""}
{t.lastError && {t.lastError}
}
{t.url.replace(/^https:\/\//, "")} {t.setupPending && !t.welcomedAt && t.actual === "running" ? welcome email not sent
: null}
{t.ownerName}{t.ownerEmail}
{timeAgo(t.createdAt)}
{t.status === "pending_payment" && act(t.slug, "approve", `Create ${t.agencyName}'s workspace without payment?`)}>Approve }
{["active", "past_due", "provisioning"].includes(t.status) && act(t.slug, "suspend", `Stop ${t.agencyName}'s workspace? Their data is kept.`)}>Suspend }
{["suspended", "cancelled"].includes(t.status) && act(t.slug, "resume")}>Resume }
{t.actual === "running" && act(t.slug, "welcome")}>Resend welcome }
{t.stripe?.customer && act(t.slug, "portal")}>Billing }
{["suspended", "cancelled"].includes(t.status) && act(t.slug, "remove", `Remove ${t.agencyName}'s container? Their data folder is archived on the server, not deleted.`)}>Remove }
))}
)}
);
}