diff --git a/src/observability/dashboard-server.ts b/src/observability/dashboard-server.ts index 55d0149..43866d9 100644 --- a/src/observability/dashboard-server.ts +++ b/src/observability/dashboard-server.ts @@ -525,190 +525,490 @@ function renderDashboardHtml( snapshot: RuntimeSnapshot, options: DashboardRenderOptions, ): string { + const initialRuntimeLabel = formatRuntimeSeconds( + snapshot.codex_totals.seconds_running, + ); + const totalTokensLabel = formatInteger(snapshot.codex_totals.total_tokens); + const inputTokensLabel = formatInteger(snapshot.codex_totals.input_tokens); + const outputTokensLabel = formatInteger(snapshot.codex_totals.output_tokens); + const initialRateLimits = prettyValue(snapshot.rate_limits); + return ` - Symphony Dashboard + Symphony Observability -
-
-
-

Symphony Dashboard

-

Generated at ${escapeHtml(snapshot.generated_at)}

-
-
- - ${ - options.liveUpdatesEnabled - ? "Live updates connected" - : "Static snapshot" - } -
-
- -
-
-
Running
-
${snapshot.counts.running}
-
-
-
Retrying
-
${snapshot.counts.retrying}
-
-
-
Input Tokens
-
${snapshot.codex_totals.input_tokens}
-
-
-
Output Tokens
-
${snapshot.codex_totals.output_tokens}
-
-
-
Total Tokens
-
${snapshot.codex_totals.total_tokens}
-
-
-
Seconds Running
-
${snapshot.codex_totals.seconds_running.toFixed(1)}
-
-
- -
-

Running Sessions

- - - - - - - - - - - - - ${renderRunningRows(snapshot)} -
IssueStateSessionTurnsLast EventLast MessageLast Event At
-
- -
-

Retry Queue

- - - - - - - - - - ${renderRetryRows(snapshot)} -
IssueAttemptDue AtError
-
- -
-

Rate Limits

-
${escapeHtml(
-          JSON.stringify(snapshot.rate_limits, null, 2) ?? "null",
-        )}
+
+
+
+
+
+

Symphony Observability

+

Operations Dashboard

+

+ Current state, retry pressure, token usage, and orchestration health for the active Symphony runtime. +

+
+ +
+ + + ${options.liveUpdatesEnabled ? "Live" : "Offline"} + +
+
+
+ +
+
+

Running

+

${snapshot.counts.running}

+

Active issue sessions in the current runtime.

+
+ +
+

Retrying

+

${snapshot.counts.retrying}

+

Issues waiting for the next retry window.

+
+ +
+

Total tokens

+

${totalTokensLabel}

+

In ${inputTokensLabel} / Out ${outputTokensLabel}

+
+ +
+

Runtime

+

${initialRuntimeLabel}

+

Generated at ${escapeHtml(snapshot.generated_at)}

+
+
+ +
+
+
+

Rate limits

+

Latest upstream rate-limit snapshot, when available.

+
+
+ +
${escapeHtml(initialRateLimits)}
+
+ +
+
+
+

Running sessions

+

Active issues, last known agent activity, and token usage.

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + ${renderRunningRows(snapshot)} +
IssueStateSessionRuntime / turnsCodex updateTokens
+
+
+ +
+
+
+

Retry queue

+

Issues waiting for the next retry window.

+
+
+ +
+ + + + + + + + + + ${renderRetryRows(snapshot)} +
IssueAttemptDue atError
+
+
@@ -814,18 +1182,64 @@ function renderDashboardHtml( function renderRunningRows(snapshot: RuntimeSnapshot): string { return snapshot.running.length === 0 - ? 'No active sessions.' + ? '

No active sessions.

' : snapshot.running .map( (row) => ` - ${escapeHtml(row.issue_identifier)} - ${escapeHtml(row.state)} - ${escapeHtml(row.session_id ?? "-")} - ${row.turn_count} - ${escapeHtml(row.last_event ?? "-")} - ${escapeHtml(row.last_message ?? "-")} - ${escapeHtml(row.last_event_at ?? "-")} + +
+ ${escapeHtml(row.issue_identifier)} + JSON details +
+ + + ${escapeHtml(row.state)} + + +
+ ${ + row.session_id === null + ? 'n/a' + : `` + } +
+ + ${formatRuntimeAndTurns( + row.started_at, + row.turn_count, + snapshot.generated_at, + )} + +
+ ${escapeHtml( + row.last_message ?? row.last_event ?? "n/a", + )} + ${escapeHtml( + row.last_event ?? "n/a", + )}${ + row.last_event_at === null + ? "" + : ` · ${escapeHtml( + row.last_event_at, + )}` + } +
+ + +
+ Total: ${formatInteger(row.tokens.total_tokens)} + In ${formatInteger( + row.tokens.input_tokens, + )} / Out ${formatInteger(row.tokens.output_tokens)} +
+ `, ) .join(""); @@ -833,22 +1247,107 @@ function renderRunningRows(snapshot: RuntimeSnapshot): string { function renderRetryRows(snapshot: RuntimeSnapshot): string { return snapshot.retrying.length === 0 - ? 'No queued retries.' + ? '

No issues are currently backing off.

' : snapshot.retrying .map( (row) => ` - ${escapeHtml(row.issue_identifier ?? row.issue_id)} + +
+ ${escapeHtml(row.issue_identifier ?? row.issue_id)} + JSON details +
+ ${row.attempt} - ${escapeHtml(row.due_at)} - ${escapeHtml(row.error ?? "-")} + ${escapeHtml(row.due_at)} + ${escapeHtml(row.error ?? "n/a")} `, ) .join(""); } -function escapeHtml(value: string): string { - return value +function formatRuntimeAndTurns( + startedAt: string, + turnCount: number, + generatedAt: string, +): string { + const runtime = formatRuntimeSeconds( + runtimeSecondsFromStartedAt(startedAt, generatedAt), + ); + return Number.isInteger(turnCount) && turnCount > 0 + ? `${runtime} / ${turnCount}` + : runtime; +} + +function formatRuntimeSeconds(seconds: number): string { + if (!Number.isFinite(seconds) || seconds < 0) { + return "0m 0s"; + } + const wholeSeconds = Math.max(0, Math.trunc(seconds)); + const mins = Math.floor(wholeSeconds / 60); + const secs = wholeSeconds % 60; + return `${mins}m ${secs}s`; +} + +function runtimeSecondsFromStartedAt( + startedAt: string, + generatedAt: string, +): number { + const start = Date.parse(startedAt); + const generated = Date.parse(generatedAt); + if ( + !Number.isFinite(start) || + !Number.isFinite(generated) || + generated < start + ) { + return 0; + } + return (generated - start) / 1000; +} + +function formatInteger(value: number): string { + return Number.isFinite(value) + ? Math.trunc(value).toLocaleString("en-US") + : "n/a"; +} + +function prettyValue(value: unknown): string { + return value === null || value === undefined + ? "n/a" + : JSON.stringify(value, null, 2); +} + +function stateBadgeClass(state: string): string { + const normalized = state.toLowerCase(); + if ( + normalized.includes("progress") || + normalized.includes("running") || + normalized.includes("active") + ) { + return "state-badge state-badge-active"; + } + if ( + normalized.includes("blocked") || + normalized.includes("error") || + normalized.includes("failed") + ) { + return "state-badge state-badge-danger"; + } + if ( + normalized.includes("todo") || + normalized.includes("queued") || + normalized.includes("pending") || + normalized.includes("retry") + ) { + return "state-badge state-badge-warning"; + } + return "state-badge"; +} + +function escapeHtml(value: string | number): string { + return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") diff --git a/tests/observability/dashboard-server.test.ts b/tests/observability/dashboard-server.test.ts index 4777117..16d6965 100644 --- a/tests/observability/dashboard-server.test.ts +++ b/tests/observability/dashboard-server.test.ts @@ -34,8 +34,13 @@ describe("dashboard server", () => { }); expect(dashboard.statusCode).toBe(200); expect(dashboard.headers["content-type"]).toContain("text/html"); - expect(dashboard.body).toContain("Symphony Dashboard"); + expect(dashboard.body).toContain("Operations Dashboard"); expect(dashboard.body).toContain("ABC-123"); + expect(dashboard.body).toContain("Running sessions"); + expect(dashboard.body).toContain("Runtime / turns"); + expect(dashboard.body).toContain("Codex update"); + expect(dashboard.body).toContain("Copy ID"); + expect(dashboard.body).toContain("state-badge"); expect(dashboard.body).toContain("window.__SYMPHONY_SNAPSHOT__"); expect(dashboard.body).toContain("/api/v1/events");