Admin UI (trails-admin) — M10 Phases 3 + 4¶
Status: pre-alpha. Phase 3 MVP described in ADR-0019 §3. Phase 4 adds the graph browser and cost/budget views; both land as plain text tables backed by SPARQL / :class:
CostTracker— no React, no Cytoscape.
trails-admin is an optional operator UI that renders every
@capability registered in your app as an auto-generated form, shows
the PROV-O trail for every invocation, and surfaces the ingestion
dashboard for trails.ingest.
It lives in a SEPARATE top-level package (trails_admin) so import
trails never pulls in FastAPI. Install with:
That puts trails-admin on $PATH (see python/pyproject.toml
[project.scripts]). To launch it:
--app is the Python import path of the module that registers your
capabilities (it's imported for its side effects — the @capability
decorator writes into the shared singleton registry). Without it the
admin UI starts empty.
Routes¶
| Route | Description |
|---|---|
GET / |
Home page with links to the three sub-dashboards. |
GET /capabilities |
List every registered @capability. |
GET /capabilities/{id} |
Auto-generated invocation form. |
GET /capabilities/{id}/schema |
JSON descriptor of parameters. |
POST /runs/new |
Form target — invokes, renders the run page. |
GET /runs |
Recent invocations (in-memory cache). |
GET /runs/{trace_id} |
Run inspector: envelope + PROV-O triples. |
GET /runs/{trace_id}.json |
Same, as JSON. |
GET /ingestion |
Document list + new-ingest form. |
GET /ingestion/list |
JSON projection of the document list. |
POST /ingestion/new |
Triggers trails.ingest.ingest_file. |
GET /graph |
List registered @node_type classes (Phase 4). |
GET /graph/{label} |
Show up to 100 instances of a label. |
GET /graph/subject/{iri} |
Every triple with this IRI as subject. |
GET /cost |
Cost tracker records, filterable (Phase 4). |
GET /cost/records.json |
JSON projection of the record table. |
GET /cost/summary |
Totals + top-10 capabilities / principals. |
GET /cost/summary.json |
JSON projection of the summary. |
Auto-generated forms¶
The form renderer walks inspect.signature(handler) (with PEP 563
string-annotations resolved via typing.get_type_hints) and picks an
input type per parameter:
| Python annotation | <input type> |
|---|---|
int |
number (step="any") |
float |
number (step="any") |
bool |
checkbox |
| anything else | text |
Parameters with a default value are not marked required. When the
form is submitted the route coerces each value back to the declared
type before calling trails.invoke.
Run inspector¶
After invocation the admin UI renders:
- The response payload, pretty-printed.
- The full envelope (including
provenance,cost,trace_id). - A table of every triple in the PROV graph whose subject mentions the
run's
trace_id— the same SPARQL shapetrails.prov_explorer.ProvenanceExploreruses.
Ingestion dashboard¶
Lists every trails.ingest.Document in the KG with its chunk count
(computed via Chunk.where(document=doc)). The form at the top accepts
a local file path and calls trails.ingest.ingest_file with a fresh
context bound to the singleton kernel store — so a Document ingested
from the admin UI shows up in the Python surface and vice-versa.
Why HTMX + Jinja2, not React?¶
ADR-0019 §3 originally proposed a React + Vite + shadcn/ui bundle shipped prebuilt inside the wheel. The MVP defers that for a straightforward reason: a React bundle is a distribution bomb for a three-view admin surface. It adds:
- Node + Vite + shadcn as contributor dependencies.
- A reproducible-bundle CI step (ADR-0014) to reach Phase 4 parity.
- A JS build pipeline entering the main repo purely to serve three list/form/detail views.
For the MVP a single index.html + per-route server-rendered pages
delivers the same user-visible surface with none of that. If and when
a Phase-4 graph browser actually needs real client-side state, we can
revisit the React bundle; the FastAPI surface under trails_admin
stays unchanged either way, because the admin sub-app talks to its
front-end through plain HTML / JSON responses.
Graph browser (Phase 4)¶
GET /graph lists every class registered with @node_type — one row
per class with its label, derived iri, declared fields, and any
extends parents. Click a label to see up to 100 instances of that
type rendered as a table keyed by IRI + declared fields; click the IRI
to see every triple with that subject.
Design notes:
- Text tables only — no interactive visualisation. Cytoscape.js / D3 / vis-network were considered and rejected for the MVP for the same reason we deferred the React bundle: a three-view admin surface doesn't need a layout engine. Phase 4's goal is to make the graph queryable without opening the Python REPL, and a triple table satisfies that. When a real operator workflow surfaces a genuine need for node/edge layout (e.g. inspecting a policy graph or a PROV-O chain visually), add it behind a route that lazy-loads the bundle so the text-table path stays zero-JS.
- SPARQL caps at
LIMIT 100per query. The page shows the number actually returned so an operator notices when they hit the cap. - Long IRIs: every IRI renders inside a
<code class=iri>tag withword-break: break-allso a 150-character trails-dev IRI wraps instead of horizontal-scrolling the whole table. Thegraph.csssheet carries the wrapping rule. - Default graph only. The browser reads from the default graph
today; named-graph filtering and
extendstraversal are future sprints.
Cost & budget views (Phase 4)¶
GET /cost renders every record on the default CostTracker
(trails.llm._module_tracker()) as a table with capability,
tokens, usd, latency_ms, call_id, parent_call_id, and the
dedupe flag. Query parameters apply an in-process filter:
?capability=<substring>— case-insensitive substring match on the capability id column.?principal=<id>— matches records that carry aprincipalattribute (the current_CostEntryshape does not yet, but the knob is wired so future schema additions flow through unchanged).?limit=<int>— cap the rows rendered (default 100, max 1000).
GET /cost/summary aggregates:
- Total USD, total tokens, total latency, invocation count — all derived from non-child entries so nested planner / capability / LLM envelopes don't multi-count.
- Top 10 capabilities by USD spend.
- Top 10 principals by spend — sourced from the tracker's budget
map (
CostTracker.set_budget(...)populates that). Records alone don't carry a principal in today's schema.
Zero-records / zero-budget is handled by a clean empty state — the
aggregates page renders totals as $0.0000 with an explicit note
explaining that's the clean-boot state, not an error.
Scope fence¶
- No authentication in the MVP. The admin app MUST only be bound to
127.0.0.1in dev. Cedar-gated admin routes land alongside the ADR-0019 Open Question #1 resolution (DID+biscuit vs. session cookie). - No interactive graph visualisation. Phase 4 ships text tables; a layout engine (Cytoscape et al.) is a later sprint if operator feedback demands it.
- No budget mutation. Cost views are read-only. Writable budgets live in the Python surface per ADR-0019.
- List-valued / dataclass params render as a plain text input. The
auto-form is deliberately minimal; future sprints can wire
input_shapemetadata into the renderer once a compelling case appears.