# 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).

