Back to Wiretrustee

dashboard — UI for agent-networks

docs/agent-networks/modules/40-dashboard.md

0.74.019.7 KB
Original Source

dashboard — UI for agent-networks

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/ and src/app/(dashboard)/agent-network/, but it also reshapes the sidebar, splits /peers, renames reverse-proxy/clustersself-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: /peers redirects to /peers/devices (src/app/(dashboard)/peers/page.tsx:7-15), /reverse-proxy/clusters was 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.

Module boundary

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.

What the UI delivers

  • AI Observability page with four tabs: Access Logs, Budget Dashboard, Budget Settings, Log Settings (replaces the standalone access-log, consumption, and global-controls routes).
  • Providers page: provider catalog + connect/edit wizard with per-vendor copy (LiteLLM, Portkey, Bifrost, Cloudflare, Vercel, OpenRouter, custom).
  • Policies page: group → provider authorization with per-policy Limits (minute-granular windows) + guardrail attach.
  • Guardrails page: reusable model-allowlist + prompt-capture sets.
  • Account controls: Log Collection / Prompt Collection / Redact PII toggles.
  • Budget rules: account-level rules reusing the policy Limits UI.
  • Control Center overlay: provider + agent-policy nodes on the graph.
  • Navigation + peers reshaping: peers split into Devices / Agents, reverse-proxy/clusters renamed to self-hosted-proxies, sidebar repackaged for agent-network focus.

Surface added

New pages

RoutePurposeBacking module(s)
/agent-networkRedirect to /agent-network/providerspage.tsx:7-15
/agent-network/providersList + connect providers; header surfaces per-account base URLproviders/page.tsx + AgentProvidersTable + AIProviderModal
/agent-network/policiesGroup → Provider authorization with per-policy Limits + Guardrail attachpolicies/page.tsx + AgentPoliciesTable + AgentPolicyModal
/agent-network/guardrailsReusable guardrail sets (model allowlist + prompt capture)guardrails/page.tsx + AgentGuardrailsTable + AgentGuardrailModal
/agent-network/observabilityTabs: Access Logs / Budget Dashboard / Budget Settings / Log Settingsobservability/page.tsx
/peers/devices, /peers/agentsSplit of /peers, shared via PeersListView keyed by kindpeers/{devices,agents}/page.tsx
/reverse-proxy/self-hosted-proxiesRenamed from clustersself-hosted-proxies/page.tsx

Removed in favor of /agent-network/observability: /agent-network/access-log, /agent-network/consumption, /agent-network/global-controls.

New modules under src/modules/agent-network

FileRole
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 + useProviderCatalogCatalog-driven brand swatch + SWR hook over /agent-network/catalog/providers
AgentPoliciesTable + AgentPolicyModal + AgentPolicyGuardrailsTab + AgentPolicyLimitsTabPolicies; modal has 3 tabs (Rule, Limits, Guardrails)
AgentGuardrailsTable + AgentGuardrailModal + AgentGuardrailBrowseModal + AgentGuardrailChecksCellGuardrails CRUD + attach-from-policy
AgentBudgetRulesTable + AgentBudgetRuleModalAccount-level budget rules; modal reuses AgentPolicyLimitsTab verbatim
AgentAccountControlsCardThree account-wide toggles (Log Collection / Prompt Collection / Redact PII)
AgentAccessLogTable + AgentAccessLogExpandedRowAccess log on /events/proxy?agent_network=true
AgentConsumptionPanel + AgentConsumptionTableToken + cost panel: charts + counter table
table/AgentProvidersTable + AgentProviderActionCellProviders table + per-row actions
data/mockData.tsDomain types and a few residual MOCK_* constants (see scrutinize)

Touched non-agent-network areas

  • control-center: agent-network overlay (provider + agent-policy nodes); removed the All Networks dropdown; hid the Networks tab in FlowSelector (FlowSelector.tsx:9-14 — enum value kept so ?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).
  • peers: Split into Devices and Agents sub-routes; shared via PeersListView keyed by kind (PeersListView.tsx:24-95). New compact-toolbar UserFilterSelector (users/UserFilterSelector.tsx).
  • reverse-proxy: Folder rename 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.
  • events: ReverseProxyEventsUserCell rewritten with user + peer fallback (ReverseProxyEventsUserCell.tsx:14-21), shared with the access-log table.
  • navigation: Full repackaging in Navigation.tsx — Agent Network items flattened (no collapsible parent), distinct icons per item; Access Control, Networks, Reverse Proxy, DNS, standalone Guardrails, Consumption, Activity removed (still URL-reachable, per lines 165-171).

Architecture & flow

Page → Provider → Table/Modal hierarchy

mermaid
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

AI Observability tab page

mermaid
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/]

Data fetch path

mermaid
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.

Public contracts consumed

  • GET/POST /api/agent-network/providers, PUT/DELETE /:id
  • GET/POST /api/agent-network/policies, PUT/DELETE /:id
  • GET/POST /api/agent-network/guardrails, PUT/DELETE /:id
  • GET/POST /api/agent-network/budget-rules, PUT/DELETE /:id
  • GET/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.

Invariants

  • Provider context wrap order on user-attribution pages: GroupsProvider > PeersProvider > AIProvidersProvider (observability/page.tsx:87-89). Reverse it and access-log group resolution silently drops names.
  • Every agent-network route checks permission?.services?.read via RestrictedAccess (observability/page.tsx:85, providers/page.tsx:184, policies/page.tsx:53, guardrails/page.tsx:55).
  • Modal 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).

Things to scrutinize

Correctness

  • Tab-state URL hand-off is one-way. observability/page.tsx:53-58 reads ?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).
  • Provider overlay runs only in 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.
  • Two useEffects race to invalidate the control-center layout. page.tsx:1655-1657 drops 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.
  • Form validation in modals is minimal. Window-seconds picker — mockData.ts:209-215 documents "minimum 60 — one minute" but there is no matching UI guard in PolicyLimitsTab; the backend validator is the enforcement point.

Security

  • No client-side enforcement claims — every cap, allowlist, and toggle is display + edit; proxy is the source of truth for deny decisions (AccessLogTable.tsx:177-191 renders backend-emitted denyReason as-is).
  • Prompt display is gated by what the backend stamps. When 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.
  • Account Controls disables 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.
  • Bifrost identity-header overrides: empty-string vs nil semantics documented in AIProvidersProvider.tsx:772-781 ("omitted = preserve, empty = explicit clear"). Mishandling could leak group attribution to a header the operator thought disabled. Focused read of Bifrost code path in AIProviderModal.tsx recommended.

Accessibility

  • Observability TabsList (observability/page.tsx:96-113) uses the shared Tabs component — should inherit Radix roving-tabindex. All four TabsTriggers carry only icon + text, no aria-label; fine because text is visible.
  • Modal focus traps are inherited from the shared Modal; agent-network modals don't override them. Quick keyboard pass recommended.
  • EndpointBadge Copy button (providers/page.tsx:66-76) has an aria-label, good.

Performance

  • 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.
  • Observability page mounts providers ONCE at page level (observability/page.tsx:87-89); tab switches keep SWR cache hot. Moving the provider mount inside TabsContent would re-fetch the access log on every switch.

Visual consistency

  • The observability tab style mirrors peers/page.tsx. Outer Tabs 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.
  • Sidebar: 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.

Test coverage

  • Cypress: One file (cypress/e2e/test.cy.ts) covering only the install-page copy-to-clipboard flow. NOTHING covers agent-network UI.
  • Component / unit tests: src/utils/version.test.ts is the only .test.* file in the repo. The agent-network modules ship without component tests.
  • Data-cy hooks exist on key controls: 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.
  • Tooling gap (pre-existing): 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.

Known limitations / explicit non-goals

  • 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.
  • Tab-state URL hand-off on observability page is one-way (read-only).
  • Access log hard-capped at 100 rows; no server-side pagination.
  • No optimistic updates. All mutations are round-trip; failures rollback via SWR revalidation.
  • 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.
  • Redirects are not query-preservingrouter.replace("/peers/devices") (peers/page.tsx:13) strips any incoming filter params.
  • Control-center cross-fetches /agent-network/{providers,policies} directly on top of AIProvidersProvider. Could be collapsed.
  • Sidebar permanently hides Access Control, Networks, Reverse Proxy, standalone Guardrails, DNS, Activity, Consumption. Routes still resolve via URL (Navigation.tsx:165-171); intentional.

Cross-references