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 (

System health

{!data ? "Checking…" : counts.bad ? `${counts.bad} problem${counts.bad === 1 ? "" : "s"} to fix` : counts.warn ? `All running, ${counts.warn} thing${counts.warn === 1 ? "" : "s"} to look at` : "Everything is running"}{data ? ` · checked ${timeAgo(data.at)}` : ""}

{!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 (

Audit log

Every change made in Quorla and everything it published. Passwords and keys are never recorded.

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 &&
}
); } 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 (

Agencies

Hosted Quorla workspaces: one private copy per agency. Sign-up page: /signup

{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. : (
{data.tenants.map((t) => ( ))}
AgencyStatusWorkspaceOwnerSigned upActions
{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" && } {["active", "past_due", "provisioning"].includes(t.status) && } {["suspended", "cancelled"].includes(t.status) && } {t.actual === "running" && } {t.stripe?.customer && } {["suspended", "cancelled"].includes(t.status) && }
)}
); }