Skip to content
Docs Operate Detent Mirrored upstream
dashboard-api.md
Docs source v0.57.0 154392918736 View this file at the pinned commit ↗

Dashboard And APIs

Back to README

The web dashboard starts with the main detent command. In running mode it shows live counts, running issues, retry queue, blocked work, completed sessions, token totals, budget status, Codex rate-limit snapshots, and GitHub REST and GraphQL rate-limit snapshots with per-cycle query cost contributors when the GitHub connector reports them. The shared GitHub API health indicator separates primary quota exhaustion from observed secondary REST backoff, so a healthy primary budget can still show backoff when GitHub returned 429 for endpoint families such as pull requests or check runs. Open the indicator to see remaining primary quota, reset times, backed-off endpoint families, last status codes, retry timing, and tracker refresh timing.

When an agent backend reports model_context_window, Detent also surfaces context pressure for running and recent sessions. Context pressure is the session's total_tokens divided by the model context window. The JSON snapshot includes context_pressure.total_tokens, context_limit_tokens, percent_used, and threshold_state; the web dashboard and TUI render the same value as a compact percent. Thresholds are normal below 70%, watch at 70%, warning at 85%, and critical at 95%. Unknown context windows omit the derived fields instead of reporting a misleading zero.

Context pressure is a model-window signal, not a Detent stop threshold. For Codex, codex.turn_timeout_ms is an inter-message liveness bound, despite its name. Each stream message starts a new timer. Omitting the key still uses the one-hour (3600000 ms) default, and continuous messages can keep one turn alive indefinitely. Lowering this value therefore detects silence sooner but does not cap total turn duration. codex.stall_timeout_ms is also reset by stream activity; when both liveness bounds are enabled, the shorter deadline wins for each receive.

agent.max_turns bounds the provider turns reported during one Detent session and is also passed to Claude Code's native turn limiter. agent.max_turn_duration_ms is the total wall-clock bound for each provider turn attempt. agent.max_session_duration_ms spans the full persisted Detent session, including a failed resume attempt and its fresh fallback. The session bound defaults to two hours (7200000 ms); 0 disables it. The per-turn bound defaults to 0. When both are configured, the shorter applicable deadline wins.

agent.no_progress_timeout_ms defaults to 90 minutes (5400000 ms). While an agent is running, Detent checks the workspace fingerprint, diff, unpushed commits, and Codex Workpad content. Any change resets the heartbeat; an unchanged session is cancelled when the timeout expires. Turn, duration, and no-progress breaches cancel the worker through Detent's normal owned process context, so process-tree reaping, scratch cleanup, session completion, and slot release still run. Detent records a cause fingerprint and parks resumable work in Rework, or returns an empty attempt to Todo.

agent.merge_worker_startup_timeout_ms independently bounds how long a dispatched merge runner may take to report its first startup progress. It defaults to four minutes (240000 ms) and is enforced by the worker context, independent of polling.interval_ms and project refresh duration. Workspace creation publishes startup progress before checkout and bootstrap begin, so healthy workspace setup stops this timer.

agent.merge_worker_max_duration_ms is a separate hard wall-clock ceiling for the full lifetime of a worker dispatched in Merging, starting when Detent acquires its slot. It defaults to six hours (21600000 ms), is not renewed by progress, and overrides the disabled general duration defaults for merge work. On breach, Detent cancels the owned worker, releases its slot, logs the elapsed time and last progress marker at WARN, and parks the issue in Blocked.

agent.max_session_tokens is an absolute configured ceiling for a session. total_tokens counts input, output, cache-created, and cache-read tokens, accumulated across every turn of the session — cached context is re-counted on each turn, so a healthy session accrues millions of tokens within minutes. Use max_session_tokens as an additional token-consumption backstop; a value near one turn's worth of context terminates every session at its ceiling. agent.max_session_context_multiplier derives a ceiling from the reported context window when that window is known. A session can show high context pressure before either ceiling is exceeded, and a low-pressure session can still hit a lower absolute agent.max_session_tokens value. Token rows also show cache-read efficiency when cached input is reported: cached input divided by input tokens. Use that value with context pressure to evaluate whether thread-resume behavior is preserving useful context without repeatedly filling the window.

budget.billing_mode accepts metered or subscription. Metered mode enforces configured USD caps and the USD progress breaker. Subscription mode keeps notional spend telemetry but never refuses dispatch or parks work based on USD; Detent instead scales dispatch concurrency with the lowest reported primary or secondary provider rate-window percentage. An omitted mode defaults to subscription, so USD controls are inert unless metered billing is declared explicitly.

agent.no_progress_token_limit defaults to 25000000 tokens and is enforced in both billing modes. Detent sums persisted total_tokens for the issue across attempts after its latest accepted lane or PR advancement. Reaching the limit parks the issue even when USD is only notional subscription telemetry. Set the value to 0 to disable the token breaker.

agent.no_progress_spend_limit_usd defaults to a base limit of 3, below the default per-issue budget backstop, and is enforced only when billing_mode: metered. The effective limit scales with the session's reasoning effort: unknown/low 1x, medium 1.5x, high 3x, xhigh 6x, and max/ultracode 8x. These multipliers assume one retry at the observed cost profile should fit before the breaker fires; detent doctor warns when an effort tier's effective limit is below its observed p50 per-session cost and recommends a base limit with 1.5x retry-cost headroom.

Duration limits stop a single overlong turn or session regardless of message volume. The cross-session progress breakers can stop repeated or expensive work even when each individual session stays below its duration cap. The token breaker remains blocking on subscription fleets; the USD breaker is metered-only.

PR creation, a new head commit, a dirty-to-clean mergeability transition, and a failing-to-passing CI transition each reset the shared token and USD window. Usage accumulates only while the PR fingerprint remains static. When an effective limit trips, Detent parks the issue in Blocked and identifies whether no PR evidence was produced or a linked PR remained static. The latter points operators toward merge-train capacity and serialization tuning; the former recommends narrowing or splitting the task. The next worker must explain the missing progress signal in its first Workpad update before using tools. detent doctor reports both effective brakes for every project and warns when neither is active.

agent.failure_breaker pauses new project dispatches when the same failure class reaches same_class_limit attempts inside window_seconds. The default is five matching failures in one hour, followed by a one-hour cooldown. After the cooldown or a workflow reload, Detent permits exactly one canary attempt; a success or different failure class closes the breaker, while the same class starts a fresh cooldown. The board banner shows the active class, count, and window.

Daily budget caps are scoped to the configured project. Session rows persist the project ID; on upgrade, Detent backfills older rows first from work attempts and then by matching each identifier's repository prefix against the configured project registry. Any session that remains unattributed counts toward every project's daily cap as a conservative fallback. detent doctor warns while unattributed completed sessions exist for the current UTC day.

agent.resume_orphaned_sessions defaults to true. After an unclean Detent restart, active sessions whose provider identity was journaled are preflighted and resumed with a short continuation prompt. Missing provider state, unsupported backends, and failed resume handshakes automatically fall back to the full fresh continuation prompt. Set the field to false to retain fresh redispatch behavior for every restart.

Completed issues persist an efficiency receipt built from session, attempt, usage, and workflow-lane rows. Receipts appear in the project Runs table and issue detail sheet; Reports shows per-merged-issue percentiles, cache share, first-attempt merge rate, dwell decomposition, anomalies, and a trailing-window baseline. The default anomaly threshold is 3x the project baseline and can be changed per workflow. OTLP lifecycle export is optional and disabled when no endpoint is configured:

observability:
  efficiency:
    anomaly_tokens_multiple: 3
    anomaly_sessions_multiple: 3
    anomaly_dwell_multiple: 3
  otlp:
    endpoint: http://127.0.0.1:4318
    service_name: detent
    timeout_ms: 5000

The exporter posts OTLP HTTP/JSON traces to /v1/traces with linked detent.dispatch, detent.session, detent.gate, and detent.merge spans. Static collector headers may be supplied with observability.otlp.headers.

Useful endpoints:

Route Purpose
/ Web dashboard.
/kanban Read-only fleet Kanban board across all registered projects. The sidebar link appears only when more than one project is registered.
/projects/<id> Project-scoped dashboard overview.
/projects/<id>/kanban Project-scoped Kanban board; read-only or integration mode follows that project's workflow config.
/projects/<id>/runs Project running, retry, blocked, and recent session details.
/projects/<id>/configuration Project workflow and runtime configuration view.
/projects/<id>/diagnostics Project health, board flow, and telemetry diagnostics.
/settings Fleet settings and configuration summary.
/reports Usage reports for spend, tokens, projects, issues, PRs, and models.
/health Server health and configured dependency checks.
/events Server-sent dashboard updates. Use ?view=kanban for the fleet board and ?project=<id>&view=kanban for a project board.
/api/v1/openapi.yaml Public OpenAPI 3 catalog for the stable JSON API. HTML, HTMX, and SSE routes are excluded.
/api/v1/state JSON telemetry snapshot.
/api/v1/timeseries?window=10m&bucket=1m Fleet chart samples for running agents, tokens/sec, and completions.
/api/v1/operator-tools/<name> Invoke one shared read-only operator tool with a JSON object via POST; requires read scope and rejects mutation tools.
/api/v1/projects/<id>/state Project-scoped JSON telemetry snapshot.
/api/v1/projects/<id>/timeseries?window=10m&bucket=1m Project chart samples for running agents, token spend, and board flow.
/api/v1/projects/<id>/issues/explanation?reference=<issue> Versioned JSON explanation of an issue's current lane, runtime state, evidence, and degraded sources.
/api/v1/projects/<id>/work-items Create a runtime work item with POST for local_sqlite and github_local trackers.
/api/v1/refresh Request an orchestrator refresh with POST.
/api/v1/webhooks/github Accept signed GitHub webhook deliveries with POST.
/api/v1/<issue> JSON detail for a known board, pipeline, running, retrying, or blocked issue.

The wildcard issue route accepts an issue ID, canonical identifier, issue URL, bare number, or #number. Add ?project=<id> when a number or other reference exists in more than one project; an unscoped collision returns ambiguous_issue_reference. The response reports the board lane separately from runtime activity (idle, running, retrying, or blocked). Board data takes precedence over pipeline and runtime copies for lane and identity, while runtime activity uses running, retrying, then blocked precedence. Completed and tracker-drift-only items are not part of this route.

Magic-link authentication is disabled unless the top-level auth block selects magic_link. When enabled, dashboard pages, browser API calls, SSE streams, and mobile views require a persisted session. Existing API keys and api_token credentials continue through their normal API checks, while signed GitHub and token-authenticated intake webhooks keep their dedicated authentication.

auth:
  mode: magic_link
  public_url: https://detent.example.com
  allowed_emails:
    - operator@example.com
  link_ttl: 15m
  session_ttl: 720h
  smtp:
    host: smtp.example.com
    port: 587
    username: smtp-user
    password: smtp-password
    from: detent@example.com

public_url is recommended for reverse-proxy deployments so email and CLI links use the externally reachable origin. Without it, the running server uses its resolved dashboard URL and the CLI uses the configured local port. SMTP credentials are optional, but username and password must be set together. Detent uses STARTTLS when the server advertises it. TLS termination for the dashboard remains the operator's responsibility.

Allowed submissions always receive the same “check your inbox” page as denied submissions. Links are single-use, short-lived, and stored only as SHA-256 hashes. Sessions are also hashed in SQLite, survive restarts, and expire after session_ttl. Auth configuration changes require a Detent restart.

If SMTP delivery is unavailable, create the same one-time link directly:

detent auth link operator@example.com --format pretty

Dashboard OIDC Authentication

Set auth.mode to oidc to use any OpenID Connect provider that supports discovery and the authorization-code flow. Detent uses S256 PKCE, binds and validates state and nonce, verifies the ID-token signature and time claims, and requires the configured issuer and client ID to match the token exactly. The provider must include a verified email claim; authentication alone does not grant board access.

auth:
  mode: oidc
  public_url: https://detent.example.com
  allowed_emails:
    - operator@example.com
  allowed_domains:
    - example.org
  session_ttl: 12h
  oidc:
    issuer_url: https://identity.example.com
    client_id: detent-dashboard
    client_secret: replace-with-provider-secret
    scopes:
      - profile
      - groups

Register this exact redirect URI with the provider:

https://detent.example.com/auth/oidc/callback

issuer_url is the exact issuer value from the provider's /.well-known/openid-configuration document, not the discovery-document URL. Detent always requests openid and email, then appends configured scopes. Allowed email and domain comparisons are case-insensitive; a domain entry is an exact domain and does not implicitly include subdomains. An absent or false email_verified claim is denied. Keep client_secret only in the permission-restricted global.yaml and do not commit that file.

OIDC uses the same hashed SQLite sessions as magic links. Sessions survive Detent restarts and expire after session_ttl; changing OIDC settings requires a restart. Existing API keys, api_token, signed GitHub webhooks, and intake webhooks retain their independent authentication paths.

For a Tailscale-only dashboard, set public_url and the provider redirect URI to the HTTPS Tailscale hostname, such as https://buildbox.example-tailnet.ts.net. The browser completing sign-in must be connected to that tailnet. Behind a reverse proxy, use the externally visible HTTPS origin for both values and forward requests to Detent without rewriting /auth/oidc/callback. TLS termination remains the operator's responsibility.

WorkOS AuthKit

Create an OAuth application in WorkOS Connect, add Detent's callback to its redirect URIs, and copy an application credential's client ID and secret. Use the AuthKit domain's issuer value, typically https://<subdomain>.authkit.app; its OpenID configuration is available at /.well-known/openid-configuration. WorkOS documents email and email_verified in the issued ID token. A minimal WorkOS configuration is:

auth:
  mode: oidc
  public_url: https://detent.example.com
  allowed_domains: [example.com]
  oidc:
    issuer_url: https://example.authkit.app
    client_id: client_01EXAMPLE
    client_secret: replace-with-workos-credential
    scopes: [profile]

See the WorkOS OIDC metadata and token claims references when confirming an environment's issuer and claims.

Clerk

Create an OAuth application in the Clerk Dashboard as described in Clerk's OIDC provider guide, allow the openid, email, and optional profile scopes, add Detent's callback URI, and copy the client ID and secret. Copy the Discovery URL shown in the application settings and use its returned issuer value. This is normally the Clerk Frontend API origin, such as https://verb-noun-00.clerk.accounts.dev in development or https://clerk.example.com for a configured production domain.

auth:
  mode: oidc
  public_url: https://detent.example.com
  allowed_emails: [operator@example.com]
  oidc:
    issuer_url: https://verb-noun-00.clerk.accounts.dev
    client_id: oauth_app_example
    client_secret: replace-with-clerk-secret
    scopes: [profile]

API Authentication And Work-Item Submission

Configure a top-level api_token in global.yaml, or set DETENT_API_TOKEN to override it at runtime. Use a high-entropy value; the recommended shape is a detent_ prefix followed by a random secret. Mutating API routes require Authorization: Bearer <token> or X-API-Key: <token>. When a token is configured, read-only GET /api/v1/* routes require it too. GET /health stays unauthenticated, and the GitHub webhook keeps its HMAC signature check.

If Detent binds a non-loopback host such as 0.0.0.0 without an api_token, API routes fail closed and mutating routes return 403 until a token is configured. With no token on loopback, read-only API routes remain open for local development.

For a non-loopback bind that still needs tokenless same-host reads, opt in with trust_loopback_peer_read: true in global.yaml. Detent then grants a read-only credential to GET requests whose raw TCP peer address is loopback, even when api_token is configured. A supplied invalid, expired, or revoked token still fails authentication. X-Forwarded-For, Forwarded, X-Real-IP, and other forwarded-client metadata never affect this decision. The setting hot-reloads.

Do not enable trust_loopback_peer_read behind a reverse proxy on the same host. Every remote request relayed by that proxy appears to Detent to have a loopback direct peer and would receive read access.

Remote MCP

The running Detent web server exposes the shared read-only operator catalog at /mcp using MCP Streamable HTTP. It uses the web server's existing listener and shutdown lifecycle; no second port or credential system is created. Configure a remote MCP client with a URL such as https://detent.example.com/mcp and send a scoped API key as Authorization: Bearer <key> or X-API-Key: <key>.

Remote MCP requires an all-projects key whose only scope is read. Static api_token values, dashboard sessions and cookies, loopback peer trust, write/admin-only keys, and project-scoped keys are not accepted. Create a dedicated key from the API Keys dashboard or POST /api/v1/keys; the server applies the existing per-IP and per-key API rate limits to every MCP request.

The endpoint supports the same protocol revisions and exactly the same five tools as detent mcp over stdio. Requests, tool arguments, tool results, and HTTP response envelopes are bounded. GET streaming is not needed by this read-only surface and returns 405; clients receive each JSON-RPC response on the POST that submitted its request. Clients can end a session with DELETE /mcp and its Mcp-Session-Id header.

Terminate TLS at Detent or a trusted reverse proxy for remote access. Public MCP URLs must use HTTPS; plain HTTP is acceptable only when testing through a loopback URL. Configure proxies and tracing systems to redact Authorization and X-API-Key headers; Detent never includes either credential in protocol errors, API usage records, or application logs.

Private Dashboard URL Access

For a personal deployment that needs remote dashboard access without a VPN, Detent can require a private, unguessable URL. Enable it with the public URL served by your TLS proxy:

detent auth token enable --base-url https://detent.example.com

The command generates a 256-bit URL-safe token, stores this configuration in the permission-restricted global config, and prints the private URL once:

dashboard_access:
  mode: private_token
  token: generated_value
  allow_write: false

Opening the printed ?token=... URL establishes a Secure, HttpOnly, SameSite session cookie and redirects to a clean URL. Dashboard pages, mobile views, reports, SSE updates, and read APIs then work without putting the token in later links. Requests without the token or a valid session receive the same non-revealing 404 response as an unknown route. /health, signed GitHub webhooks, intake webhooks, and API clients with their own bearer credentials remain independent of dashboard access.

Private URL access is read-only by default. This matters because the URL is a bearer credential: anyone who receives it can use the dashboard. Set dashboard_access.allow_write: true only when every holder should also be able to stop runs, move or comment on items, rotate API keys, and change runtime state. Rotate a leaked or stale URL immediately:

detent auth token rotate --base-url https://detent.example.com

Rotation hot-reloads and invalidates every existing private dashboard session. Detent redacts the token from reload logs, but browsers, chat systems, proxy logs, and referrer destinations can still expose bearer URLs. This mode is appropriate for a small personal deployment and is weaker than identity-based magic-link or OAuth/OIDC access. TLS termination and proxy log redaction are the deployer's responsibility. Public --base-url values must use HTTPS; plain HTTP is accepted only for loopback testing. To disable the mode, remove dashboard_access from the global config.

Create a runtime work item:

curl -fsS -X POST "http://127.0.0.1:4000/api/v1/projects/digitaldrywood-video/work-items" \
  -H "Authorization: Bearer $DETENT_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "title": "Author beat visuals",
    "description": "Full markdown brief.",
    "state": "Todo",
    "labels": ["video-assets"],
    "fields": {"render_status": "queued"},
    "priority": 2,
    "deliverable": {
      "kind": "artifact",
      "review_url": "http://127.0.0.1:8090/v/example/g/assets"
    }
  }'

The response is 201 with {"id":"...","identifier":"...","url":"..."}. Duplicate submitted identifiers return 409, invalid states or missing title and description return 422, unknown projects return 404, and trackers other than local_sqlite or github_local return 501.

The CLI uses the same validation and writes through the configured tracker directly:

detent work-item add digitaldrywood-video \
  --title "Author beat visuals" \
  --body-file brief.md \
  --label video-assets \
  --field render_status=queued \
  --priority 2 \
  --deliverable-review-url "http://127.0.0.1:8090/v/example/g/assets" \
  --format json

The terminal TUI renders the same telemetry snapshot model for terminal-first operator surfaces. The default binary path starts the web dashboard; embedding the TUI uses the internal/tui Bubble Tea model with a telemetry hub.

The standing Go-vs-Elixir parity checklist is maintained in docs/parity-audit.md.