docs/agent-networks/modules/40-dashboard.md
This module documents code that lives in the dashboard repo (under
src/modules/agent-network/ and src/app/(dashboard)/agent-network/), not
in this repo. It is co-located here so backend readers see the full picture.
Risk level: Medium. The new surface is isolated under
src/modules/agent-network/andsrc/app/(dashboard)/agent-network/, but it also reshapes the sidebar, splits/peers, renamesreverse-proxy/clusters→self-hosted-proxies, and overlays the Control Center graph. Regressions here would be cross-cutting. Backward-compat impact: Additive on the API side. Breaking on URL/navigation:/peersredirects to/peers/devices(src/app/(dashboard)/peers/page.tsx:7-15),/reverse-proxy/clusterswas renamed to/reverse-proxy/self-hosted-proxies, the sidebar lost Access Control / Networks / Reverse Proxy / DNS / standalone Guardrails / Consumption / Activity (Navigation.tsx:165-171 — routes still resolve via URL), and the standalone/agent-network/{access-log,consumption,global-controls}routes are gone in favor of/agent-network/observability.
The dashboard is the only place an operator interacts with agent-networks: provider catalog, configured providers, policies, guardrails, account-level budget rules, account settings (collection / redaction toggles), per-request access log, and consumption rollups all render, paginate, and edit here. Data flows in via SWR (useFetchApi) keyed by REST URL. One big context provider (src/modules/agent-network/AIProvidersProvider.tsx) aggregates five resources (providers, policies, guardrails, budget rules, settings) plus the proxy access-log stream filtered to agent_network=true, and exposes add* / update* / toggle* / delete* mutators that call through useApiCall and re-mutate() SWR. Pages mount the provider once at the top and compose presentational tables and modals beneath. The control-center page additionally fetches /agent-network/{providers,policies} directly (control-center/page.tsx:123-130) to overlay graph nodes.
reverse-proxy/clusters renamed to self-hosted-proxies, sidebar
repackaged for agent-network focus.| Route | Purpose | Backing module(s) |
|---|---|---|
/agent-network | Redirect to /agent-network/providers | page.tsx:7-15 |
/agent-network/providers | List + connect providers; header surfaces per-account base URL | providers/page.tsx + AgentProvidersTable + AIProviderModal |
/agent-network/policies | Group → Provider authorization with per-policy Limits + Guardrail attach | policies/page.tsx + AgentPoliciesTable + AgentPolicyModal |
/agent-network/guardrails | Reusable guardrail sets (model allowlist + prompt capture) | guardrails/page.tsx + AgentGuardrailsTable + AgentGuardrailModal |
/agent-network/observability | Tabs: Access Logs / Budget Dashboard / Budget Settings / Log Settings | observability/page.tsx |
/peers/devices, /peers/agents | Split of /peers, shared via PeersListView keyed by kind | peers/{devices,agents}/page.tsx |
/reverse-proxy/self-hosted-proxies | Renamed from clusters | self-hosted-proxies/page.tsx |
Removed in favor of /agent-network/observability: /agent-network/access-log, /agent-network/consumption, /agent-network/global-controls.
| File | Role |
|---|---|
| AIProvidersProvider.tsx (~1158 LOC) | Aggregates every agent-network resource via SWR; normalises snake↔camel; exposes mutators; holds wizard-open state |
| AIProviderModal.tsx (~1268 LOC) | Connect / edit provider wizard with per-vendor copy (Bifrost, Portkey, LiteLLM, Cloudflare, Vercel, OpenRouter, custom) |
| AIProviderLogo + useProviderCatalog | Catalog-driven brand swatch + SWR hook over /agent-network/catalog/providers |
| AgentPoliciesTable + AgentPolicyModal + AgentPolicyGuardrailsTab + AgentPolicyLimitsTab | Policies; modal has 3 tabs (Rule, Limits, Guardrails) |
| AgentGuardrailsTable + AgentGuardrailModal + AgentGuardrailBrowseModal + AgentGuardrailChecksCell | Guardrails CRUD + attach-from-policy |
| AgentBudgetRulesTable + AgentBudgetRuleModal | Account-level budget rules; modal reuses AgentPolicyLimitsTab verbatim |
| AgentAccountControlsCard | Three account-wide toggles (Log Collection / Prompt Collection / Redact PII) |
| AgentAccessLogTable + AgentAccessLogExpandedRow | Access log on /events/proxy?agent_network=true |
| AgentConsumptionPanel + AgentConsumptionTable | Token + cost panel: charts + counter table |
| table/AgentProvidersTable + AgentProviderActionCell | Providers table + per-row actions |
| data/mockData.ts | Domain types and a few residual MOCK_* constants (see scrutinize) |
?tab=networks still type-checks); wrapped ControlCenterView in AIProvidersProvider (page.tsx:73-83); agentPolicyNode clicks routed to a separate state slot (page.tsx:1871-1874). New node renderers: nodes/ProviderNode.tsx, nodes/AgentPolicyNode.tsx (registered at utils/nodes.ts:21-22).PeersListView keyed by kind (PeersListView.tsx:24-95). New compact-toolbar UserFilterSelector (users/UserFilterSelector.tsx).clusters/ → self-hosted-proxies/; deleted ClustersFeaturesCell.tsx, ClusterTypeIndicator.tsx; new ReverseProxyClusterTargetSelector for cluster target type; Private toggle on target modal; body-capture knobs removed; new ReverseProxyEventExpandedRow.ReverseProxyEventsUserCell rewritten with user + peer fallback (ReverseProxyEventsUserCell.tsx:14-21), shared with the access-log table.graph TD
Nav[Navigation.tsx]
Nav --> ProvidersPage[/agent-network/providers/]
Nav --> PoliciesPage[/agent-network/policies/]
Nav --> GuardrailsPage[/agent-network/guardrails/]
Nav --> ObsPage[/agent-network/observability/]
ProvidersPage --> AIPP1[AIProvidersProvider]
PoliciesPage --> AIPP2[AIProvidersProvider]
GuardrailsPage --> AIPP3[AIProvidersProvider]
ObsPage --> AIPP4[AIProvidersProvider]
ObsPage -.wraps.-> GroupsProvider
ObsPage -.wraps.-> PeersProvider
AIPP1 --> ProvTable[AgentProvidersTable]
ProvTable --> ProvModal[AIProviderModal]
AIPP2 --> PolTable[AgentPoliciesTable]
PolTable --> PolModal[AgentPolicyModal]
PolModal --> PolGuardTab[AgentPolicyGuardrailsTab]
PolModal --> PolLimitsTab[AgentPolicyLimitsTab]
PolGuardTab --> GuardBrowse[AgentGuardrailBrowseModal]
PolGuardTab --> GuardModal[AgentGuardrailModal]
AIPP3 --> GuardTable[AgentGuardrailsTable]
GuardTable --> GuardModal
AIPP4 --> Tabs[Tabs]
Tabs --> AccessLog[AgentAccessLogTable]
Tabs --> Consumption[AgentConsumptionPanel]
Tabs --> BudgetRules[AgentBudgetRulesTable]
Tabs --> AccountCtl[AgentAccountControlsCard]
BudgetRules --> BudgetModal[AgentBudgetRuleModal]
BudgetModal -.reuses.-> PolLimitsTab
graph LR
Page[AIObservabilityPage] --> RA[RestrictedAccess
permission.services.read]
RA --> GP[GroupsProvider]
GP --> PP[PeersProvider]
PP --> AIP[AIProvidersProvider]
AIP --> Tabs[Tabs / TabsList]
Tabs --> T1[Access Logs
AgentAccessLogTable]
Tabs --> T2[Budget Dashboard
AgentConsumptionPanel]
Tabs --> T3[Budget Settings
AgentBudgetRulesTable]
Tabs --> T4[Log Settings
AgentAccountControlsCard]
T1 -.GET.-> EP[/events/proxy?agent_network=true/]
T2 -.GET poll 5s.-> CONS[/agent-network/consumption/]
T3 -.GET/PUT.-> BR[/agent-network/budget-rules/]
T4 -.GET/PUT.-> ST[/agent-network/settings/]
graph TD
Page[Page component] --> Prov[AIProvidersProvider]
Prov -->|useFetchApi| SWR[(SWR cache
key = URL)]
SWR -.GET.-> P[/agent-network/providers/]
SWR -.GET.-> POL[/agent-network/policies/]
SWR -.GET.-> G[/agent-network/guardrails/]
SWR -.GET.-> BR[/agent-network/budget-rules/]
SWR -.GET ignoreError.-> ST[/agent-network/settings/]
SWR -.GET.-> CAT[/agent-network/catalog/providers/]
SWR -.GET pageSize=100.-> EVT[/events/proxy agent_network=true/]
Prov --> Mut[useApiCall.post/put/del]
Mut -.on success.-> MutateSWR[SWR mutate keys]
Prov --> Children[Tables / Modals via useAIProviders]
Every list view reaches management through SWR over /api/agent-network/*. The provider context maps snake-case payloads to camelCase domain types (fromAPI, policyFromAPI, guardrailFromAPI, budgetRuleFromAPI, settingsFromAPI, accessLogFromAPI — AIProvidersProvider.tsx:138-562) and back via matching *ToRequest adaptors. The access log piggy-backs on /events/proxy with agent_network=true&page_size=100 (line 707-709) and decodes LLM-specific fields from per-event metadata. Group IDs on events are resolved to current names through the surrounding GroupsProvider catalog (lines 515-521, 717-731) — no extra round trip. Mutators run *ToRequest, await useApiCall.post/put/del, call SWR mutate(), then notify. Errors caught and surfaced via notify — no exceptions escape into render. The Connect Provider modal's open state lives in the provider itself (isWizardOpen at lines 732-735) so the providers-page empty-state CTA and the table's + button share one modal. Control-center re-fetches /agent-network/{providers,policies} directly on top of AIProvidersProvider — SWR de-dupes but the code path is harder to reason about.
GET/POST /api/agent-network/providers, PUT/DELETE /:idGET/POST /api/agent-network/policies, PUT/DELETE /:idGET/POST /api/agent-network/guardrails, PUT/DELETE /:idGET/POST /api/agent-network/budget-rules, PUT/DELETE /:idGET/PUT /api/agent-network/settings (ignoreError-tolerant; 404 = not yet bootstrapped — auto-bootstrap on first provider create via bootstrap_cluster field — AIProvidersProvider.tsx:737-760)GET /api/agent-network/catalog/providers (read-only declarative; backend owns vendor list, IDs, brand colors, models, extra_headers, identity_injection — useProviderCatalog.ts:6-95)GET /api/agent-network/consumption (polled every 5s on Budget Dashboard — ConsumptionPanel.tsx:53,65-71)GET /api/events/proxy?agent_network=true&page_size=100 (shared with Proxy Events)permission?.services?.read gates every agent-network route via RestrictedAccess.AIProviderId is a closed union in dashboard types (data/mockData.ts:8-21) but the converter tolerates anything the backend ships — unknown ids fall through to "custom" (AIProvidersProvider.tsx:497-506). Catalog values are pure read-through: anything declared in extra_headers renders in the modal automatically, copy keyed by header name (EXTRA_HEADER_UI in AIProviderModal.tsx:61-89), labeled-fallback for unknown ones.
GroupsProvider > PeersProvider > AIProvidersProvider (observability/page.tsx:87-89). Reverse it and access-log group resolution silently drops names.permission?.services?.read via RestrictedAccess (observability/page.tsx:85, providers/page.tsx:184, policies/page.tsx:53, guardrails/page.tsx:55).key={open ? 1 : 0} pattern is used to force unmount/remount on close so internal useState resets between edits (AgentBudgetRuleModal.tsx:60, AgentPolicyModal.tsx:66). Removing this would leak prior-row state into a new-row session.mockData.ts is the canonical home for ALL agent-network domain types; MOCK_* constants must never reach a production code path. One leak remains (below).?tab= on mount (despite the file comment at line 28 saying URL hand-off is future) but setTab does NOT push back, so reload preserves the chosen tab only if it came in via the link. Inconsistent with control-center (page.tsx:1817-1831).applySingleGroupView / applyPeerView (control-center/page.tsx:557, 1159-1166). User view does NOT show providers — if agent-network is a primary lens, that's a gap.layoutInitialized when agentPolicies / agentProviders arrive; the main effect (1786-1799) also lists them as deps. Functional but fragile — watch for flash-of-empty-graph.updateProvider / updatePolicy / updateBudgetRule use ?? on enabled (AIProvidersProvider.tsx:784, 859, 1018). Toggle paths are safe; any caller sending enabled: false thinking "leave it off" gets existing.enabled instead. Audit modal callers.denyReason as-is).enable_prompt_collection is OFF the proxy must not put prompt/completion into event metadata; the dashboard renders whatever it gets verbatim (AccessLogTable lines 532-534, AccessLogExpandedRow.tsx:42-57). No UI filter on top of backend collection switches.Redact PII when Prompt Collection is off (AgentAccountControlsCard.tsx:122) and clears it on off-transition (line 100), but relies on backend to enforce the same gate at write — confirm PUT handler rejects redact_pii=true && enable_prompt_collection=false.aria-label; fine because text is visible.EndpointBadge Copy button (providers/page.tsx:66-76) has an aria-label, good.AgentConsumptionPanel polls /agent-network/consumption every 5s (ConsumptionPanel.tsx:53,70). Tab switches unmount the panel, so the poll stops — verify in network panel.AgentAccessLogTable is hard-capped at 100 rows via page_size=100 (AIProvidersProvider.tsx:707-709). Server-side pagination is future work; high-traffic tenants miss everything past row 100 — known limitation.TabsContent would re-fetch the access log on every switch.pt-4 pb-0 mb-0, TabsList px-8 (observability/page.tsx:94-96) — confirm chrome height matches so the page doesn't visually jump.Boxes for Providers, AccessControlIcon for Policies, TelescopeIcon for AI Observability (Navigation.tsx:113,120,133). Reusing AccessControlIcon makes Policies look identical to the (now hidden) Access Control item — if Access Control ever comes back, they collide.AgentNetworkIcon is used in breadcrumbs on every agent-network page but NOT in the sidebar (per-page icons instead). Deliberate departure — record so it doesn't get reverted.cypress/e2e/test.cy.ts) covering only the install-page copy-to-clipboard flow. NOTHING covers agent-network UI.src/utils/version.test.ts is the only .test.* file in the repo. The agent-network modules ship without component tests.save-account-controls (AgentAccountControlsCard.tsx:71), enable-log-collection, enable-prompt-collection, redact-pii, plus existing data-cy={policy.name} / data-cy={provider.name} on ActiveInactiveRow. Sufficient hooks for Cypress flows; none written yet.npm run lint (next lint) is broken in Next 16 — the lint subcommand was removed from the Next CLI in 16.x, so the dashboard effectively has no working lint gate. The fix is to add either a flat-config eslint . script or wire ESLint via an explicit eslint-config-next invocation.data/mockData.ts still contains MOCK_GROUPS, MOCK_PROVIDERS, MOCK_PEERS. Only MOCK_GROUPS is referenced from production — AgentPoliciesTable.tsx:45,76 uses it as a name-lookup fallback when a policy references a group ID the real GroupsProvider doesn't know about. MOCK_PROVIDERS / MOCK_PEERS are unreferenced; safe to delete. The file is /* eslint-disable */ so dead-code warnings don't flag them.FlowView.NETWORKS retained but hidden from FlowSelector (FlowSelector.tsx:9-14). Old ?tab=networks links still route to the hidden view because applyNetworksView still runs.router.replace("/peers/devices") (peers/page.tsx:13) strips any incoming filter params./agent-network/{providers,policies} directly on top of AIProvidersProvider. Could be collapsed.