AegisAgent — SOC Console Design System & Frontend Architecture¶
Status: Design (2026-06-24). Document type: Product Design System + Frontend Architecture. Companion to
AegisAgent_SOC_Console_HLD_LLD.md(the engineering HLD/LLD: datasource, panel framework, dashboard schema, AQL, drilldown, performance) andAegisAgent_SOC_UI_Design.md(the product/UX design). This doc owns the layers those two do not: brand identity, theme tokens, design language, application shell, the component library, and the investigation/approval/timeline experiences. Where architecture is already specified (panels do not fetch;PanelRuntimefetches; dashboard JSON controls composition), this doc references it and does not re-derive it.
1. Executive summary¶
The AegisAgent SOC Console is not a monitoring dashboard. It is a mission-control surface for AI agent governance — where a human operator watches autonomous agents act, approves the dangerous ones against cryptographically frozen evidence, and proves after the fact exactly what happened. Three things on this screen exist nowhere in Grafana, Kibana, Datadog, or Splunk: the Approval Card (a human binding their sign-off to exact bytes), the Provable Timeline (a hash-chained incident you can verify in one click), and the Receipt Integrity viewer (tamper detection as a first-class view).
The design system therefore optimizes for a specific operator: a security analyst scanning many low-signal streams for the few high-stakes moments, who must act fast and be right. That dictates everything downstream — high information density, evidence-forward visual hierarchy, restrained motion, and a dark-first palette where one accent color carries trust and severity carries meaning, never decoration.
This document specifies: the Aegis visual identity (§3), a three-theme token system (Light / Dark SOC / OLED, §3.4), the design language (density, hierarchy, motion, §2), the application shell (collapsible nav + Grafana-style control bar, §4), the component library with per-component HLD/LLD (§5–§6), the frontend folder structure (§7), the panel catalog including two new AegisAgent panels — Agent Risk Map and Decision Graph (§9), the investigation / approval / timeline UX (§8), accessibility (§10), and a Phase 0→5 roadmap reconciled with the HLD/LLD's U0–U6 (§11).
Anti-template mandate: every surface here must read as a deliberate security product, not a shadcn starter. The checklist in §12 enforces it.
2. Design language¶
2.1 The operating principle — evidence over ornament¶
Each pixel earns its place by helping an operator make or prove a decision. A panel that looks impressive but doesn't change what the analyst does next is removed. This is the inverse of a marketing dashboard.
2.2 Information density¶
SOC operators monitor many signals at once. The console uses a compact density baseline, denser than a typical SaaS app:
| Token | Value | Use |
|---|---|---|
--row-height-compact |
28px |
table/feed rows (default) |
--row-height-cozy |
36px |
investigation tables where payloads matter |
--space-panel-pad |
12px |
inside panels (not 24px SaaS default) |
--space-grid-gap |
12px |
between panels |
--font-size-data |
12.5px |
tabular data, log lines |
--font-size-label |
10.5px / uppercase / 0.04em tracking |
panel titles, field labels |
A density toggle (Compact / Cozy) lives in the user menu and rewrites the --row-height-* and --space-* custom properties at the :root scope — no component re-render, pure CSS variable swap.
2.3 Visual hierarchy (four scales)¶
| Scale | Carries hierarchy through | Rule |
|---|---|---|
| Page | left-nav active state + breadcrumb + a single H1-equivalent (--font-size-page, 18px) |
one page = one job; no competing titles |
| Dashboard | rows/tabs; a stat row always leads (the "vital signs"), detail panels below | the most decision-relevant number is top-left |
| Panel | a 10.5px uppercase title + a dominant value or chart; chrome recedes (--text-muted) |
the data is the brightest thing in the panel |
| Alert / severity | color + icon + weight, never color alone | critical = rose, filled; lower severities = outline |
Hierarchy is built from scale contrast and weight, not boxes-within-boxes. Elevation (surface layering, §3.4) separates concerns; borders are hairline and recede.
2.4 Motion¶
Motion is functional only. It communicates state change or progress; it never decorates.
| Allowed | Duration / easing | Why |
|---|---|---|
| Live event arrival (feed row slides in + brief highlight fade) | --duration-fast 120ms, ease-out |
draws the eye to new evidence |
| State transition (decision badge allow→deny, agent active→frozen) | 150ms cross-fade |
confirms the system changed |
| Verification progress (chain walk, indeterminate → resolved) | stepped, --duration-normal 240ms per link |
makes proof feel earned, not instant-magic |
| Drawer / command palette open | 180ms transform+opacity |
spatial continuity |
Forbidden: parallax, decorative gradients-in-motion, looping ambient animation, springy bounce on data, skeleton shimmer longer than 400ms. All motion respects prefers-reduced-motion (collapses to instant state swaps; progress becomes a static stepped indicator). Animate only compositor-friendly properties (transform, opacity) — never layout-bound ones.
3. Visual identity — the Aegis brand¶
3.1 Brand personality¶
| Trait | What it means on screen | Why it fits AegisAgent |
|---|---|---|
| Secure | locked-down chrome, no playful affordances, fail-closed empty states | the product is a security control |
| Evidence-driven | hashes, receipts, and provenance are visible first-class data, not metadata | the moat is provability (Gap Reassessment §3) |
| Autonomous | the UI watches machines acting on their own; agents are first-class "citizens" with status/risk | the subject is autonomous agents, not human users |
| Defensive | severity and trust drive color; the resting state is calm, alarms are loud | a SOC is calm until it isn't |
| Enterprise / technical | dense, keyboard-first, monospace for all identifiers | the buyer is a security engineer, not a consumer |
The personality resolves a tension: the product must feel trustworthy and calm (you stake production agent actions on it) while making threats unmistakable (an injection attempt must scream). The answer: a low-chroma, high-contrast resting palette where a single saturated accent and the severity ramp are the only loud colors — so when they appear, they mean something.
3.2 The Aegis Mark (logo direction — not a generic shield)¶
The mark encodes the three things that make AegisAgent unique — the gate (authorization boundary), the chain (receipt provenance), and oversight (the human watching) — in one glyph.
╭───────────╮ Concept: "The Verified Aperture"
╱ ◜─────◝ ╲
│ ╱ ▲ ╲ │ ◀ outer hexagon = the gate (the authorization boundary)
│ │ ╱ ╲ │ │ ◀ inner aperture = oversight (an eye / a lens, watching)
│ ╲ ▼ ╱ ╱ ◀ the aperture's lower notch resolves into a ✓ when state = verified
╲ ◟─────◞ ╱
╰────●────╯ ◀ the single anchor node = the genesis link of the receipt chain
- Primary logo: the Mark +
AegisAgentwordmark (geometric grotesque, see §3.3), the word "Agent" set in--text-secondaryso "Aegis" leads. - Icon / app favicon: the Mark alone — a hexagonal aperture. At 16px it reads as a faceted ring with a center dot (the chain's genesis node).
- Collapsed sidebar icon: the Mark at 24px, monochrome
--brand; the aperture's notch animates to a ✓ for ~600ms on a successful global verify (the only place the logo ever animates — a deliberate, rare "all clear" signal). - Symbol meaning: hexagon = a sealed boundary (six sides nod to the six trust levels); aperture = human oversight (Article 14); the anchor node + implied links = the hash chain; the notch-to-check = verifiability. No sword, no padlock, no generic shield silhouette.
- Motion signature: on receipt-chain verification anywhere in the app, the Mark briefly draws its chain links left-to-right (stepped, reduced-motion-safe) — the brand is the verification gesture.
3.3 Typography¶
Two families, deliberately paired:
| Role | Family | Rationale |
|---|---|---|
| Display / UI | Geist (or Inter Tight as fallback) — geometric grotesque | technical, neutral, excellent at small sizes & dense tables |
| Mono / evidence | Geist Mono (or JetBrains Mono) | every identifier — action_hash, receipt_hash, agent IDs, payloads, AQL — is monospace; evidence must be unambiguous and copy-exact |
Type scale (clamp() for the few responsive sizes; fixed px for dense data):
--font-size-page: 18px; /* page H1 */
--font-size-section:14px; /* row/section titles */
--font-size-body: 13px;
--font-size-data: 12.5px;/* tables, logs */
--font-size-label: 10.5px;/* uppercase field labels, 0.04em tracking */
--font-size-hero-stat: clamp(28px, 2vw + 1rem, 40px); /* the big stat number */
Rule: max two families, font-display: swap, subset to Latin + the box-drawing/symbol glyphs the timeline uses, preload only the one critical weight (UI Medium 500).
3.4 Color system & themes¶
Tokens are defined in OKLCH (perceptually uniform — severity ramps stay legible across themes) and exposed as CSS custom properties. Three themes share one token contract; only values change.
Semantic token contract (theme-independent names):
/* Brand */
--brand /* AegisAgent indigo — the ONE saturated accent */
--brand-emphasis /* hover/active brand */
--brand-subtle /* brand at low alpha for fills/selection */
/* Interactive */
--interactive-fg, --interactive-bg, --interactive-bg-hover, --focus-ring
/* SOC severity & decision ramp (meaning, never decoration) */
--sev-critical /* rose */ --decision-deny /* red */
--sev-high /* red */ --decision-approval /* amber */
--sev-medium /* amber */ --decision-allow /* green */
--sev-low /* sky */ --state-verified /* emerald*/
--sev-info /* slate */ --state-failed /* rose */
--state-pending /* amber, pulsing dot */
/* Trust-provenance ramp (the 6 levels — a dedicated, ordered scale) */
--trust-internal-signed /* emerald */ --trust-external /* red */
--trust-internal-unsigned /* sky */ --trust-malicious /* rose, bold */
--trust-customer /* amber */ --trust-unknown /* slate */
/* Surface elevation (4 layers — depth via surface, not shadow) */
--surface-app /* deepest — page background */
--surface-panel /* panel background */
--surface-elevated /* popover/drawer/hover-row */
--surface-modal /* modal/dialog */
--surface-overlay /* scrim */
/* Text */
--text-primary, --text-secondary, --text-muted, --text-on-brand
/* Borders (hairline; recede) */
--border-default, --border-hover, --border-active, --border-focus
Theme value tables (representative anchors; full ramp generated from these):
| Token | Light | Dark SOC (default) | OLED |
|---|---|---|---|
--surface-app |
oklch(98.5% 0 0) |
oklch(20% 0.02 255) ≈ #0f172a |
oklch(0% 0 0) #000000 |
--surface-panel |
oklch(100% 0 0) |
oklch(24% 0.02 255) ≈ #111827 |
oklch(8% 0.01 255) |
--surface-elevated |
oklch(97% 0 0) |
oklch(28% 0.02 255) |
oklch(13% 0.01 255) |
--text-primary |
oklch(22% 0.02 255) |
oklch(92% 0.01 255) ≈ #e2e8f0 |
oklch(96% 0 0) |
--text-muted |
oklch(55% 0.02 255) |
oklch(60% 0.02 255) ≈ #64748b |
oklch(58% 0.02 255) |
--border-default |
oklch(90% 0.01 255) |
oklch(34% 0.02 255) ≈ #334155 |
oklch(18% 0.01 255) |
--brand |
oklch(55% 0.20 270) |
oklch(62% 0.20 270) (indigo-500/600) |
oklch(64% 0.21 270) |
--decision-deny |
oklch(58% 0.20 25) |
oklch(64% 0.21 25) |
oklch(66% 0.22 25) |
--decision-allow |
oklch(60% 0.16 150) |
oklch(70% 0.17 150) |
oklch(72% 0.18 150) |
--state-verified |
oklch(60% 0.15 160) |
oklch(72% 0.16 160) |
oklch(74% 0.17 160) |
--trust-malicious |
oklch(55% 0.22 15) |
oklch(62% 0.24 15) |
oklch(64% 0.25 15) |
Notes:
- The Dark SOC theme is the default and matches the existing ui/ palette (#0f172a / #e2e8f0 / #334155) — adopting it means zero visual regression on the current console while formalizing the tokens.
- OLED is true-black for 24/7 wall displays and battery; it raises panel surfaces slightly so panels read as objects floating on black, and slightly boosts severity chroma to survive the higher contrast.
- Light exists for daytime/print/evidence-export readability; it is held to the same WCAG AA contrast (§10) and is not an afterthought (Design-Quality rule: both themes must feel intentional).
- Depth comes from the four surface layers, not heavy shadows. Shadows are a single hairline --border-default + at most one soft elevation shadow on --surface-elevated/modal.
- Severity and trust are reserved palettes. No UI chrome may use the severity ramp decoratively. This is what makes a red row mean something.
4. Application shell¶
The shell is the persistent frame around every page: left navigation + top control bar + the routed content region. Implemented as ui/src/chrome/AppShell.tsx; replaces today's monolithic page.tsx switch.
4.1 Left navigation¶
┌──────────────┐ Expanded (240px) Collapsed (56px)
│ ◈ AegisAgent │ ◀ Mark + wordmark ◈ ◀ Mark only
│ ──────────── │
│ ▦ Overview │ active: --brand-subtle bg, ▦ ◀ icon only; tooltip on hover
│ ⌕ Explore │ left 2px --brand bar, ⌕ (Radix tooltip, 400ms delay)
│ ◭ Incidents │ --text-primary ◭
│ ◮ Detections │ badge: count chip (deny/ ◮• ◀ badge becomes a dot
│ �ស Approvals 3│ pending) right-aligned ⏸③
│ ⬡ Agents │ ⬡
│ ⛁ MCP │ ⛁
│ ⛓ Receipts │ ⛓
│ ▤ Dashboards │ ▤
│ ──────────── │
│ ⚙ Settings │ ⚙
│ « collapse │ ◀ pin/collapse toggle »
└──────────────┘
- State ownership:
consoleStore.navCollapsed(Zustand, persisted tolocalStorage); collapse is a CSS-grid column-width change, not a remount. - Active state: route-derived (
usePathname), never local state — deep links light the correct item. - Badges: live counts from the SSE stream (
Approvalspending,Detectionsfiring). Collapsed → badge degrades to a colored dot. Badge color = the relevant severity/state token. - Keyboard shortcuts:
gthen a letter (Gmail-style chord) —g oOverview,g eExplore,g iIncidents,g aApprovals, etc.;[toggles collapse;?opens the shortcut cheatsheet. Registered centrally inuseGlobalHotkeys()so they're discoverable and conflict-free. - Icons:
lucide-react(already a dependency), one consistent stroke weight; the nav never mixes filled/outline.
4.2 Top control bar (chrome/ControlsBar.tsx + chrome/TimeRangePicker.tsx)¶
Grafana/Kibana-style global controls. The single most important shell decision: these controls own global query context, and changing any of them invalidates every PanelRuntime query key (see HLD/LLD §7.4).
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ [tenant ▾] │ $agent ▾ $env ▾ │ ⌕ search… (⌘K) │ [◷ Last 24h ▾] [⟳ 10s ▾] [● Live] │ 🔔③ │ ◐ │ 𝗨 ▾ │
└────────────────────────────────────────────────────────────────────────────────────────┘
tenant template vars command palette time range / refresh / live notif theme user
| Control | Component API (props) | State owner | Behavior |
|---|---|---|---|
| Tenant selector | <TenantSelect value onChange options> |
consoleStore.tenant |
lists only entitled tenants; switching clears all caches (no cross-tenant bleed) |
| Template variables | <VariableControl def value onChange> |
consoleStore.variables[name] |
chained vars re-query when their parent changes; URL-synced |
| Global search / palette | <CommandPalette> (⌘K/Ctrl K) |
local + consoleStore |
fuzzy: pages, agents, incidents, "verify receipt …", "freeze agent …" — actions, not just nav |
| Time range | <TimeRangePicker value onChange> |
consoleStore.timeRange |
relative tokens (now-24h) + absolute; quick presets; URL-synced |
| Refresh interval | <RefreshSelect> |
consoleStore.refreshSec |
off/5s/10s/30s/1m; drives refetchInterval |
| Live toggle | <LiveToggle> |
consoleStore.live |
turns on SSE subscriptions (feed panels, badges); pulsing --state-pending dot when active |
| Notifications | <NotificationsBell count> |
SSE | opens a drawer of recent alerts/approvals |
| Theme | <ThemeToggle> |
consoleStore.theme |
Light / Dark SOC / OLED; writes data-theme on <html> |
| User menu | <UserMenu role> |
session | role, density toggle, sign-out, shortcut cheatsheet |
All control-bar state is URL-synced (search params) so any view is a shareable link — the Grafana/Kibana "state in the URL" pattern and the repo's "URL as state" web rule.
5. Component library — HLD¶
5.1 Folder structure (ui/src/)¶
ui/src/
├── app/ # Next.js App Router ent
│ ├── layout.tsx # providers + AppShell mount
│ ├── (console)/…/page.tsx # one route per nav item → renders a Dashboard or a Page
│ └── providers.tsx # TanStack Query, theme, hotkeys
├── chrome/ # the shell
│ ├── AppShell.tsx LeftNav.tsx ControlsBar.tsx TimeRangePicker.tsx
│ ├── CommandPalette.tsx NotificationsDrawer.tsx UserMenu.tsx
├── design-system/ # tokens + primitives (the "ui/" layer)
│ ├── tokens.css # all CSS custom properties, per-theme blocks
│ ├── Button.tsx Badge.tsx Card.tsx Input.tsx Select.tsx
│ ├── Drawer.tsx Modal.tsx Tooltip.tsx Toast.tsx Tabs.tsx Skeleton.tsx
│ └── primitives/ # Radix wrappers (a11y baked in)
├── components/
│ ├── security/ # AegisAgent-specific shared UI
│ │ ├── HashChip.tsx TrustBadge.tsx DecisionBadge.tsx SeverityTag.tsx
│ │ ├── CanonicalActionView.tsx VerifyButton.tsx ExpiryCountdown.tsx
│ ├── tables/ # DataGrid (TanStack Table + virtual) + cell renderers
│ ├── charts/ # chart adapters (Recharts now; uPlot/ECharts later)
│ └── filters/ # FilterBuilder, AqlInput, FieldSidebar
├── panels/ # PANEL FRAMEWORK — see HLD/LLD §7
│ ├── PanelRuntime.tsx PanelContainer.tsx registry.ts types.ts
│ ├── standard/ # Stat TimeSeries Table Heatmap Status Feed
│ └── differentiators/ # ApprovalCard ProvableTimeline ReceiptIntegrity
│ │ # AgentRiskMap DecisionGraph
├── dashboards/ # DASHBOARD MODEL — see HLD/LLD §7.3
│ ├── DashboardLoader.tsx schema.ts drilldown.ts
│ └── system/ # curated dashboards-as-code (overview.ts, fleet.ts, …)
├── datasources/ # DATASOURCE LAYER — see HLD/LLD §5
│ ├── types.ts gatewayEntity.ts socQuery.ts receipt.ts stream.ts registry.ts
│ └── aql/ # parser, autocomplete, AST→params
├── pages/ # non-dashboard pages (Explore, Rules, Settings)
├── state/ # consoleStore.ts (Zustand) + url-sync.ts
├── hooks/ # useSocStream useDrilldownRouter useGlobalHotkeys useTheme
└── lib/ # format.ts (hash/time/bytes) color.ts validate.ts (zod)
Folder responsibilities: design-system/ = brand primitives, zero domain knowledge (a Button knows nothing about receipts). components/security/ = the reusable AegisAgent vocabulary (HashChip, TrustBadge) shared by panels and pages. panels/, dashboards/, datasources/ = the framework (specified in the HLD/LLD doc). pages/ = the few surfaces that are not dashboards (Explore, Rules, Settings). This is feature/surface organization, not file-type organization (repo web coding-style rule).
5.2 Component HLD table¶
| Component | Layer | Purpose | Built on |
|---|---|---|---|
Button |
design-system | all actions; variants encode intent | Radix Slot |
Badge / SeverityTag |
design-system / security | status & severity, icon + label always | — |
Card / PanelContainer |
design-system / panels | the panel frame: title, toolbar, state slots | — |
MetricCard (Stat) |
panels/standard | a single decision-relevant number | Recharts spark |
DataGrid |
tables | virtualized high-row-count tables | TanStack Table + react-virtual |
Timeline / ProvableTimeline |
security / panels | ordered events; provable variant adds receipt+verify | — |
SearchBar / AqlInput |
filters | AQL query entry + autocomplete | — |
FilterBuilder |
filters | click-to-build field:value filters | — |
FieldSidebar |
filters | Discover-style facet sidebar | datasource fields() |
CommandPalette |
chrome | ⌘K nav + actions | Radix Dialog + cmdk |
Drawer / Modal |
design-system | side context / blocking confirm | Radix Dialog |
Toast |
design-system | non-blocking action feedback | Radix Toast |
Tooltip |
design-system | dense-mode disclosure | Radix Tooltip |
HashChip |
security | truncated mono hash + copy + drilldown | — |
TrustBadge / DecisionBadge |
security | the 6 trust levels / decision ramp | — |
CanonicalActionView |
security | the exact bytes an approval covers | — |
VerifyButton |
security | trigger + render chain verification | datasource verifyReceipt() |
6. Component library — LLD (representative contracts)¶
Every component obeys the repo TypeScript/React rules: named type Props, destructured params, explicit public types, no any, presentational components are pure (data in, JSX out). Each defines its states (loading / empty / error / success where applicable) and variants.
6.1 Button¶
type ButtonProps = {
variant?: "primary" | "secondary" | "ghost" | "danger" | "verify";
size?: "sm" | "md";
isLoading?: boolean;
disabled?: boolean;
disabledReason?: string; // renders as a tooltip — never silently disable a security action
leftIcon?: React.ReactNode;
onClick?: () => void;
children: React.ReactNode;
};
primary = --brand; danger = --decision-deny (freeze/revoke/reject); verify = --state-verified outline (the receipt-verify gesture has its own visual identity).
- States: isLoading shows an inline spinner and blocks re-click; disabled must pair with disabledReason for any security action (RBAC gating shows why, per §8.4 / HLD §9).
- A11y: real <button>, focus ring --border-focus, aria-busy when loading.
6.2 HashChip (the most-used security primitive)¶
type HashChipProps = {
hash: string; // full sha256 (e.g. "sha256:9af1…")
kind: "action" | "receipt" | "manifest";
truncate?: number; // default 8 head + 4 tail
onDrilldown?: () => void; // e.g. open Receipt Integrity at this hash
};
--text-secondary, with a copy affordance on hover (copies the full hash, not the truncated display — evidence integrity). kind sets a 1px left accent (action=brand, receipt=verified, manifest=sky). Click → onDrilldown. Never wraps; never shows raw secrets.
6.3 DataGrid (high-row-count tables)¶
type DataGridProps<TRow> = {
data: DataFrame; // columnar — from PanelRuntime, never fetched here
columns: ColumnDef<TRow>[]; // TanStack Table defs
rowKey: (row: TRow) => string; // stable key — never index
onRowClick?: (row: TRow) => void;
density?: "compact" | "cozy";
emptyLabel?: string;
isLoading?: boolean;
error?: string;
};
@tanstack/react-virtual) — renders only visible rows; handles 1,000+ rows at 60fps. Cell renderers map Field.format → HashChip / TrustBadge / DecisionBadge / relative-time. States: loading = 8 skeleton rows; empty = emptyLabel + a hint; error = inline --state-failed banner with the message (never a blank grid). Column sort is client-side for the loaded page, server-side for full-dataset sort (cursor).
6.4 CanonicalActionView (approval safety)¶
type CanonicalActionViewProps = {
action: CanonicalAction; // the exact, frozen, hashed bytes
actionHash: string;
edited?: { diff: ActionDiff }; // when an approver edited; shows a before/after diff
};
action_hash shown adjacent via HashChip. If edited, shows a diff and a banner: "Editing re-hashes and re-evaluates — you are approving the new bytes." This component is the literal embodiment of the approval-integrity moat; it must never render a "friendly" summary that diverges from the bytes (the render-vs-bytes attack).
6.5 VerifyButton + verification states¶
type VerifyButtonProps = {
receiptId: string;
range?: { fromId: string; toId: string }; // verify a chain segment
onResult?: (r: VerifyResult) => void;
};
// VerifyResult from HLD/LLD §5.1: { ok, brokenAtRow?, message, chainHead? }
verify variant) → walking (stepped progress, one tick per link, reduced-motion = static "verifying N links") → ok (--state-verified "tamper-free …a1b2 → …c3d4") → failed (--state-failed "broken at event 42", with a jump-to-row action). The button's result is also surfaced on the row(s) it covers.
7. Frontend architecture (HLD)¶
Browser (Next.js App Router)
│
┌──────────────────────▼───────────────────────┐
│ AppShell (LeftNav · ControlsBar · routes) │ owns global query context
└──────────────────────┬───────────────────────┘
│ consoleStore (tenant, timeRange, variables, live, theme, role)
┌──────────────────┼─────────────────────────┐
▼ ▼ ▼
DashboardLoader Page (Explore/Rules/Settings) CommandPalette
(schema → grid) │ (nav + actions)
│ │
▼ ▼
PanelRuntime ◀── the ONLY data-fetching layer (TanStack Query) ──▶
│ builds QueryRequest from (panel.query + timeRange + variables)
▼
Panel component ◀── pure: (DataFrame, state) → JSX; never fetches
│ emits onDrilldown → useDrilldownRouter
▼
Datasource layer (GatewayEntity · SocQuery/AQL · Receipt · Stream)
│ tenant context attached; AQL sent as structured AST (no interpolation)
▼
AegisAgent Gateway (Rust/Axum) — tenant-scoped, fail-closed, parameterized SQL
Responsibility boundaries (the load-bearing rule):
- AppShell / consoleStore own global query context. Nothing else writes time/tenant/vars.
- DashboardLoader owns composition — it reads a DashboardSchema and lays out PanelRuntimes. It knows nothing about data.
- PanelRuntime owns fetching — the single place TanStack Query is called for panels. Cache key = [panelId, query, timeRange, variables, tenant].
- Panels own rendering only. A panel that calls fetch is a bug.
- Datasources own transport + shape — they turn requests into gateway calls and responses into DataFrames. The only layer that knows REST paths.
This is the invariant the user emphasized and it is enforced by the folder boundaries (panels/standard/* may not import from datasources/ except types; only PanelRuntime imports the registry).
7.1 Event & rendering flow (LLD)¶
1. User changes time range → consoleStore.timeRange updates → URL syncs
2. consoleStore change → every PanelRuntime's TanStack Query key changes
3. PanelRuntime → builds QueryRequest, calls datasource.query(req) (deduped/cached)
4. Datasource → gateway call → DataFrame
5. PanelRuntime → passes DataFrame + {isLoading,error} to Panel component
6. Panel → renders; user clicks a row
7. Panel → onDrilldown(link, row) → useDrilldownRouter
8. Drilldown router → maps vars, navigates (e.g. Explore filtered to agent_id) — context preserved
9. SSE (if live) → useSocStream pushes deltas → feed panels + nav badges update (advisory)
State models: server state = TanStack Query (never copied into Zustand); global UI state = Zustand (consoleStore); URL state = search params (filters, time, tab, selected entity); local component state = useState. No server data is duplicated into the store (repo state rule).
8. Investigation, approval & timeline experiences¶
8.1 The investigation spine (navigation model)¶
The console's superpower is that every object links to its evidence. The canonical path:
Detection ──▶ Incident ──▶ Agent ──▶ Decision ──▶ Action ──▶ Receipt ──▶ Verification
(alert) (case) (entity) (allow/deny) (bytes) (hash) (proof / break)
- Drilldown system: every arrow is a
DrilldownLink(HLD/LLD §7.5). Clicking a detection opens the incident; the incident's timeline rows drill to the agent or to the receipt; an agent drills to its decisions in Explore. - Breadcrumbs: the shell renders a context breadcrumb (
Incidents / inc_01J / read_issue #391 / receipt …a1b2) so the analyst never loses the trail. Breadcrumb segments are themselves links (back up the spine). - Context preservation: drilldowns carry the time range and relevant variables; opening a side
Drawer(e.g. agent detail) keeps the underlying list in place. Back navigation restores scroll + expansion state (URL state, not remount).
8.2 Approval experience (the product's trust story, made visible)¶
The ApprovalCard panel (HLD/LLD §7.2) is the most carefully designed surface in the product.
┌ Approval required ────────────────────────── ⏱ expires 04:52 ┐
│ ⬡ coding-agent-prod ▸ run r_8f2… group platform-leads │
│ trust ⬤ untrusted_external risk ███████░░ 78 composite │
│ ───────────────────────────────────────────────────────────── │
│ action github.merge_pull_request → payments-service/main │
│ ┌ canonical payload (exact bytes you approve) ──────────────┐ │
│ │ { "base_branch": "main", "pr_number": 482, "merge_method" │ │
│ │ : "squash" } │ │
│ └────────────────────────────────────────────────────────────┘ │
│ action_hash sha256:9af1c8…b3 📋 reason PR merge after CI │
│ ───────────────────────────────────────────────────────────── │
│ [ Approve ] [ Reject ] [ Edit & re-evaluate ] [ Escalate ]│
└────────────────────────────────────────────────────────────────┘
UX safety rules (non-negotiable):
1. Bytes, not a summary. The payload shown is the canonical hashed action, verbatim. No prettified divergence (defeats render-vs-bytes).
2. Edit re-hashes + re-evaluates. Editing shows a diff and a warning; it produces a new action_hash and re-runs policy — it never patches the approved hash. If the gateway lacks PUT /v1/approvals/:id, Edit is disabled with a tooltip, never faked (fail-closed UX).
3. Expiry is loud. The countdown turns --state-pending → --decision-deny under 60s; an expired card cannot be approved (the button disables with reason).
4. Separation of duties. Approve/Reject are enabled only for role === "approver" (and never the agent's owner); others see them disabled-with-reason. Server enforces too.
5. Trust is front-and-center. The TrustBadge for untrusted_external / malicious_suspected is the brightest non-action element — the analyst sees why approval was required before they see the action.
6. Every approval action emits its own receipt (the console is inside the evidence boundary).
8.3 Provable Timeline experience¶
┌ inc_01J Prompt injection → high-risk action [critical] [contained] [⛓ Verify chain] ┐
│ ● 10:02 read_issue #391 (public) trust ⬤ untrusted_external rcpt …a1b2 ✓ │
│ │ 10:03 ⛨ AEG-1002 confused-deputy (L12) ATLAS AML.T0051 │
│ │ 10:05 ⛔ merge → main forbid-untrusted decision deny rcpt …c3d4 ✓ │
│ │ 10:05 ⛔ approve-then-swap → SDK fail-closed (T-A1) rcpt …c3d4 ✓ │
│ ● 10:05 🛡 Active Response: agent FROZEN · Slack #agent-security contained │
│ ───────────────────────────────────────────────────────────────────────────────────────── │
│ Chain …a1b2 → …c3d4 ✓ tamper-free (5/5 links) [Freeze ▾] [Revoke] [Export pack] │
└──────────────────────────────────────────────────────────────────────────────────────────┘
- Each row carries its
receipt_hash(HashChip) and a per-row verify tick. Verify chain walks the segment (VerifyButton, §6.5): success = green "tamper-free (N/N links)"; failure = red "broken at event 42" with the offending link rendered in--state-failedand a jump-to-row. A break also exists as areceipt-chain-brokenP1 detection. - Rows are virtualized (incidents can be long); the verify walk is stepped + reduced-motion-safe.
8.4 Explore page (Kibana Discover equivalent) — pages/Explore.tsx¶
┌ ⌕ agent_id:coding-agent-prod AND decision:deny AND @time:[now-24h TO now] [Search] [Save] ┐
├──────────────┬──────────────────────────────────────────────────────────────────────────────┤
│ FIELDS │ ▁▂▅▇▅▃▂▁ histogram (count over time, click-drag to zoom the range) │
│ ◔ decision │ ─────────────────────────────────────────────────────────────────────────────│
│ deny 52 │ time decision tool agent trust receipt │
│ allow 311 │ 10:05:12 ⛔ deny github.merge coding-agent-prod ⬤ untrusted …c3d4 ▸ │
│ ◔ source_trust│ 10:04:58 ⛔ deny db.write coding-agent-prod ⬤ untrusted …b8e1 ▸ │
│ untrusted 7│ … (virtualized doc table; click a row → expand) │
│ ◔ tool │ ┌ expanded row ───────────────────────────────────────────────────────────┐ │
│ ◔ agent_id │ │ full ASE JSON · action_hash …9af1 · [Verify receipt] · [Open in timeline] │ │
│ ◔ event_type │ └──────────────────────────────────────────────────────────────────────────┘ │
└──────────────┴──────────────────────────────────────────────────────────────────────────────┘
AqlInputwith field-aware autocomplete (fromdatasource.fields()), error squiggles (client-side parse), and structured-AST submission (no string interpolation).FieldSidebarfacets are clickable → append to the query (KQL-style). Counts come from the aggregate query.- Histogram = a
timeseriespanel bound tocount_over_time; drag-to-zoom writesconsoleStore.timeRange. - Doc table =
DataGrid, virtualized; row-expand reuses today'sExploreTabinspector JSX; the row'saction_hash/receipt_hashare one-click verifiable; "Open in timeline" drills to the incident. - Save persists a named search (server-side,
tenant_id-keyed); saved searches can be pinned as afeed/tablepanel.
9. Panel architecture catalog¶
All panels implement PanelProps (HLD/LLD §7.1), declare the four states, and never fetch. Standard panels first, then the AegisAgent-unique panels (including two new ones).
9.1 Standard panels¶
| Panel | Purpose / UX | Data contract (DataFrame fields) |
States |
|---|---|---|---|
| Stat | one decision number + spark + threshold color | [time, value] or scalar + optional series |
loading=number skeleton; empty="no data"; error=inline; success=value + sparkline |
| Time series | decisions/min, denies/agent | time + ≥1 number series |
same; empty=flat baseline with hint |
| Table | top risky agents, recent denies | N typed fields (uses DataGrid) |
per §6.3 |
| Heatmap | provenance × hour, detection density | x (time) × y (category) × number |
success=ECharts heatmap; empty=grid ghost |
| Status | receipt-chain ✓/✗, agent statuses | label + state enum |
success=status pills; error=--state-failed |
| Feed | live ASE stream (Discover-lite) | streamed rows (virtualized) | live dot when streaming; empty="awaiting events" |
9.2 AegisAgent panels (the differentiators)¶
| Panel | Purpose / UX | Data contract | States |
|---|---|---|---|
| Approval Card | frozen action + Approve/Reject/Edit/Escalate (§8.2) | one approval (action, hash, trust, expiry, params, risk) |
loading=card skeleton; empty="no pending approvals" (a good empty state); error; success=card |
| Provable Timeline | hash-chained incident, one-click verify (§8.3) | ordered events each w/ receipt_hash |
verify sub-states (§6.5); empty="no events"; error |
| Receipt Integrity | browse chain, verify range, visualize break | paginated receipts (cursor) + chain links | success=chain; break=red connector; error |
| Agent Risk Map (new) | the fleet at a glance: agents positioned by risk tier × trust exposure, sized by recent action volume, colored by status (active/frozen/quarantined). A spatial "mission control" of the fleet | agent rows: risk_score, status, trust_exposure, action_count, open_alerts |
loading=ghost nodes; empty="no agents"; success=interactive map; click→agent drilldown |
| Decision Graph (new) | the confused-deputy view: a directed graph of untrusted source → agent → attempted action, edges colored by decision (deny/approval/allow). Makes provenance attacks legible at a glance | nodes (source, agent, action) + edges (decision, count) |
loading=graph skeleton; empty="no flows"; success=force/sankey graph (ECharts); edge click→Explore |
The two new panels are defensible by construction: Agent Risk Map visualizes the fleet's risk posture; Decision Graph visualizes trust-provenance gating — both ride the moat (Gap B) rather than being generic charts. They are registry entries like any other panel, composable into dashboards.
10. Accessibility¶
- Keyboard: every action reachable without a mouse. Global chords (§4.1);
Tab/Shift+Taborder follows visual order; tables support arrow-key row nav +Enterto expand;Esccloses drawers/modals/palette. Focus is never trapped except in modals (where it's intentionally trapped + restored on close). - ARIA: Radix primitives provide correct roles/
aria-*for menus, dialogs, tabs, tooltips. Live regions: new critical detections announce viaaria-live="assertive"; the approval queue badge viaaria-live="polite". The verify result is announced (role="status"). - Color independence (color-blind safe): status is never color-only — every severity/decision/trust token pairs with an icon and a text label (the existing badges already do this). The severity ramp is chosen to remain distinguishable under deuteranopia/protanopia (rose vs red separated by lightness + icon; allow/deny carry ✓/⛔ glyphs).
- Contrast: all themes meet WCAG 2.2 AA for text (≥4.5:1 body, ≥3:1 large/UI) — OKLCH lightness anchors in §3.4 are chosen to satisfy this; Light theme is held to the same bar as Dark.
- Screen reader: the
CanonicalActionViewexposes the payload as readable text (not an image);HashChiphas anaria-labelwith the full hash + kind; charts provide a visually-hidden data table fallback. - Reduced motion:
prefers-reduced-motioncollapses all §2.4 motion to instant state changes; verification becomes a static stepped count.
11. Implementation roadmap (reconciled with HLD/LLD U0–U6)¶
The design-system phases map onto — and front-load — the HLD/LLD's build phases. Each phase ships something usable.
| Phase | Design-system deliverable | HLD/LLD phase | Exit criteria |
|---|---|---|---|
| P0 — Foundation | tokens.css (3 themes), design-system/ primitives, typography, the Aegis Mark, density system |
U0 | a themed Button/Badge/Card/HashChip/TrustBadge set renders; Storybook (or a /kitchen-sink route) green |
| P1 — SOC shell | AppShell, LeftNav (collapse + badges + hotkeys), ControlsBar, TimeRangePicker, CommandPalette, theme/density toggles, URL-sync |
U0–U1 | navigate all pages; global query context drives a single live panel |
| P2 — Panel framework | PanelRuntime, registry, PanelContainer states, standard panels (Stat/TimeSeries/Table/Status/Feed) on Recharts + DataGrid |
U1 | a DashboardSchema renders a real Overview from JSON; panels don't fetch |
| P3 — Dashboards + differentiator panels | DashboardLoader, curated system/ dashboards, ApprovalCard, ProvableTimeline, ReceiptIntegrity, SSE live |
U1–U2 (MVP) | Overview + Live feed + Approval queue + Provable timeline w/ verify working end-to-end |
| P4 — Investigation | Explore (AqlInput, FieldSidebar, histogram, doc table), drilldown router, breadcrumbs, Detections/Rules, Incidents | U3–U4 | the full Detection→…→Verification spine is click-navigable; saved searches |
| P5 — Advanced | Agent Risk Map, Decision Graph, Analytics dashboards, uPlot/ECharts swap for hot panels, OffscreenCanvas/Wasm perf (HLD/LLD §12), in-app dashboard editor, RBAC settings | U5–U6 | fleet/MCP/active-response; editor writes DashboardSchema; perf targets met |
MVP = P0–P3 (= U0–U2): the shell, the panel framework, the Overview, the live feed, and the two surfaces neither Grafana nor Kibana can show — the Approval Queue and the Provable Timeline. Everything before P3 is scaffolding to make those two feel inevitable.
12. Anti-template acceptance checklist¶
Before any surface ships, it must satisfy the design-quality bar (no shadcn-default look):
- [ ] Uses the four surface layers for depth, not uniform cards-on-gray.
- [ ] Severity/trust palette used only semantically; chrome stays low-chroma.
- [ ] Hierarchy from scale + weight, not nested boxes; one clear focal point per panel.
- [ ] Every identifier is monospace + copyable; hashes never wrap or truncate the copied value.
- [ ] Hover/focus/active states are designed (the verify gesture, the approval expiry ramp), not browser defaults.
- [ ] Both Light and Dark SOC themes feel intentional; OLED is a real wall-display mode, not inverted dark.
- [ ] No decorative motion; all motion is functional and reduced-motion-safe.
- [ ] The screen would be believable in a real SOC product screenshot — it reads as AegisAgent, not "a dashboard template."
13. Summary¶
This design system turns the AegisAgent SOC Console into a product that looks and behaves like the next-generation of Grafana + Kibana + Elastic Security — but is unmistakably AegisAgent because its center of gravity is provable AI-agent governance, not metrics. It specifies a deliberate visual identity (the Verified Aperture mark, a reserved severity/trust palette, three real themes), a dense, evidence-forward design language, a keyboard-first mission-control shell, a layered component library with the framework boundary the architecture demands (panels render, PanelRuntime fetches, dashboard JSON composes), and the investigation, approval, and provable-timeline experiences that are the product's reason to exist. The roadmap front-loads the foundation and the shell so the two irreplaceable surfaces — the Approval Queue and the Provable Timeline — ship as the MVP, exactly where the moat lives.