- | ${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.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
- ? '
- | ${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");