SPB Git

spb/akos

Public
5.9 KB · 51 lines tsx
1import { useState } from "react";2import { Link } from "react-router";3import { api, ApiError, type PackageRow, type Project } from "../api";4import { useFetch, fmtDate } from "../hooks";5import { Badge, Empty, ErrorBox, Loading, Td } from "../components/ui";6import { useMe } from "../App";78type Decision = { id: string; execution_id: string | null; project_id: string; mode: string; decision: string; reasons: string[]; checks: Record<string, unknown>; created_at: string; project_slug: string; url: string | null; method: string | null; state: string | null };9type Egress = { id: number; ts: string; host: string; port: number; decision: string; execution_id: string | null; client: string | null; reason: string | null };10const MODES = ["discovery", "development", "research", "production"];1112export default function Policy() {13  const { me } = useMe();14  const projects = useFetch<{ projects: Project[] }>("projects");15  const pkgs = useFetch<{ packages: PackageRow[] }>("akps");16  const [filter, setFilter] = useState("");17  const decisions = useFetch<{ decisions: Decision[]; summary: { mode: string; decision: string; n: number }[]; modes: Record<string, string> }>(`policy/decisions?limit=100${filter ? `&decision=${filter}` : ""}`);18  const egress = useFetch<{ egress: Egress[] }>("egress?limit=100");19  const [err, setErr] = useState<ApiError | null>(null);20  const canWrite = me?.role === "admin" || me?.role === "operator";21  const patch = async (id: string, body: Record<string, unknown>) => { setErr(null); try { await api(`projects/${id}`, { method: "PATCH", body }); projects.reload(); } catch (e) { setErr(e as ApiError); } };22  const approvable = pkgs.data?.packages.filter((p) => ["tested", "registered", "monitored"].includes(p.lifecycle)) ?? [];23  return (24    <div>25      <h1>Policy Center</h1>26      <div className="card"><h3>Policy modes</h3>{decisions.data ? <dl className="kv">{Object.entries(decisions.data.modes).map(([m, d]) => <div key={m} style={{ display: "contents" }}><dt><Badge v={m} /></dt><dd>{d}</dd></div>)}</dl> : <Loading />}</div>27      <ErrorBox error={err} />28      <div className="card"><h3>Projects</h3>{projects.loading && <Loading />}<ErrorBox error={projects.error} />29        {projects.data && <table className="tbl"><thead><tr><th>Project</th><th>Mode</th><th>Approved AKPs (production)</th><th>Budget / day</th><th>Executions 24 h</th></tr></thead><tbody>30          {projects.data.projects.map((p) => <tr key={p.id}>31            <Td l="project"><Link to={`/projects/${p.id}`}>{p.name}</Link> <span className="hint">{p.slug}</span></Td>32            <Td l="mode">{canWrite ? <select aria-label={`policy mode of ${p.slug}`} value={p.policy_mode} onChange={(e) => patch(p.id, { policy_mode: e.target.value })}>{MODES.map((m) => <option key={m} disabled={m === "production" && me?.role !== "admin"}>{m}</option>)}</select> : <Badge v={p.policy_mode} />}</Td>33            <Td l="approved" wide>{canWrite ? <select aria-label={`approved packages of ${p.slug}`} multiple value={p.approved_akps} onChange={(e) => patch(p.id, { approved_akps: Array.from(e.target.selectedOptions).map((o) => o.value) })} style={{ minHeight: 60 }}>{approvable.map((k) => <option key={k.id} value={k.id}>{k.akp_id} v{k.version} ({k.lifecycle})</option>)}</select> : p.approved_akps.length} {approvable.length === 0 && <div className="hint">no tested packages yet</div>}</Td>34            <Td l="budget" className="num">{p.budget?.max_executions_per_day ?? "—"}</Td><Td l="24h" className="num">{p.executions_24h ?? 0}</Td></tr>)}35        </tbody></table>}</div>36      <div className="card"><div className="row between"><h3>Decision log</h3><select aria-label="Decision filter" style={{ maxWidth: 160 }} value={filter} onChange={(e) => setFilter(e.target.value)}><option value="">all decisions</option><option>allow</option><option>deny</option><option>quarantine</option></select></div>37        {decisions.data && <div className="row" style={{ marginBottom: 8 }}>{decisions.data.summary.map((s) => <span key={s.mode + s.decision} className={`badge ${s.decision === "allow" ? "good" : s.decision === "deny" ? "critical" : "serious"}`}>{s.mode}/{s.decision}: {s.n}</span>)}</div>}38        {decisions.loading && <Loading />}<ErrorBox error={decisions.error} />39        {decisions.data && decisions.data.decisions.length === 0 && <Empty>No policy decisions recorded.</Empty>}40        {decisions.data && decisions.data.decisions.length > 0 && <table className="tbl"><thead><tr><th>When</th><th>Project</th><th>Mode</th><th>Decision</th><th>Request</th><th>Reasons</th></tr></thead><tbody>41          {decisions.data.decisions.map((d) => <tr key={d.id}><Td l="when">{d.execution_id ? <Link to={`/executions/${d.execution_id}`}>{fmtDate(d.created_at)}</Link> : fmtDate(d.created_at)}</Td><Td l="project">{d.project_slug}</Td><Td l="mode">{d.mode}</Td><Td l="decision"><Badge v={d.decision} /></Td><Td l="request" wide><span className="truncate mono">{d.method} {d.url}</span></Td><Td l="reasons" wide><span className="hint">{d.reasons.join("; ")}</span></Td></tr>)}42        </tbody></table>}</div>43      <div className="card"><h3>Egress proxy log (sandboxed workers)</h3>{egress.loading && <Loading />}<ErrorBox error={egress.error} />44        {egress.data && egress.data.egress.length === 0 && <Empty>No proxy traffic yet. Sandboxed runs reach the network only through the egress proxy.</Empty>}45        {egress.data && egress.data.egress.length > 0 && <table className="tbl"><thead><tr><th>When</th><th>Host</th><th>Decision</th><th>Reason</th><th>Execution</th></tr></thead><tbody>46          {egress.data.egress.map((g) => <tr key={g.id}><Td l="when">{fmtDate(g.ts)}</Td><Td l="host"><code>{g.host}:{g.port}</code></Td><Td l="decision"><Badge v={g.decision} /></Td><Td l="reason">{g.reason}</Td><Td l="execution">{g.execution_id ? <Link to={`/executions/${g.execution_id}`}>{g.execution_id.slice(0, 8)}</Link> : "—"}</Td></tr>)}47        </tbody></table>}</div>48    </div>49  );50}51
Files · da58f809c01c422e65c786b05d6ffa3079c9feba
Menu

Theme