Files
Claude-Code-Monitor/client/README.md
T
nntrivi2001 4d9a385c5e feat(lanes): lane runtime UI, docs, and env additions (A1+A2)
Client-side rendering for the per-lane runtime facts (slot, ports,
database, Redis index, service liveness) added in the server-side
A1/A2 work, plus the doc updates (README, CLAUDE.md, docs/API.md,
client/server READMEs) describing the new profile.env keys, hook
environment contract, and REST endpoints.
2026-08-04 10:04:58 +07:00

1159 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<br/>Pub/Sub]
NotifMgr[Notification Manager]
WSManager[WebSocket Manager]
end
subgraph "Data Access Layer"
APIClient[API Client<br/>Fetch Wrapper]
WSClient[WebSocket Client]
end
subgraph "Utility Layer"
Format[Formatters<br/>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<br/>Router + WS + Notifications]
Layout[Layout.tsx<br/>Sidebar + Outlet]
Dashboard[Dashboard<br/>Monitor tab: stats + agents + events<br/>Health tab: SystemHealthTab]
Kanban[KanbanBoard<br/>4-column agent board]
Sessions[Sessions<br/>filterable table]
Detail[SessionDetail<br/>agent hierarchy + timeline]
Feed[ActivityFeed<br/>streaming event log]
Analytics[Analytics<br/>tokens + heatmap + trends]
Workflows[Workflows<br/>D3.js visualizations]
Settings[Settings<br/>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<br/>(health score, storage donut,<br/>gauges, tool bars, subagent<br/>effectiveness, model tokens)"]
Detail --> AgentCard
Feed --> EventDetail[EventDetail<br/>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<br/>React Router]
Local[Component State<br/>useState]
API[Server API<br/>REST fetch]
WS[WebSocket Events<br/>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<br/>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<string, Set<Function>>();
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 (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="kanban" element={<KanbanBoard />} />
<Route path="sessions" element={<Sessions />} />
<Route path="sessions/:id" element={<SessionDetail />} />
<Route path="activity" element={<ActivityFeed />} />
<Route path="analytics" element={<Analytics />} />
<Route path="workflows" element={<Workflows />} />
<Route path="cc-config" element={<CcConfig />} />
<Route path="run" element={<Run />} />
<Route path="settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
```
### 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<br/>expands the inline payload panel.<br/>The "Session →" button on each row<br/>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<number>`).
#### 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<br/>Number formatting]
fmtCost[fmtCost<br/>Currency formatting]
timeAgo[timeAgo<br/>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(<SessionCard session={session} />);
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<br/>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<br/>Code splitting]
Vite --> CSS[CSS Bundle<br/>Minified]
Vite --> Assets[Static Assets<br/>Optimized]
Bundle --> Dist[dist/]
CSS --> Dist
Assets --> Dist
Dist --> Server[Served by<br/>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 (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<SessionCard
key={sessions[virtualRow.index].id}
session={sessions[virtualRow.index]}
/>
))}
</div>
</div>
);
}
```
---
## 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 (`<nav>`, `<main>`, `<article>`)
- ✅ ARIA labels on interactive elements
- ✅ Keyboard navigation (Tab, Enter, Escape)
- ✅ Focus indicators (outline on :focus)
- ✅ Color contrast ratio >= 4.5:1 for text
- ✅ Alternative text for icons (aria-label)
- ✅ Skip links for screen readers
### Example
```tsx
<button
onClick={handleDelete}
aria-label="Delete pricing rule"
className="focus:outline-blue-500"
>
<Trash2 aria-hidden="true" />
</button>
```
---
## Summary
The client is a production-ready React application with:
- 🚀 **Modern Stack** - React 18, TypeScript, Vite, Tailwind
-**Real-time** - WebSocket integration for live updates
- 🧪 **Tested** - Vitest + React Testing Library
- 📦 **Optimized** - Code splitting, tree shaking, lazy loading
-**Accessible** - WCAG 2.1 AA compliant
- 🎨 **Maintainable** - Clear architecture, type-safe, well-documented
For server documentation, see [server/README.md](../server/README.md).