This document describes the REST API and Server-Sent Events (SSE) interface provided by the ccboard web backend (Axum).
Start the backend:
cargo run -- web --port 8080Base URL: http://localhost:8080
CORS: Configured for local development (allows http://localhost:3333)
Health check endpoint returning server status and basic metrics.
Response (200 OK):
{
"status": "healthy",
"sessions": 1234,
"stats_loaded": true
}Fields:
status(string):"healthy"if all systems operational,"degraded"if issues detectedsessions(integer): Total number of sessions loaded in memorystats_loaded(boolean): Whether stats-cache.json was successfully loaded
Use Case: Monitor backend health, check if data is loaded before making other API calls
Example:
curl http://localhost:8080/api/health | jqReturns global Claude Code statistics aggregated from ~/.claude/stats-cache.json, enriched with analytics (forecast, daily activity, model breakdown).
Response (200 OK):
{
"version": 2,
"lastComputedDate": "2026-02-10",
"firstSessionDate": "2025-12-10T09:45:00.350Z",
"totalSessions": 1757,
"totalMessages": 512937,
"thisMonthCost": 11205.38,
"avgSessionCost": 3.06,
"cacheHitRatio": 0.999,
"mcpServersCount": 3,
"mostUsedModel": "claude-opus-4-6",
"totalSpeculationTimeSavedMs": 0,
"longestSession": {
"sessionId": "d78b55ae-...",
"messageCount": 10827,
"date": null
},
"modelUsage": {
"claude-opus-4-6": {
"inputTokens": 5000,
"outputTokens": 7000,
"cacheCreationInputTokens": 300,
"cacheReadInputTokens": 45,
"costUsd": 123.45,
"contextWindow": 0,
"maxOutputTokens": 0,
"webSearchRequests": 0
}
},
"hourCounts": { "0": 6, "1": 2, "10": 133 },
"dailyActivity": [
{
"date": "2026-02-10",
"sessionCount": 42,
"messageCount": 12345,
"toolCallCount": 3456
}
],
"dailyModelTokens": [
{
"date": "2026-02-10",
"tokensByModel": { "claude-opus-4-6": 1332082 }
}
],
"dailyTokens30d": [66938374, 45000000],
"forecastTokens30d": [33071759, 40000000],
"forecastConfidence": 0.14,
"forecastCost30d": 9921.53,
"projectsByCost": [
{
"project": "/Users/john/code/myapp",
"cost": 4986.04,
"percentage": 44.5
}
]
}Top-Level Fields:
version(integer): Stats cache format versionlastComputedDate(string): Date stats were last computed (YYYY-MM-DD)firstSessionDate(ISO 8601): Earliest session timestamptotalSessions(integer): Total number of sessions in stats cachetotalMessages(integer): Total messages across all sessionsthisMonthCost(float): Total cost in USD for the current monthavgSessionCost(float): Average cost per session in USDcacheHitRatio(float): Cache read/creation ratio (0-1)mcpServersCount(integer): Number of configured MCP serversmostUsedModel(string|null): Most frequently used model nametotalSpeculationTimeSavedMs(integer): Speculative execution time saved in ms
Nested Objects:
longestSession(object): Session with most messages (sessionId,messageCount,date)modelUsage(object): Per-model token breakdown keyed by model ID, each containinginputTokens,outputTokens,cacheCreationInputTokens,cacheReadInputTokens,costUsd,contextWindow,maxOutputTokens,webSearchRequestshourCounts(object): Message count per hour (0-23), keyed as strings
Arrays:
dailyActivity(array): Daily aggregates withdate,sessionCount,messageCount,toolCallCountdailyModelTokens(array): Daily token usage per model withdateandtokensByModelmapdailyTokens30d(array): Daily total tokens for last 30 days (integers)forecastTokens30d(array): Predicted daily tokens for next 30 days (integers)forecastConfidence(float): Forecast confidence score (0-1)forecastCost30d(float): Predicted cost for next 30 days in USDprojectsByCost(array): Top 5 projects by cost withproject,cost,percentage
Error Codes:
500 Internal Server Error: Stats cache failed to load
Example:
curl http://localhost:8080/api/stats | jqReturns the N most recent sessions across all projects (lightweight endpoint for dashboards).
Query Parameters:
limit(integer, optional): Number of sessions to return (default: 5, max: 100)
Response (200 OK):
{
"sessions": [
{
"id": "ea23759a-...",
"date": "2026-02-09T10:30:00Z",
"first_timestamp": "2026-02-09T10:00:00Z",
"project": "-Users-john-code-myapp",
"model": "claude-sonnet-4-5",
"messages": 42,
"tokens": 12345,
"input_tokens": 5000,
"output_tokens": 7000,
"cache_creation_tokens": 300,
"cache_read_tokens": 45,
"cost": 1.23,
"status": "completed",
"duration_seconds": null,
"preview": "How do I implement authentication?"
}
],
"total": 1234
}Fields:
sessions(array): Recent sessions sorted by date descending (same object format as/api/sessions)total(integer): Total number of sessions across all projects
Example:
curl "http://localhost:8080/api/sessions/recent?limit=10" | jqReturns active Claude Code processes with real-time CPU and RAM monitoring.
Response (200 OK):
{
"sessions": [
{
"pid": 12345,
"startTime": "2026-02-09T10:30:00+01:00",
"workingDirectory": "/Users/john/code/myapp",
"command": "claude",
"cpuPercent": 15.3,
"memoryMb": 512,
"tokens": 1234,
"sessionId": "ea23759a-...",
"sessionName": null
}
],
"total": 3
}Fields:
pid(integer): Process IDstartTime(ISO 8601): Process start time (with timezone offset)workingDirectory(string): Current working directory of the processcommand(string): Process command name (e.g.,"claude")cpuPercent(float): Current CPU usage percentagememoryMb(integer): Current memory usage in MBtokens(integer): Tokens consumed in current sessionsessionId(string|null): Associated session UUID (matched from JSONL files)sessionName(string|null): Optional session name (if set by user)total(integer): Total number of active Claude processes
Use Case: Live monitoring dashboard with CPU/RAM badges
Example:
curl http://localhost:8080/api/sessions/live | jqReturns session metadata with pagination, filtering, and sorting.
Query Parameters:
page(integer, optional): Page number (default: 0)limit(integer, optional): Page size (default: 50, max: 100)search(string, optional): Search in session ID, project path, or first messageproject(string, optional): Filter by project path (partial match)model(string, optional): Filter by model name (partial match)since(string, optional): Filter by time range (e.g.,7d,30d,1h)sort(string, optional): Sort field (date,tokens,cost) (default:date)order(string, optional): Sort order (asc,desc) (default:desc)
Response (200 OK):
{
"sessions": [
{
"id": "ea23759a-1234-5678-90ab-cdef01234567",
"date": "2026-02-09T10:30:00Z",
"project": "-Users-john-code-myapp",
"model": "claude-sonnet-4-5",
"messages": 42,
"tokens": 12345,
"input_tokens": 5000,
"output_tokens": 7000,
"cache_creation_tokens": 300,
"cache_read_tokens": 45,
"cost": 1.23,
"status": "completed",
"first_timestamp": "2026-02-09T10:00:00Z",
"duration_seconds": 1800,
"preview": "How do I implement authentication?"
}
],
"total": 1234,
"page": 0,
"page_size": 50
}Response Fields:
sessions(array): Array of session objectstotal(integer): Total number of sessions matching filters (before pagination)page(integer): Current page numberpage_size(integer): Number of sessions per page
Session Object Fields:
id(string): Session UUIDdate(ISO 8601): Last message timestampproject(string): Project pathmodel(string): Primary model used (first in list)messages(integer): Number of messages in sessiontokens(integer): Total tokens (input + output + cache)input_tokens(integer): Input tokens consumedoutput_tokens(integer): Output tokens generatedcache_creation_tokens(integer): Tokens written to cachecache_read_tokens(integer): Tokens read from cachecost(float): Estimated cost in USDfirst_timestamp(ISO 8601): Session start timeduration_seconds(integer|null): Session duration (null if not computed)preview(string): First user message (truncated to ~200 chars)
Error Codes:
500 Internal Server Error: Failed to load sessions from SQLite cache
Examples:
# Get recent sessions for specific project
curl "http://localhost:8080/api/sessions?project=myapp&limit=10" | jq
# Search sessions containing "auth"
curl "http://localhost:8080/api/sessions?search=auth" | jq
# Get sessions from last 7 days, sorted by cost
curl "http://localhost:8080/api/sessions?since=7d&sort=cost&order=desc" | jq
# Filter by model and paginate
curl "http://localhost:8080/api/sessions?model=opus&page=1&limit=20" | jqReturns merged configuration from global, project, and local settings.
Response (200 OK):
{
"global": {
"model": "claude-sonnet-4-5",
"temperature": 0.7
},
"project": {
"temperature": 0.5
},
"local": {
"max_tokens": 4096
},
"merged": {
"model": "claude-sonnet-4-5",
"temperature": 0.5,
"max_tokens": 4096
}
}Merge Priority: local > project > global > defaults
Fields:
global(object): Settings from~/.claude/settings.jsonproject(object): Settings from.claude/settings.jsonlocal(object): Settings from.claude/settings.local.jsonmerged(object): Final merged configuration (highest priority wins)
Error Codes:
500 Internal Server Error: Failed to parse configuration files
Example:
curl http://localhost:8080/api/config/merged | jqReturns all configured hooks from merged settings (global + project + local) with script content.
Response (200 OK):
{
"hooks": [
{
"name": "UserPromptSubmit",
"event": "UserPromptSubmit",
"command": "current_model=$(jq -r ...) ...",
"description": "current_model=$(jq -r ...) ...",
"async": false,
"timeout": null,
"cwd": null,
"matcher": null,
"scriptPath": null,
"scriptContent": null
},
{
"name": "PreToolUse-0-0",
"event": "PreToolUse",
"command": "case \"$TOOL_INPUT\" in ...",
"description": "case \"$TOOL_INPUT\" in ...",
"async": false,
"timeout": null,
"cwd": null,
"matcher": "Bash",
"scriptPath": null,
"scriptContent": null
}
],
"total": 5
}Fields:
name(string): Hook identifier (event name orevent-group-indexfor multiple hooks per event)event(string): Event trigger (UserPromptSubmit,PreToolUse,Custom, etc.)command(string): Inline command or script pathdescription(string): Extracted from# Description:comment in script, or command itselfasync(boolean): Whether hook runs asynchronouslytimeout(integer|null): Timeout in seconds (null if not set)cwd(string|null): Working directory overridematcher(string|null): Tool matcher pattern (e.g.,"Bash"for PreToolUse hooks)scriptPath(string|null): Path to external script file (if command references a.shfile)scriptContent(string|null): Full script content (loaded when scriptPath is set)total(integer): Total number of hooks
Use Case: Hooks tab in TUI/Web, syntax highlighting for bash scripts
Example:
curl http://localhost:8080/api/hooks | jqReturns MCP server configuration from claude_desktop_config.json.
Response (200 OK):
{
"servers": [
{
"name": "filesystem",
"command": "npx -y @modelcontextprotocol/server-filesystem /Users/john",
"serverType": "stdio",
"url": null,
"args": ["/Users/john"],
"env": {},
"hasEnv": false
},
{
"name": "brave-search",
"command": null,
"serverType": "http",
"url": "http://localhost:3100/sse",
"args": [],
"env": {"BRAVE_API_KEY": "..."},
"hasEnv": true
}
],
"total": 2
}Fields:
name(string): Server identifiercommand(string): Command for stdio serversserverType(string):"stdio"or"http"url(string): URL for HTTP serversargs(array): Command argumentsenv(object): Environment variableshasEnv(boolean): Whether server has environment variables
Use Case: MCP tab in TUI/Web, server status monitoring
Example:
curl http://localhost:8080/api/mcp | jqReturns agents from ~/.claude/agents/ with frontmatter metadata.
Response (200 OK):
{
"items": [
{
"name": "backend-architect",
"frontmatter": {
"title": "Backend Architect",
"version": "1.0.0",
"description": "Expert backend developer"
},
"body": "# Backend Architect\n\nExpert senior en architecture backend...",
"path": "/Users/john/.claude/agents/backend-architect.md"
}
],
"total": 1
}Fields:
name(string): Agent filename (without.md)frontmatter(object): YAML metadata between---markers (empty{}if no frontmatter)body(string): Markdown content after frontmatterpath(string): Full file pathtotal(integer): Total number of items
Use Case: Agents/Capabilities tab in TUI/Web
Example:
curl http://localhost:8080/api/agents | jqReturns commands from ~/.claude/commands/ with frontmatter metadata.
Response: Same format as /api/agents
Example:
curl http://localhost:8080/api/commands | jqReturns skills from ~/.claude/skills/*/SKILL.md with frontmatter metadata.
Response (200 OK):
{
"items": [
{
"name": "ccboard",
"frontmatter": {
"name": "ccboard",
"invoke": "ccboard",
"version": "0.5.0"
},
"body": "# ccboard Skill\n\nComprehensive TUI/Web dashboard...",
"path": "/Users/john/.claude/skills/ccboard/SKILL.md"
}
],
"total": 1
}Fields: Same as /api/agents and /api/commands
Note: Skills are scanned from subdirectories, looking for SKILL.md files
Example:
curl http://localhost:8080/api/skills | jqLive update stream for real-time monitoring. Pushes events when ~/.claude files change.
Event Types:
stats_updated: Global stats changed (e.g.,stats-cache.jsonmodified)session_created: New session detected (e.g., new.jsonlfile)session_updated: Session file modified (e.g., message added)config_changed: Configuration file changed (e.g.,settings.jsonmodified)
Response (SSE stream):
event: stats_updated
data: {"total_sessions": 1235, "total_tokens": 45680000}
event: session_created
data: {"id": "fb34860b-...", "project": "-Users-john-code-myapp"}
event: session_updated
data: {"id": "ea23759a-...", "message_count": 43}
event: config_changed
data: {"scope": "global", "key": "model", "value": "claude-opus-4-6"}
Usage (JavaScript):
const events = new EventSource('http://localhost:8080/api/events');
events.addEventListener('stats_updated', (e) => {
const data = JSON.parse(e.data);
console.log('Stats updated:', data);
});
events.addEventListener('session_created', (e) => {
const data = JSON.parse(e.data);
console.log('New session:', data.id);
});
events.onerror = (err) => {
console.error('SSE error:', err);
events.close();
};Connection Management:
- Server sends keepalive every 30 seconds (
: keepalivecomment) - Client should reconnect on error (automatic in most SSE libraries)
- Server closes connection after 5 minutes idle (no events)
Error Codes:
500 Internal Server Error: EventBus subscription failed
The API is configured for local development with CORS enabled:
Allowed Origins:
http://localhost:3333(Leptos frontend via Trunk)http://127.0.0.1:3333(alternative localhost)
Allowed Methods:
GET,POST,OPTIONS
Allowed Headers:
Content-Type,Authorization
Production Note: Update CORS origins in ccboard-web/src/router.rs before deploying to production.
All endpoints return JSON errors with this format:
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"details": "Optional additional context"
}Common Error Codes:
STATS_LOAD_FAILED: Failed to load stats-cache.jsonSESSIONS_LOAD_FAILED: Failed to query SQLite cacheCONFIG_PARSE_FAILED: Failed to parse settings.jsonPROJECT_NOT_FOUND: Specified project does not existINVALID_PARAMETER: Missing or invalid query parameter
- Stats: Cached in memory, invalidated on
stats-cache.jsonchange - Sessions: Stored in SQLite cache (89x faster than JSONL parsing)
- Config: Cached in memory, invalidated on
settings.jsonchange
Currently no rate limiting (local development only). Production deployment should add:
- Token bucket rate limiting (e.g., 100 requests/minute per IP)
- Request size limits (max 1MB for POST bodies)
- Single-instance: Designed for local
~/.claudemonitoring (1 user) - Multi-user: Not currently supported (local-only design)
// stats.ts
export async function getStats() {
const response = await fetch('http://localhost:8080/api/stats');
if (!response.ok) throw new Error('Failed to fetch stats');
return response.json();
}
// sessions.ts
export async function getSessions(project: string) {
const url = `http://localhost:8080/api/sessions?project=${encodeURIComponent(project)}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch sessions');
return response.json();
}
// events.ts
export function subscribeToEvents(handlers: {
onStatsUpdated?: (data: any) => void;
onSessionCreated?: (data: any) => void;
}) {
const events = new EventSource('http://localhost:8080/api/events');
if (handlers.onStatsUpdated) {
events.addEventListener('stats_updated', (e) => {
handlers.onStatsUpdated(JSON.parse(e.data));
});
}
if (handlers.onSessionCreated) {
events.addEventListener('session_created', (e) => {
handlers.onSessionCreated(JSON.parse(e.data));
});
}
return events;
}// Using reqwest for HTTP client
use reqwest::Client;
use serde_json::Value;
async fn get_stats(client: &Client) -> Result<Value, reqwest::Error> {
let response = client
.get("http://localhost:8080/api/stats")
.send()
.await?;
response.json().await
}
async fn get_sessions(client: &Client, project: &str) -> Result<Vec<Value>, reqwest::Error> {
let url = format!("http://localhost:8080/api/sessions?project={}", project);
let response = client.get(&url).send().await?;
response.json().await
}# Start backend
cargo run -- web --port 8080
# Test stats endpoint
curl http://localhost:8080/api/stats | jq
# Test sessions endpoint
curl "http://localhost:8080/api/sessions?project=-Users-john-code-myapp" | jq
# Test config endpoint
curl http://localhost:8080/api/config/merged | jq
# Test SSE (leave running)
curl -N http://localhost:8080/api/events# Integration tests (requires real ~/.claude data)
cargo test --test api_integration
# Load testing with wrk
wrk -t4 -c100 -d30s http://localhost:8080/api/statsCause: ~/.claude/stats-cache.json missing or corrupted
Solution: Run Claude Code once to generate stats cache
Cause: Backend not running or wrong port
Solution: Verify backend is running with lsof -i :8080
Cause: Server closes connection after 5 minutes idle
Solution: Client should automatically reconnect (built into EventSource)
Cause: Frontend running on non-allowed origin
Solution: Add origin to CORS config in ccboard-web/src/router.rs
Planned endpoints:
GET /api/conversation/:session_id(Conversation Viewer: full JSONL content display)GET /api/plan/:project(Plan-Aware: PLAN.md parsing and task tracking)
Last Updated: 2026-02-11 API Version: v0.5.2 Backend: Axum + Tokio Frontend: Leptos + WASM