SPB Git

spb/akos

Public
4.9 KB · 44 lines tsx
1import { useState } from "react";2import { Link, useParams } from "react-router";3import { api, ApiError, type PackageDetail, type Project } from "../api";4import { useFetch, fmtBytes, fmtDate, short } from "../hooks";5import { Badge, ErrorBox, JobStatus, JsonTree, KV, Loading, Tabs } from "../components/ui";6import { DebtTable, EndpointRow, EvidenceList, LifecycleButtons } from "../components/shared";78export default function AkpDetail() {9  const { id } = useParams();10  const { data, error, loading, reload } = useFetch<PackageDetail>(`akps/${id}`);11  const projects = useFetch<{ projects: Project[] }>("projects");12  const [tab, setTab] = useState("Files");13  const [file, setFile] = useState<{ name: string; text: string } | null>(null);14  const [fileErr, setFileErr] = useState<ApiError | null>(null);15  const [job, setJob] = useState<{ id: string; label: string } | null>(null);16  if (loading) return <Loading />; if (error) return <ErrorBox error={error} />; if (!data) return null;17  const k = data;18  const m = k.manifest as { static_scan?: { ok: boolean; findings: string[] }; credential_env?: string | null; files?: Record<string, string>; knowledge_debt?: string[] };19  const open = async (name: string) => { setFileErr(null); try { setFile({ name, text: await api<string>(`/api/v1/akps/${k.id}/files/${name}`, { raw: true }) }); } catch (e) { setFileErr(e as ApiError); } };20  const run = async (kind: "verify" | "compile") => { const r = await api<{ job_id: string }>(`akps/${k.id}/${kind}`, { body: { live: true } }); setJob({ id: r.job_id, label: kind }); };21  const projs = projects.data?.projects.map((p) => ({ slug: p.slug, name: `${p.name} (${p.policy_mode})` })) ?? [{ slug: "default", name: "default" }];22  return (23    <div>24      <div className="row between"><div><h1>{k.akp_id} <span className="hint">v{k.version}</span> {k.opaque && <Badge v="critical" label="opaque" />}</h1><div className="hint"><Link to={`/apis/${k.api_id}`}>{k.api_name}</Link> · {k.provider_name} · <Badge v={k.lifecycle} /> <Badge v={k.knowledge_state} label={`knowledge: ${k.knowledge_state}`} />{k.superseded_by && <> · superseded by <Link to={`/akps/${k.superseded_by}`}>{short(k.superseded_by)}</Link></>}</div></div>25        <div className="row"><button className="btn sm" onClick={() => run("verify")}>Verify (tests + live call)</button><button className="btn sm" onClick={() => run("compile")}>Recompile (new version)</button><a className="btn sm" href={`/api/v1/akps/${k.id}/bundle.zip`}>Download bundle.zip</a></div></div>26      <JobStatus jobId={job?.id ?? null} label={job?.label} onDone={reload} />27      <div className="card"><KV items={[["Package hash", <code>{k.hash}</code>], ["Contract", <Link to={`/versions/${k.api_version_id}`}>{k.api_version_label}</Link>], ["Contract hash", <code>{k.contract_hash}</code>], ["License", (k.license?.name as string) ?? <Badge v="warn" label="unresolved" />], ["Snapshots", (k.license?.snapshots as string) ?? "metadata_only"], ["Credential", m.credential_env ? <code>{m.credential_env} (vault reference{k.credential_ref_id ? "" : " not bound"})</code> : "none (public API)"], ["Static scan", m.static_scan ? <Badge v={m.static_scan.ok ? "pass" : "fail"} /> : "—"], ["Created", `${fmtDate(k.created_at)}${k.created_by ? ` by ${k.created_by}` : ""}`]]} />28        {m.static_scan && !m.static_scan.ok && <pre>{m.static_scan.findings.join("\n")}</pre>}29        <h3>Lifecycle</h3><LifecycleButtons allowed={k.allowed_transitions} onTransition={async (to, reason) => { await api(`akps/${k.id}/lifecycle`, { body: { to, reason } }); reload(); }} /></div>30      <Tabs tabs={["Files", "Operations", "Evidence", "Debt", "Manifest", "README"]} active={tab} onChange={setTab} />31      {tab === "Files" && (32        <div className="grid c2">33          <div className="card"><h3>Generated files (content-addressed)</h3>{k.files.map((f) => <div key={f.filename} className="row between" style={{ padding: "5px 0", borderBottom: "1px solid var(--border)" }}><span><button className="btn sm" onClick={() => open(f.filename)}>{f.filename}</button> <span className="hint">{f.language}</span></span><span className="hint">{fmtBytes(f.size_bytes)} · <code>{short(f.sha256, 10)}</code></span></div>)}</div>34          <div className="card"><h3>{file ? file.name : "Viewer"}</h3><ErrorBox error={fileErr} />{file ? <pre>{file.text}</pre> : <div className="empty">Select a file to view its content.</div>}</div>35        </div>)}36      {tab === "Operations" && <div className="stack">{k.endpoints.map((e) => <EndpointRow key={e.id} e={{ ...e, parameters: e.parameters ?? [] }} packageId={k.id} projects={projs} />)}</div>}37      {tab === "Evidence" && <EvidenceList evidence={k.evidence} />}38      {tab === "Debt" && <DebtTable debt={k.debt} onChange={reload} />}39      {tab === "Manifest" && <div className="card"><JsonTree data={k.manifest} open /></div>}40      {tab === "README" && <div className="card"><pre>{k.readme}</pre></div>}41    </div>42  );43}44
Files · main
Menu

Theme