Audit & stats endpoints
Read-only endpoints the SPA hits for the dashboard. All tenant-scoped.
GET /audit?limit=N
Recent decisions, newest first. limit defaults to 100.
GET /events (SSE)
text/event-stream. One data: line per new decision. Caddy is configured with flush_interval -1 so chunks aren't buffered.
GET /stats?window=N
Aggregates over the last N seconds. Default 3600.
{
"window_seconds": 3600,
"total_decisions": 153,
"by_action": { "allow": 142, "redact": 8, "deny": 3 },
"top_rules": [ { "key": "redact_secrets_on_egress", "count": 8 }, ... ],
"top_agents": [ { "key": "support-bot-v2", "count": 1000, "ip": "10.0.3.42", ... }, ... ],
"top_blocked_agents": [ { "key": "...", "count": 3, ... }, ... ],
"top_blocked_hosts": [ { "key": "...", "count": 2 }, ... ]
}top_* fields are lists of {key, count} (agent lists carry network identity too), not maps. There are more rollups than shown (shadow agents, anomaly/approval counts, token/cost totals, principal counts) — the dashboard consumes the ones it needs.
GET /vocabulary
Distinct values observed in the audit data — used to populate filter autocomplete on Live Traffic.
{
"agents": ["support-bot-v2", "qa_eval_loop", ...],
"actions": ["http_request", "tool_call", "llm_prompt"],
"hosts": ["api.anthropic.com", "api.openai.com", ...],
"tools": ["shell.exec", "fs.read", ...],
"models": ["claude-sonnet-4-6", "gpt-5-mini", ...]
}GET /audit/export?format=csv|json&window=N
Same data as /audit but as a download with Content-Disposition set (capped at 10,000 rows). format=csv is what the dashboard's Export button uses; format=json returns the same rows as JSON.
GET /principals/summary
Per-principal-type counts (people vs. agents vs. unknown):
{
"user": { "count": 12, "names": [...] },
"agent": { "count": 8, "names": [...] },
"unknown": { "count": 0, "names": [] }
}