# Client Application Enterprise-grade React + TypeScript dashboard for real-time Claude Code agent monitoring. ![Claude Code](https://img.shields.io/badge/Claude_Code-orange?style=flat-square&logo=claude&logoColor=white) ![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white) ![React](https://img.shields.io/badge/React-18.3-61DAFB?style=flat-square&logo=react&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6?style=flat-square&logo=typescript&logoColor=white) ![Javascript](https://img.shields.io/badge/JavaScript-ES6-F7DF1E?style=flat-square&logo=javascript&logoColor=white) ![Vite](https://img.shields.io/badge/Vite-6.1-646CFF?style=flat-square&logo=vite&logoColor=white) ![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-3.4-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white) ![WebSocket](https://img.shields.io/badge/WebSocket-RFC_6455-010101?style=flat-square&logo=socketdotio&logoColor=white) ![i18next](https://img.shields.io/badge/i18next-22.4-7A42FF?style=flat-square&logo=i18next&logoColor=white) ![i18next Language Detector](https://img.shields.io/badge/i18next_Language_Detector-6.1-7A42FF?style=flat-square&logo=i18next&logoColor=white) ![Mermaid](https://img.shields.io/badge/Mermaid-10.2-ff3333?style=flat-square&logo=mermaid&logoColor=white) ![React Router](https://img.shields.io/badge/React_Router-6.28-CA4245?style=flat-square&logo=reactrouter&logoColor=white) ![Lucide](https://img.shields.io/badge/Lucide_Icons-0.474-F56565?style=flat-square&logo=lucide&logoColor=white) ![D3.js](https://img.shields.io/badge/D3.js-7-F9A03C?style=flat-square&logo=d3&logoColor=white) ![PostCSS](https://img.shields.io/badge/PostCSS-8.5-DD3A0A?style=flat-square&logo=postcss&logoColor=white) ![Autoprefixer](https://img.shields.io/badge/Autoprefixer-10.4-DD3735?style=flat-square&logo=autoprefixer&logoColor=white) ![ESLint](https://img.shields.io/badge/ESLint-8.44-4B32C3?style=flat-square&logo=eslint&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-20.10-2496ED?style=flat-square&logo=docker&logoColor=white) ![Podman](https://img.shields.io/badge/Podman-4.0-CC342D?style=flat-square&logo=podman&logoColor=white) ![Prometheus](https://img.shields.io/badge/Prometheus-2.x-E6522C?style=flat-square&logo=prometheus&logoColor=white) ![Grafana](https://img.shields.io/badge/Grafana-10.x-F46800?style=flat-square&logo=grafana&logoColor=white) ![Vitest](https://img.shields.io/badge/Vitest-1.0-646CFF?style=flat-square&logo=vitest&logoColor=white) ![React Testing Library](https://img.shields.io/badge/React_Testing_Library-13.0-FF5733?style=flat-square&logo=testinglibrary&logoColor=white) --- ## Table of Contents - [Overview](#overview) - [Architecture](#architecture) - [Component Hierarchy](#component-hierarchy) - [State Management](#state-management) - [WebSocket Integration](#websocket-integration) - [Routing](#routing) - [API Client](#api-client) - [UI Components](#ui-components) - [Utilities](#utilities) - [Testing](#testing) - [Build & Deployment](#build--deployment) - [Development](#development) - [Performance](#performance) - [Accessibility](#accessibility) --- ## Overview The client is a single-page application (SPA) built with modern web technologies: - **React 18.3** - Component-based UI with hooks and concurrent features - **TypeScript 5.7** - Full type safety across components, utilities, and API contracts - **Vite 6.1** - Lightning-fast HMR during development, optimized production builds - **Tailwind CSS 3.4** - Utility-first CSS framework; colors are CSS-variable-backed tokens (`darkMode: "class"`) so a Dark/Light toggle (next to the language switcher in the sidebar, `useTheme` hook) re-themes the whole app by flipping one class - **React Router 6.28** - Client-side routing with nested layouts - **WebSocket** - Real-time event streaming from server - **Lucide Icons** - Modern, consistent icon set ```mermaid graph TB subgraph "Browser Runtime" subgraph "React Application" Router[React Router] Layout[Layout Component] subgraph "Pages" Home[Dashboard] Kanban[KanbanBoard] Sessions[Sessions] Detail[SessionDetail] Feed[ActivityFeed] Analytics[Analytics] Workflows[Workflows] Settings[Settings] end subgraph "Shared Components" AgentCard[AgentCard] StatCard[StatCard] StatusBadge[StatusBadge] EventDetail[EventDetail] EmptyState[EmptyState] end end subgraph "Core Services" API[API Client] WS[WebSocket Manager] Bus[Event Bus] Notif[Notification Manager] end end subgraph "Server (localhost:4820)" REST[REST API] WSS[WebSocket Server] end Router --> Layout Layout --> Home & Kanban & Sessions & Detail & Feed & Analytics & Workflows & Settings Home & Detail --> AgentCard & StatCard & StatusBadge Feed --> EventDetail API --> REST WS --> WSS Bus --> Notif WS --> Bus style Router fill:#61DAFB style API fill:#10B981 style WS fill:#F59E0B style Bus fill:#8B5CF6 ``` --- ## Architecture ### Component Architecture The client follows a layered architecture with clear separation of concerns: ```mermaid graph TB subgraph "Presentation Layer" Pages[Pages/Routes] Components[Reusable Components] Hooks[Custom Hooks] end subgraph "Business Logic Layer" EventBus[Event Bus
Pub/Sub] NotifMgr[Notification Manager] WSManager[WebSocket Manager] end subgraph "Data Access Layer" APIClient[API Client
Fetch Wrapper] WSClient[WebSocket Client] end subgraph "Utility Layer" Format[Formatters
fmt, fmtCost, timeAgo] Types[TypeScript Types] Const[Constants] end Pages --> Components Pages --> Hooks Components --> Hooks Hooks --> EventBus Hooks --> NotifMgr Hooks --> WSManager Hooks --> APIClient WSManager --> WSClient WSManager --> EventBus APIClient --> Format Components --> Format style Pages fill:#3B82F6 style EventBus fill:#8B5CF6 style APIClient fill:#10B981 style Format fill:#F59E0B ``` ### Directory Structure ``` client/ ├── src/ │ ├── components/ # Reusable UI components │ │ ├── __tests__/ # Component tests │ │ ├── AgentCard.tsx │ │ ├── StatCard.tsx │ │ ├── StatusBadge.tsx │ │ ├── EventDetail.tsx # Inline hook payload viewer (used by ActivityFeed + SessionDetail) │ │ ├── EmptyState.tsx │ │ ├── Sidebar.tsx │ │ ├── Layout.tsx │ │ ├── RemoteSources.tsx # Remote Data Sources settings panel (SSH multi-machine collection) │ │ └── workflows/ # D3.js workflow visualization components (12 files) │ │ │ ├── pages/ # Route pages │ │ ├── Dashboard.tsx │ │ ├── KanbanBoard.tsx │ │ ├── Sessions.tsx # Filterable table; shows each session's real name (synced live from the transcript), falls back to the short ID │ │ ├── SessionDetail.tsx # Agent tree + event timeline + Conversation tab (slash-command pills & output, inline rename markers) │ │ ├── ActivityFeed.tsx # Real-time event log; row click expands payload; Session btn navigates │ │ ├── Analytics.tsx │ │ ├── Workflows.tsx │ │ ├── Settings.tsx │ │ └── NotFound.tsx │ │ │ ├── lib/ # Core utilities & business logic │ │ ├── __tests__/ # Utility tests │ │ ├── api.ts # REST API client │ │ ├── eventBus.ts # WebSocket pub/sub + connection state │ │ ├── dataScope.ts # Global data-scope store (app-wide ?sources= selection) │ │ ├── format.ts # Formatters (formatTime, timeAgo, fmtCost) │ │ └── types.ts # TypeScript type definitions │ │ │ ├── hooks/ │ │ ├── useWebSocket.ts # Auto-reconnecting WebSocket hook │ │ ├── useNotifications.ts # Browser push notification triggers │ │ └── useTheme.ts # Dark/light mode: toggles the `dark` class, persists to localStorage │ │ │ ├── i18n/ # Internationalization (en / zh / vi / ko) │ ├── App.tsx # Root component + router setup │ ├── main.tsx # Entry point │ └── index.css # Tailwind + CSS-variable color tokens (dark/light) │ ├── public/ # Static assets (sw.js service worker) ├── index.html # HTML template ├── vite.config.ts # Vite + proxy config ├── tailwind.config.js # Dark/light color tokens (`darkMode: "class"`, CSS-variable-backed) ├── tsconfig.json # Strict TypeScript config └── package.json ``` --- ## Component Hierarchy ### Page Components ```mermaid graph TB App[App.tsx
Router + WS + Notifications] Layout[Layout.tsx
Sidebar + Outlet] Dashboard[Dashboard
Monitor tab: stats + agents + events
Health tab: SystemHealthTab] Kanban[KanbanBoard
4-column agent board] Sessions[Sessions
filterable table] Detail[SessionDetail
agent hierarchy + timeline] Feed[ActivityFeed
streaming event log] Analytics[Analytics
tokens + heatmap + trends] Workflows[Workflows
D3.js visualizations] Settings[Settings
pricing + notifications + hooks] App --> Layout Layout --> Dashboard & Kanban & Sessions & Detail & Feed & Analytics & Workflows & Settings Dashboard --> StatCard[StatCard × 6] Dashboard --> AgentCard[AgentCard × N] Dashboard --> HealthTab["SystemHealthTab
(health score, storage donut,
gauges, tool bars, subagent
effectiveness, model tokens)"] Detail --> AgentCard Feed --> EventDetail[EventDetail
inline payload viewer] Detail --> EventDetail style App fill:#1E40AF style Layout fill:#3B82F6 style Feed fill:#8B5CF6 style EventDetail fill:#10B981 ``` ### Component Props Flow ```mermaid sequenceDiagram participant Router participant Page participant Component participant API participant WS Router->>Page: Navigate (params from URL) Page->>API: Fetch initial data API-->>Page: Return data Page->>Component: Pass data as props Component->>Component: Render UI Note over WS: Real-time updates WS->>Page: Event via EventBus Page->>Page: Update local state Page->>Component: Re-render with new props ``` --- ## State Management The client uses **local component state** and **React hooks** for state management. No global state library (Redux, Zustand) is used to keep the architecture simple. The one small exception is the **data-scope store** (`lib/dataScope.ts`): a lightweight app-wide store holding the current set of data sources (`local` plus any configured [Remote Data Sources](../server/README.md#remote-data-sources)). Pages read it and append `?sources=` to their API requests, so a single selector narrows the whole app to one or more machines' data. Remote sources are managed from the Settings page via the `RemoteSources` component (`components/RemoteSources.tsx`), which drives the `/api/remote-sources` CRUD/test/sync endpoints and reflects live `remote_source.status` WebSocket updates. When a sync finishes, stats pages refetch via `lib/remoteDataEvents.ts` (`remote_data.updated`, `remote_source.status` with `ok`, or remote `import.progress` complete). **Cursor sessions (informational):** Settings surfaces a subtle note on the CLAUDE_HOME, Import History, and Remote Data Sources panels — **Cursor** agent sessions count too because Cursor stores transcripts under the same `~/.claude` paths as Claude Code locally (and on synced remotes). ### State Strategy ```mermaid graph TB subgraph "State Sources" URL[URL Params
React Router] Local[Component State
useState] API[Server API
REST fetch] WS[WebSocket Events
Real-time] end subgraph "State Consumers" Pages[Page Components] Components[Child Components] end URL --> Pages API --> Pages WS --> Pages Pages --> Local Local --> Components style URL fill:#F59E0B style API fill:#10B981 style WS fill:#EF4444 style Local fill:#3B82F6 ``` ### State Update Pattern 1. **Initial Load**: Page component fetches data via API client on mount (`useEffect`) 2. **URL Changes**: React Router triggers re-render, page refetches data 3. **Real-time Updates**: WebSocket events trigger state updates via `EventBus` 4. **User Actions**: Click handlers call API, optimistically update local state Example from `SessionDetailPage`: ```typescript function SessionDetailPage() { const { sessionId } = useParams(); const [session, setSession] = useState(null); const [agents, setAgents] = useState([]); // Initial load useEffect(() => { fetchSession(sessionId).then(setSession); fetchAgents(sessionId).then(setAgents); }, [sessionId]); // Real-time updates useEffect(() => { const unsubscribe = eventBus.on('agent.created', (agent) => { if (agent.session_id === sessionId) { setAgents(prev => [...prev, agent]); } }); return unsubscribe; }, [sessionId]); } ``` --- ## WebSocket Integration ### WebSocket Lifecycle ```mermaid stateDiagram-v2 [*] --> Disconnected Disconnected --> Connecting: connect() Connecting --> Connected: onopen Connecting --> Disconnected: onerror Connected --> Disconnected: onclose Connected --> Connected: onmessage Disconnected --> Connecting: auto-reconnect (3s) note right of Connected Heartbeat every 30s Emit events to EventBus end note ``` ### WebSocket Message Flow ```mermaid sequenceDiagram participant Server participant WS as WebSocket
Manager participant Bus as Event Bus participant Page as Page Component participant UI Server->>WS: { type: 'session.created', data: {...} } WS->>WS: Parse JSON WS->>Bus: emit('session.created', data) Bus->>Page: callback(data) Page->>Page: Update state Page->>UI: Re-render ``` ### Event Types Server broadcasts these event types over WebSocket: | Event Type | Payload | Triggered By | |------------|---------|--------------| | `session.created` | Session object | SessionStart hook | | `session.updated` | Session object | Any hook touching session | | `agent.created` | Agent object | PreToolUse hook | | `agent.updated` | Agent object | PostToolUse/Stop hooks | | `tool.executed` | Tool execution record | PostToolUse hook | | `notification.received` | Notification object | Notification hook | | `remote_source.status` | `{ id, status, error?, last_sync_at? }` (`status`: `idle`/`syncing`/`ok`/`error`/`deleted`) | Remote Data Source sync poller + `/api/remote-sources` routes | | `remote_data.updated` | `{ sourceId, source, label?, counters?, last_sync_at? }` | Emitted once per successful remote sync; triggers stats/cost/session refetches. The server also broadcasts `session_created` / `session_updated` (and main-agent frames) for each mirrored session so Kanban/Sessions update immediately | | `lane_hook_output` | `{ laneId, hook, stream, line }` | One output line from a lane's profile hook, pushed while it still runs. A build can take minutes; `LaneCard` shows the latest line so a boot does not read as a hang | | `lane_runtime` | `{ laneId, runtime? , error? }` | A lane's stack finished coming up or failed to. Carries the fresh runtime facts, so a listener need not re-request them | | `lane_hook_result` | `{ laneId, hook, code, error? }` | A profile hook exited (`POST /api/lanes/:id/hook/:name`). `code` is `null` when the hook could not be started | ### EventBus Pattern The `eventBus` is a simple pub/sub system: ```typescript // lib/eventBus.ts class EventBus { private listeners = new Map>(); on(event: string, callback: Function): () => void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); // Return unsubscribe function return () => this.listeners.get(event)?.delete(callback); } emit(event: string, data: any): void { this.listeners.get(event)?.forEach(cb => cb(data)); } } export const eventBus = new EventBus(); ``` Usage in components: ```typescript useEffect(() => { const unsubscribe = eventBus.on('session.created', handleNewSession); return unsubscribe; // Cleanup on unmount }, []); ``` --- ## Routing ### Route Structure ```mermaid graph TB Root["/"] Dashboard["/ (Dashboard)"] Kanban["/kanban"] Sessions["/sessions"] Detail["/sessions/:id"] Feed["/activity"] Analytics["/analytics"] Workflows["/workflows"] CcConfig["/cc-config"] Run["/run"] Settings["/settings"] NF["/* (NotFound)"] Root --> Dashboard & Kanban & Sessions & Detail & Feed & Analytics & Workflows & CcConfig & Run & Settings & NF style Dashboard fill:#3B82F6 style Detail fill:#3B82F6 style Feed fill:#8B5CF6 ``` ### Route Configuration ```tsx // App.tsx import { BrowserRouter, Routes, Route } from 'react-router-dom'; function App() { return ( }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } ``` ### Navigation Flow ```mermaid sequenceDiagram participant User participant UI participant Router participant Page User->>UI: Click session row in Sessions table UI->>Router: navigate('/sessions/123') Router->>Page: Mount SessionDetail Page->>Page: Read params.id = '123' Page->>Page: Fetch session data Page->>UI: Render agent tree + event timeline Note over UI: In ActivityFeed, clicking a row
expands the inline payload panel.
The "Session →" button on each row
navigates to /sessions/:id instead. ``` --- ## API Client ### API Architecture ```mermaid graph LR Component[React Component] --> API[api.ts] API --> Fetch[fetch API] Fetch --> Server[Server :4820] Server --> Response[JSON Response] Response --> API API --> Component style API fill:#10B981 style Server fill:#3B82F6 ``` ### API Client Structure ```typescript // lib/api.ts const BASE_URL = 'http://localhost:4820'; class APIClient { private async request(path: string, options?: RequestInit) { const response = await fetch(`${BASE_URL}${path}`, options); if (!response.ok) throw new Error(`API error: ${response.statusText}`); return response.json(); } // Sessions getSessions() { return this.request('/api/sessions'); } getSession(id: string) { return this.request(`/api/sessions/${id}`); } // Agents getAgents(sessionId: string) { return this.request(`/api/sessions/${sessionId}/agents`); } getAgent(id: string) { return this.request(`/api/agents/${id}`); } // Tools getTools(agentId: string) { return this.request(`/api/agents/${agentId}/tools`); } // Pricing getPricingRules() { return this.request('/api/pricing'); } createPricingRule(rule: PricingRule) { return this.request('/api/pricing', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(rule) }); } deletePricingRule(pattern: string) { return this.request(`/api/pricing/${encodeURIComponent(pattern)}`, { method: 'DELETE' }); } } export const api = new APIClient(); ``` > **API reference:** the endpoints this client calls are fully documented by the server's OpenAPI 3.0.3 spec. With the dashboard running (default port `4820`), explore them at `/api/docs` (interactive Swagger UI), `/api/redoc` (read-optimized ReDoc reference), or `/api/openapi.json` (raw spec). A committed `openapi.yaml` at the repo root mirrors the live spec. ### Error Handling ```mermaid graph TB Request[API Request] FetchCall[Call fetch API] NetworkError{Network Error?} HTTPError{HTTP Error?} Success[Return JSON] Request --> FetchCall FetchCall --> NetworkError NetworkError -->|Yes| Throw1[Throw Network Error] NetworkError -->|No| HTTPError HTTPError -->|Yes| Throw2[Throw HTTP Error] HTTPError -->|No| Success style Throw1 fill:#EF4444 style Throw2 fill:#EF4444 style Success fill:#10B981 ``` --- ## UI Components ### Component Catalog #### SessionCard Displays session summary with status, model, cost, and agent count. **Props:** ```typescript interface SessionCardProps { session: Session; } ``` **Visual Structure:** ``` ┌────────────────────────────────────────┐ │ 🟢 Session Title $0.45 │ │ claude-sonnet-4 │ │ Started: 2 hours ago │ │ Agents: 3 | Tools: 12 │ └────────────────────────────────────────┘ ``` #### AgentCard Shows agent type, status, tool usage, and cost breakdown. **Props:** ```typescript interface AgentCardProps { agent: Agent; } ``` #### StatusBadge Colored status pills for agents (`AgentStatusBadge`) and sessions (`SessionStatusBadge`). When a row is in the yellow **Waiting** overlay (`awaiting_input_since` set), an optional `reason` prop explains WHY: a hover tooltip carries the full explanation, and — unless `compact` is set — a small nested chip (icon + short label) renders inline. Card layouts (Kanban / Dashboard trees) pass `compact` so the chip never squeezes the card title; the Sessions table and session-detail header show the full chip: | `awaiting_reason` | Label | Meaning | | ----------------- | ----- | ------- | | `notification` | Needs input | Blocked on a permission prompt / input request (urgent — amber) | | `stop` | Turn done | Claude finished its reply; idle until the next prompt | | `session_start` | At prompt | Fresh/resumed CLI sitting at an empty prompt | | `interrupted` | Interrupted | Turn cut short — Esc or a recovered hook (urgent — amber) | **Props:** ```typescript interface AgentStatusBadgeProps { status: EffectiveAgentStatus; pulse?: boolean; reason?: AwaitingReason | null; // from agentAwaitingReason(agent) compact?: boolean; // tooltip-only (no inline chip) for tight card layouts } ``` Unknown/future server reasons degrade to a plain Waiting badge (`normalizeAwaitingReason` filters them to null). SessionDetail additionally renders a waiting-for-input banner (same reason + relative time) under the header via the shared `REASON_ICONS` map. #### ToolCard Displays tool execution details with timing and token usage. **Props:** ```typescript interface ToolCardProps { tool: ToolExecution; } ``` #### EventTimeline Chronological view of session events (hooks, tools, notifications). ```mermaid graph TB Timeline[EventTimeline] Events[Event List] Event1[SessionStart] Event2[PreToolUse] Event3[PostToolUse] Event4[Notification] Timeline --> Events Events --> Event1 Events --> Event2 Events --> Event3 Events --> Event4 style Event1 fill:#10B981 style Event2 fill:#3B82F6 style Event3 fill:#8B5CF6 style Event4 fill:#F59E0B ``` #### ActivityFeed (`pages/ActivityFeed.tsx`) Real-time streaming event log with pause/resume, pagination, and inline payload expansion. **UX interaction model:** ```mermaid flowchart LR ROW["Event row\n(role=button)"] -->|click| EXPAND["Toggle EventDetail\n(inline payload)"] ROW --> SESSBTN["Session → button\n(right edge)"] SESSBTN -->|click + stopPropagation| NAV["/sessions/:id"] EXPAND --> ED["EventDetail.tsx\nparsed fields + JSON blocks"] style ROW fill:#1a1a28,stroke:#4f4f6a,color:#e4e4ed style SESSBTN fill:#3B82F6,stroke:#60A5FA,color:#fff style ED fill:#10B981,stroke:#34D399,color:#fff style NAV fill:#8B5CF6,stroke:#A78BFA,color:#fff ``` - The entire row is clickable (keyboard accessible via `Enter`/`Space`) and toggles the `EventDetail` dropdown. - The chevron icon rotates 90° when a row is expanded — it is a visual indicator only, not a separate button. - The **Session →** button uses `e.stopPropagation()` so navigating to session details never collapses an open payload panel. - Multiple rows can be expanded simultaneously (state stored in `Set`). #### EventDetail (`components/EventDetail.tsx`) Renders the hook payload for a single event inline below its row. Scalars appear as `key: value` pairs; objects and arrays render in a terminal-styled code block with a copy button. --- ## Utilities ### Formatters (lib/format.ts) ```mermaid graph LR subgraph "Formatting Functions" fmt[fmt
Number formatting] fmtCost[fmtCost
Currency formatting] timeAgo[timeAgo
Relative time] end Components[UI Components] --> fmt Components --> fmtCost Components --> timeAgo fmt --> Output1["1,234,567"] fmtCost --> Output2["$12.34"] timeAgo --> Output3["2 hours ago"] style fmt fill:#10B981 style fmtCost fill:#10B981 style timeAgo fill:#10B981 ``` **Function Signatures:** ```typescript // Format large numbers with commas export function fmt(n: number | null | undefined): string; // Examples: fmt(1234) → "1,234" // fmt(null) → "—" // Format cost in dollars export function fmtCost(cost: number | null | undefined): string; // Examples: fmtCost(1.234) → "$1.23" // fmtCost(0) → "$0.00" // Relative time string export function timeAgo(date: string | Date | null | undefined): string; // Examples: timeAgo('2024-03-18T12:00:00Z') → "2 hours ago" // timeAgo(null) → "—" ``` ### Type Definitions (lib/types.ts) All TypeScript interfaces match server response shapes: ```typescript interface Session { id: string; session_id: string; model: string; status: 'active' | 'completed' | 'error' | 'abandoned'; total_cost: number; created_at: string; updated_at: string; } interface Agent { id: number; agent_id: string; session_id: string; agent_type: string; status: 'working' | 'waiting' | 'completed' | 'error'; input_tokens: number; output_tokens: number; cost: number; created_at: string; } interface ToolExecution { id: number; agent_id: string; tool_name: string; duration_ms: number; success: boolean; created_at: string; } ``` --- ## Testing ### Test Stack - **Vitest** - Fast unit test runner (Vite-native) - **React Testing Library** - Component testing - **jsdom** - Browser environment simulation ### Test Structure ``` client/src/ ├── components/__tests__/ │ ├── AgentCard.test.tsx │ ├── SessionCard.test.tsx │ └── EventTimeline.test.tsx │ ├── pages/__tests__/ │ ├── screens.snapshot.test.tsx # render snapshots for every screen │ └── __snapshots__/ # committed .snap baselines │ └── lib/__tests__/ ├── format.test.ts ├── eventBus.test.ts └── api.test.ts ``` ### Running Tests ```bash # Run all tests npm test # Watch mode npm run test:watch # Coverage report npm run test:coverage ``` ### Example Test ```tsx // components/__tests__/SessionCard.test.tsx import { render, screen } from '@testing-library/react'; import { SessionCard } from '../SessionCard'; test('renders session title and cost', () => { const session = { id: '1', session_id: 'sess_123', model: 'claude-sonnet-4', total_cost: 1.23, status: 'active', created_at: '2024-03-18T12:00:00Z' }; render(); expect(screen.getByText('sess_123')).toBeInTheDocument(); expect(screen.getByText('$1.23')).toBeInTheDocument(); }); ``` ### Snapshot Testing `pages/__tests__/screens.snapshot.test.tsx` renders **every routed screen** (Dashboard, Kanban, Sessions, Session detail, Activity feed, Analytics, Workflows, Claude Config, Run, Settings, Not found) and asserts each against a committed snapshot in `pages/__tests__/__snapshots__/`. These are structural regression guards — they catch unintended changes to layout, markup, or localized copy. To keep snapshots **deterministic** across machines and CI, the suite: - mocks the API layer (`vi.mock("../../lib/api", …)`) to a loaded-empty state (empty collections + zeroed scalars), so no live data or noisy chart DOM leaks in — `importOriginal` keeps non-`api` exports real; - stubs `eventBus`, push notifications, and the jsdom-missing `ResizeObserver` / `IntersectionObserver` / `matchMedia` / `scroll*` APIs; - pins the clock (`vi.useFakeTimers`) and timezone (`TZ=UTC`) so any rendered timestamps are stable. When you change a screen **intentionally**, review the diff and regenerate the baselines: ```bash cd client && npx vitest run -u src/pages/__tests__/screens.snapshot.test.tsx ``` Commit the updated `.snap` file alongside the change. --- ## Build & Deployment ### Development Build ```bash npm run dev ``` Starts Vite dev server with HMR at `http://localhost:5173` ```mermaid graph LR Source[src/**/*.tsx] --> Vite[Vite Dev Server] Vite --> HMR[Hot Module
Replacement] HMR --> Browser[Browser] Browser -->|Changes| Vite style Vite fill:#646CFF ``` ### Production Build ```bash npm run build ``` Output: `client/dist/` (optimized static files) ```mermaid graph TB Source[src/] --> Vite[Vite Build] Vite --> Bundle[JS Bundle
Code splitting] Vite --> CSS[CSS Bundle
Minified] Vite --> Assets[Static Assets
Optimized] Bundle --> Dist[dist/] CSS --> Dist Assets --> Dist Dist --> Server[Served by
Express] style Vite fill:#646CFF style Dist fill:#10B981 ``` ### Build Optimizations 1. **Code Splitting** - Lazy load routes with `React.lazy()` 2. **Tree Shaking** - Remove unused code 3. **Minification** - Terser for JS, cssnano for CSS 4. **Asset Hashing** - Cache busting with content hashes 5. **Compression** - Gzip/Brotli (handled by Express) --- ## Development ### Prerequisites - Node.js >= 20.0.0 - npm >= 9.0.0 ### Setup ```bash # Install dependencies npm install # Start dev server npm run dev ``` ### Environment Variables The client uses hardcoded API URL (`http://localhost:4820`). For custom configuration, update `lib/api.ts`: ```typescript const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4820'; ``` Then create `.env`: ``` VITE_API_URL=http://localhost:4820 ``` ### Hot Module Replacement (HMR) Vite provides instant feedback on code changes: ```mermaid sequenceDiagram participant Dev as Developer participant File as Source File participant Vite participant Browser Dev->>File: Edit component File->>Vite: File change detected Vite->>Vite: Rebuild module Vite->>Browser: Send HMR update Browser->>Browser: Replace module Browser->>Browser: Re-render component Note over Browser: State preserved! ``` --- ## Performance ### Metrics - **First Contentful Paint (FCP)**: < 0.5s - **Time to Interactive (TTI)**: < 1.5s - **Bundle Size**: ~150KB gzipped (main chunk) ### Optimization Techniques ```mermaid graph TB subgraph "Bundle Optimization" Split[Code Splitting] Lazy[Lazy Loading] Tree[Tree Shaking] end subgraph "Runtime Optimization" Memo[React.memo] Callback[useCallback] Virtual[Virtual Scrolling] end subgraph "Network Optimization" Cache[HTTP Caching] WS[WebSocket vs Polling] Prefetch[Link Prefetching] end Split --> FastLoad[Fast Initial Load] Lazy --> FastLoad Tree --> FastLoad Memo --> SmoothUI[Smooth UI] Callback --> SmoothUI Virtual --> SmoothUI Cache --> LowLatency[Low Latency] WS --> LowLatency Prefetch --> LowLatency ``` ### Virtual Scrolling For large lists (100+ sessions), implement virtual scrolling: ```tsx import { useVirtualizer } from '@tanstack/react-virtual'; function SessionList({ sessions }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: sessions.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, // estimated row height }); return (
{virtualizer.getVirtualItems().map(virtualRow => ( ))}
); } ``` --- ## Accessibility ### WCAG 2.1 Level AA Compliance ```mermaid graph TB subgraph "Accessibility Features" Semantic[Semantic HTML] ARIA[ARIA Labels] Keyboard[Keyboard Navigation] Focus[Focus Management] Contrast[Color Contrast] Screen[Screen Reader Support] end Semantic --> A11y[WCAG 2.1 AA] ARIA --> A11y Keyboard --> A11y Focus --> A11y Contrast --> A11y Screen --> A11y style A11y fill:#10B981 ``` ### Implementation Checklist - ✅ Semantic HTML5 elements (`