Foreman includes a built-in web dashboard for monitoring pipeline state, inspecting LLM costs, and managing tickets. It exposes a REST API, WebSocket live event stream, and a Prometheus metrics endpoint.
The dashboard starts automatically with the daemon when enabled = true:
[dashboard]
enabled = true
port = 8080
host = "127.0.0.1"
auth_token = "${FOREMAN_DASHBOARD_TOKEN}"Generate a token:
./foreman token generateAccess the dashboard at http://localhost:8080. All endpoints require a bearer token.
Security note: The default
host = "127.0.0.1"binds to loopback only. Do not sethost = "0.0.0.0"without placing the dashboard behind a reverse proxy with TLS.
All REST API endpoints require a bearer token in the Authorization header:
Authorization: Bearer <your-token>
Tokens are stored as SHA-256 hashes in the database. They can be revoked via the API.
The WebSocket endpoint supports three auth modes (in priority order):
Sec-WebSocket-Protocol: bearer.<token>(preferred)Authorization: Bearer <token>?token=<token>query parameter (deprecated)
Example (preferred):
const token = '<your-token>';
const ws = new WebSocket('ws://localhost:8080/ws/events', [`bearer.${token}`]);Deprecated query-param form still works:
ws://localhost:8080/ws/events?token=<your-token>
GET /api/status
Returns dashboard process status, daemon runtime state, and optional channel/MCP connectivity.
{
"status": "running",
"version": "0.1.0",
"uptime": "3h24m12s",
"daemon_state": "running",
"channels": {
"whatsapp": { "connected": true }
},
"mcp_servers": {
"filesystem": { "healthy": true }
}
}GET /api/tickets
Returns a list of all tickets. Optional query parameter:
| Parameter | Values | Description |
|---|---|---|
status |
queued, clarification_needed, planning, plan_validating, implementing, reviewing, pr_created, awaiting_merge, merged, pr_closed, decomposing, decomposed, done, failed, blocked, partial |
Filter by status |
[
{
"id": "uuid",
"external_id": "PROJ-123",
"title": "Add user authentication",
"status": "implementing",
"cost_usd": 1.23,
"created_at": "2026-03-05T10:00:00Z"
}
]GET /api/tickets/{id}
Returns full details for a single ticket by its internal UUID.
{
"id": "uuid",
"external_id": "PROJ-123",
"title": "Add user authentication",
"description": "...",
"acceptance_criteria": "...",
"status": "implementing",
"branch_name": "foreman/PROJ-123-add-auth",
"pr_url": "",
"cost_usd": 1.23,
"tokens_input": 45000,
"tokens_output": 8000,
"total_llm_calls": 4,
"is_partial": false,
"last_completed_task_seq": 2,
"created_at": "2026-03-05T10:00:00Z",
"started_at": "2026-03-05T10:01:00Z"
}GET /api/tickets/{id}/tasks
Returns the task list for a ticket.
[
{
"id": "task-uuid",
"ticket_id": "uuid",
"sequence": 1,
"title": "Create User model and migration",
"status": "done",
"implementation_attempts": 1,
"spec_review_attempts": 1,
"quality_review_attempts": 1,
"total_llm_calls": 3,
"commit_sha": "abc123",
"cost_usd": 0.45
}
]GET /api/tickets/{id}/events
Returns the last 100 events for a ticket.
[
{
"id": "evt-uuid",
"ticket_id": "uuid",
"task_id": "task-uuid",
"event_type": "task_tdd_verify_pass",
"severity": "info",
"message": "TDD verification passed",
"details": null,
"created_at": "2026-03-05T10:05:00Z"
}
]GET /api/ticket-summaries
Returns compact ticket cards used by the dashboard list view.
GET /api/events?limit=50&offset=0
Returns global event feed entries across tickets.
DELETE /api/tickets/{id}
Permanently deletes a ticket and associated records.
{ "status": "deleted", "ticket_id": "uuid" }GET /api/tickets/{id}/llm-calls
Returns all recorded LLM calls for a ticket.
[
{
"id": "call-uuid",
"ticket_id": "uuid",
"task_id": "task-uuid",
"role": "implementer",
"provider": "anthropic",
"model": "claude-sonnet-4-6",
"attempt": 1,
"tokens_input": 12000,
"tokens_output": 2500,
"cost_usd": 0.21,
"duration_ms": 8400,
"status": "success",
"created_at": "2026-03-05T10:03:00Z"
}
]POST /api/tickets/{id}/retry
Resets a failed or blocked ticket to queued so it will be picked up on the next daemon poll cycle.
{ "status": "retrying", "ticket_id": "uuid" }POST /api/tasks/{id}/retry
Resets a failed task to pending so the pipeline can retry it.
{ "status": "retrying", "task_id": "task-uuid" }GET /api/tasks/{id}/context
Returns context budget/utilization stats for a task.
{
"budget": 18000,
"used": 12450,
"utilization_pct": 69.1,
"files_selected": 14,
"files_touched": 6,
"cache_hits": 3
}GET /api/pipeline/active
Returns currently executing pipelines with their current stage.
[
{
"ticket_id": "uuid",
"external_id": "PROJ-123",
"title": "Add user authentication",
"current_stage": "implementing",
"current_task_seq": 2,
"total_tasks": 4,
"started_at": "2026-03-05T10:01:00Z"
}
]GET /api/costs/today
Returns total spend for today.
{
"date": "2026-03-05",
"cost_usd": 12.45
}GET /api/costs/week
Returns spend for the past 7 days as a flat day-by-day array.
[
{ "date": "2026-02-27", "cost_usd": 11.20 },
{ "date": "2026-02-28", "cost_usd": 9.80 }
]GET /api/costs/month
Returns spend for the current calendar month.
{
"month": "2026-03",
"cost_usd": 245.10
}GET /api/costs/budgets
Returns configured budget limits.
{
"max_daily_usd": 150,
"max_monthly_usd": 3000,
"max_ticket_usd": 100,
"alert_threshold_pct": 80
}GET /api/stats/team returns 7-day submitter aggregates.
GET /api/stats/recent-prs returns recent merged/created PR-linked tickets.
GET /api/prompts/versions returns prompt-template snapshot hashes for auditability.
POST /api/daemon/pause
Pauses the daemon scheduler. Active pipelines finish their current task then stop.
{ "message": "daemon paused" }POST /api/daemon/resume
Resumes a paused daemon.
{ "message": "daemon resumed" }GET /api/config/summary
Returns a redacted summary of the active configuration (global + project) — useful for the settings drawer in the dashboard UI. API keys are shown truncated (first 8 chars + ...).
{
"llm": {
"provider": "anthropic",
"models": {
"planner": "anthropic:claude-sonnet-4-6",
"implementer": "anthropic:claude-sonnet-4-6",
"spec_reviewer": "anthropic:claude-haiku-4-5",
"quality_reviewer": "anthropic:claude-haiku-4-5",
"final_reviewer": "anthropic:claude-sonnet-4-6"
},
"api_key": "sk-ant-a..."
},
"tracker": { "provider": "github", "poll_interval": "60s" },
"git": { "provider": "github", "clone_url": "git@github.com:org/repo.git", "branch_prefix": "foreman", "auto_merge": false },
"agent_runner": { "provider": "builtin", "max_turns": 20, "token_budget": 0 },
"daemon": { "work_dir": "~/.foreman/work", "log_level": "info", "max_parallel_tickets": 3, "max_parallel_tasks": 3 },
"database": { "driver": "sqlite", "path": "~/.foreman/foreman.db" },
"mcp": { "servers": [] },
"cost": { "daily_budget": 150.0, "monthly_budget": 3000.0, "per_ticket_budget": 15.0, "alert_threshold": 80 },
"rate_limit": { "requests_per_minute": 50 }
}GET /api/usage/activity
Returns LLM call attribution broken down by runner, model, and role for the last 7 days, plus the 20 most recent calls.
{
"by_runner": [
{ "runner": "builtin", "calls": 142, "tokens_in": 1820000, "tokens_out": 310000, "cost_usd": 8.23 }
],
"by_model": [
{ "model": "anthropic:claude-sonnet-4-6", "calls": 98, "tokens_in": 1200000, "tokens_out": 210000, "cost_usd": 6.75 }
],
"by_role": [
{ "role": "implementer", "runner": "builtin", "model": "anthropic:claude-sonnet-4-6", "calls": 60, "cost_usd": 4.20 }
],
"recent_calls": [
{
"ticket_id": "uuid",
"ticket_title": "Add user auth",
"task_title": "Write auth middleware",
"role": "implementer",
"runner": "builtin",
"model": "anthropic:claude-sonnet-4-6",
"status": "success",
"timestamp": "2026-03-09T10:00:00Z",
"cost_usd": 0.14,
"tokens_in": 18000,
"tokens_out": 3200,
"duration_ms": 4200
}
]
}GET /api/usage/claude-code
Returns aggregated usage data parsed from Claude Code's local session log files (~/.claude/). Returns { "available": false } when the logs are not present on the host.
{
"available": true,
"total_sessions": 24,
"today": {
"sessions": 3,
"cost_usd": 0.84
},
"last_7_days": [
{ "date": "2026-03-09", "sessions": 3, "cost_usd": 0.84 }
],
"estimate_note": "Costs are estimates derived from local Claude Code session logs."
}Connect to /ws/events to receive real-time pipeline events as JSON objects.
const token = '<your-token>';
const ws = new WebSocket('ws://localhost:8080/ws/events', [`bearer.${token}`]);
ws.onmessage = (event) => {
const evt = JSON.parse(event.data);
console.log(evt.event_type, evt.message);
};Each message has the same shape as events returned by GET /api/tickets/{id}/events, enriched with ticket context fields:
{
"id": "evt-uuid",
"ticket_id": "uuid",
"task_id": "task-uuid",
"seq": 42,
"event_type": "task_spec_review_pass",
"severity": "info",
"message": "Spec review passed",
"details": null,
"created_at": "2026-03-05T10:06:00Z",
"ticket_title": "Add user authentication",
"submitter": "123456789@s.whatsapp.net"
}The WebSocket endpoint broadcasts all pipeline events to all connected clients. There is no per-ticket subscription filtering on the server side.
The /api/metrics endpoint exposes Prometheus-compatible metrics. It requires the same bearer token authentication as other API routes.
# prometheus.yml scrape config
scrape_configs:
- job_name: foreman
static_configs:
- targets: ['localhost:8080']
metrics_path: /api/metrics| Metric | Type | Labels | Description |
|---|---|---|---|
foreman_tickets_total |
Counter | status |
Total tickets by final status |
foreman_tickets_active |
Gauge | — | Currently active pipelines |
foreman_tasks_total |
Counter | status |
Total tasks by final status |
foreman_llm_calls_total |
Counter | role, model, status |
Total LLM calls |
foreman_llm_tokens_total |
Counter | direction, model |
Total tokens (input/output) |
foreman_llm_duration_seconds |
Histogram | role, model |
LLM call latency |
foreman_cost_usd_total |
Counter | model |
Total cost by model |
foreman_pipeline_duration_seconds |
Histogram | — | End-to-end pipeline duration |
foreman_test_runs_total |
Counter | result |
Test run outcomes |
foreman_retries_total |
Counter | role |
Retry counts by pipeline role |
foreman_rate_limits_total |
Counter | provider |
Rate limit hits by provider |
foreman_tdd_verify_total |
Counter | result |
TDD verification outcomes |
foreman_partial_prs_total |
Counter | — | Partial PR count |
foreman_clarifications_total |
Counter | — | Clarification requests issued |
foreman_secrets_detected_total |
Counter | — | Secrets detected and excluded |
foreman_hook_executions_total |
Counter | hook |
Hook point executions |
foreman_skill_executions_total |
Counter | skill, status |
Skill executions by outcome |
The web UI is a Svelte 5 + TypeScript single-page app built with Vite and Tailwind CSS 4. Build output is embedded into the binary at build time (//go:embed dist) and served from /.
Developer commands:
make dashboard-dev # run Vite dev server
make dashboard-build # build dashboard assets
make build # builds dashboard assets + foreman binaryThe UI provides:
- Ticket list with status indicators and filtering
- Ticket detail view with task breakdown, cost summary, and event log
- Live event feed via WebSocket
- Cost overview (today, week, month) with budget indicators
- Active pipeline monitor
- Configuration —
[dashboard]config reference - Deployment — exposing the dashboard over HTTPS in production
- Getting Started — generating a dashboard token during initial setup