Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions docs/code/dashboard.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
title: "Observability Dashboard"
description: "A local web dashboard for worker run history: per-task timelines, stage-by-stage outcomes, aggregate stats, and worker logs"
description: "A local web dashboard for worker run history: per-task timelines, stage-by-stage outcomes, aggregate stats, run retries, and worker logs"
section: "Server Automation"
order: 2
dateModified: 2026-08-27
---

# Observability Dashboard

`devintern dashboard` serves a local web dashboard over the worker's run history: every task, PR mention, and scheduled automation the worker handled, the stages each run went through (feasibility, implementation, self-review, change requests, outcome), aggregate stats like success rate and runs per week, and the worker's own log output.
`devintern dashboard` serves a local web dashboard over the worker's run history: every task, PR mention, and scheduled automation the worker handled, the stages each run went through (feasibility, implementation, self-review, change requests, outcome), aggregate stats like success rate and runs per week, a retry action for failed runs, and the worker's own log output.

All data is read from the worker's local database (`.devintern-code/queue.db`). Nothing is uploaded anywhere: the dashboard runs on your machine and binds to localhost by default.

Expand Down Expand Up @@ -38,6 +38,36 @@ Success and escalation rates are computed over finished runs only. Run duration

The ticket link and description snapshot work for remote trackers whose web URLs can be derived from base configuration plus the task key (Jira, GitHub Issues, GitLab, Azure DevOps, Asana, Trello). Linear issue links need the organization slug, so those keys stay plain text there; markdown-file runs (including scheduled automations) have no tracker page at all. Descriptions are persisted when the run begins, so history keeps showing what was asked even if the ticket is later edited or deleted.

## Retrying a run

Failed, escalated, and abandoned runs (and only those — succeeded runs have nothing to redo, deferred runs retry on their own schedule, and in-progress runs are still going) show a **Retry this run** action on the run detail page. Confirming it schedules the same flow a support engineer would run by hand:

```bash
devintern PROJ-123 --force
```

How the retry executes depends on where the dashboard runs:

- **Workspace worker (fleet mode)** — the default dashboard served by `devintern worker` inserts the retry into the shared workspace database, and the worker's retry-queue acquirer picks it up (default every 5 seconds, tunable with `WORKER_RETRY_INTERVAL_SECONDS`). The retry then runs through exactly the same pipeline as any fleet task: routing rules pick the repo, the task gets a disposable worktree from the bare clone, the per-repo environment applies, and the repo run lock serializes concurrent work. `--force` bypasses the incomplete-attempt retry gate, like the manual CLI flow.
- **Standalone `devintern dashboard`** — when the dashboard runs by itself inside a repo checkout, it spawns `devintern <TASK> --force` as its own subprocess instead, mirroring the manual CLI flow; branch selection, worktree handling, and comments behave exactly like the CLI.

In both modes the new attempt appears as a fresh run in the run list; the dashboard shows success/failure feedback for the trigger itself plus the new run's progress through its stages.

Safeguards:

- A confirmation prompt states exactly what will be re-run before anything starts.
- The actor must be signed in (`devintern login`); when `DASHBOARD_RETRY_EMAILS` is set, only those support-role email addresses may trigger retries.
- Retries are serialized per task: while a retry is already scheduled or running (including an attempt recorded by the worker), further triggers are refused.
- Every trigger is audited in `.devintern-code/queue.db` (`run_retry_audit`): who retried, when, and against which original run. The run detail page lists this history under "Retry history".

### Environment variables

| Variable | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------- |
| `DASHBOARD_PORT` | Port to listen on when `--port` is not given |
| `DASHBOARD_RETRY_EMAILS` | Comma-separated allowlist of emails authorized to trigger retries; unset means any signed-in user |
| `WORKER_RETRY_INTERVAL_SECONDS` | How often the workspace worker drains scheduled dashboard retries (default 5, minimum 1) |

## Where the logs come from

The worker daemon tees its own console output into `worker.stdout.log` and `worker.stderr.log` in the workspace home (`~/.devintern`), so the Logs tab works no matter how the daemon is launched — a systemd unit, launchd agent, cron wrapper, or a plain terminal — without the service definition having to redirect stdout/stderr. Each file rotates to `<name>.1` past 8 MiB. The Logs tab tails those capture files — whether or not the worker is currently running — and only reads a bounded tail (the most recent ~256 KiB per file, at most 500 lines), so huge logs never slow down or bloat the dashboard.
Expand Down Expand Up @@ -75,7 +105,8 @@ The dashboard is backed by a small read-only JSON API you can use directly, for
| Endpoint | Returns |
| --------------------------- | --------------------------------------------------------------------- |
| `GET /api/runs` | Paginated run list (`limit`, `offset`, `status`, `origin`, `taskKey`); `origin=scheduled` is supported |
| `GET /api/runs/:id` | One run with its stage timeline |
| `GET /api/runs/:id` | One run with its stage timeline and retry metadata |
| `POST /api/runs/:id/retry` | Schedule a re-run of the task behind a failed/escalated/abandoned run (requires sign-in) |
| `GET /api/stats?window=30d` | Aggregate stats (`7d`, `30d`, `90d`, or `all`) |
| `GET /api/worker` | Worker liveness, queue counts, agent PRs, poll cursors |
| `GET /api/logs` | Recent worker log entries (`limit` 1–1000, default 500; `level` all/info/warn/error) |
Expand Down
18 changes: 17 additions & 1 deletion packages/code/src/dashboard-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import { join, normalize, resolve } from "path";
import {
DashboardData,
handleLogs,
handleRetryRun,
handleRuns,
handleRunDetail,
handleStats,
handleWorkerStatus,
} from "./lib/dashboard-api";
import type { RetryHandlerDeps } from "./lib/dashboard-api";

export const DEFAULT_DASHBOARD_PORT = 4400;

Expand All @@ -31,6 +33,14 @@ export interface DashboardServerOptions {
dbPath?: string;
/** Project root used to locate the worker lock file. */
workingDir?: string;
/**
* Retry execution mode (default `spawn`). The workspace worker passes
* `schedule` so dashboard retries are drained through the fleet pipeline;
* a standalone `devintern dashboard` keeps the detached-CLI spawn.
*/
retryMode?: "spawn" | "schedule";
/** Collaborator overrides for the retry action (tests). */
retryDeps?: RetryHandlerDeps;
/** Directories to search for worker capture files (primary first). */
logDirs?: string[];
}
Expand Down Expand Up @@ -93,6 +103,7 @@ export function startDashboardServer(
const data = new DashboardData({
dbPath: options.dbPath,
workingDir: options.workingDir,
retryMode: options.retryMode,
logDirs: options.logDirs,
});
const uiDir = resolveUiDir();
Expand All @@ -105,15 +116,20 @@ export function startDashboardServer(
}

const runDetailPattern = /^\/api\/runs\/([^/]+)$/;
const runRetryPattern = /^\/api\/runs\/([^/]+)\/retry$/;

const server = Bun.serve({
port,
hostname: host,
fetch(request: Request): Response {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const { pathname } = url;

if (pathname.startsWith("/api")) {
const retry = request.method === "POST" ? pathname.match(runRetryPattern) : null;
if (retry) {
return json(await handleRetryRun(data, retry[1], options.retryDeps));
}
if (request.method !== "GET") {
return json({ status: 405, body: { error: "method not allowed" } });
}
Expand Down
Loading
Loading