From 2253f888c7edc5806038ddde2a2722df3ff8da6d Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 31 May 2026 01:19:57 -0700 Subject: [PATCH] feat(tracker-web): per-run cost/token/time metrics + log download; fix fleet proxy Job detail Runs table now shows Duration, Model, Tokens (in/out + cached) and Cost per run, plus a per-job totals header (cost / tokens / wall-time). Artifacts get a view/download button via a fresh signed URL. Also fix the fleet API proxy to forward to /api/fleet/* (backend mounts fleet under /api) so a live backend resolves; previously it returned 404 and only the mocked e2e passed. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/app/api/fleet/[...path]/route.ts | 3 +- .../app/dashboard/fleet/jobs/[id]/page.tsx | 168 +++++++++++++++--- .../tracker-web/src/lib/fleet-client.ts | 23 ++- 3 files changed, 168 insertions(+), 26 deletions(-) diff --git a/dashboards/tracker-web/src/app/api/fleet/[...path]/route.ts b/dashboards/tracker-web/src/app/api/fleet/[...path]/route.ts index fab494a1..8d6bc366 100644 --- a/dashboards/tracker-web/src/app/api/fleet/[...path]/route.ts +++ b/dashboards/tracker-web/src/app/api/fleet/[...path]/route.ts @@ -9,7 +9,8 @@ const PLATFORM_API = process.env.PLATFORM_API_URL || 'http://localhost:4003'; async function proxy(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { const { path } = await params; - const targetPath = `/fleet/${path.join('/')}`; + // platform-service mounts fleet routes under the /api prefix. + const targetPath = `/api/fleet/${path.join('/')}`; const url = new URL(targetPath, PLATFORM_API); req.nextUrl.searchParams.forEach((value, key) => { diff --git a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx index a4162819..a9275db9 100644 --- a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/[id]/page.tsx @@ -11,6 +11,7 @@ import { getJobRuns, getJobEvents, getJobArtifacts, + getArtifactDownloadUrl, getJobDag, getJobExplain, patchJob, @@ -317,34 +318,95 @@ export default function FleetJobDetailPage() { {/* Runs */}
-

Runs

+
+

Runs

+ {runs.length > 0 && + (() => { + const t = runTotals(runs); + return ( +
+ 0 ? fmtUsd(t.costUsd) : '—'} /> + 0 ? fmtNum(t.tokensIn) : '—'} /> + 0 ? fmtNum(t.tokensOut) : '—'} /> + 0 ? fmtDuration(t.durationMs) : '—'} + /> +
+ ); + })()} +
{runs.length === 0 ? (

No runs yet.

) : ( - - - - - - - - - - - - {runs.map(r => ( - - - - - - +
+
AttemptEngineFactoryResultStarted
#{r.attempt}{r.engine}{r.factoryId ?? '—'}{r.result ?? 'running'} - {new Date(r.startedAt).toLocaleString()} -
+ + + + + + + + + + + - ))} - -
AttemptEngineFactoryResultStartedDurationModelTokens (in/out)Cost
+ + + {runs.map(r => { + const ins = r.insights ?? {}; + const dur = r.endedAt ? Date.parse(r.endedAt) - Date.parse(r.startedAt) : null; + const tin = ins.tokensIn; + const tout = ins.tokensOut; + const tcached = ins.tokensCached; + return ( + + #{r.attempt} + {r.engine} + {r.factoryId ?? '—'} + {r.result ?? 'running'} + + {new Date(r.startedAt).toLocaleString()} + + + {dur != null && dur >= 0 ? fmtDuration(dur) : '—'} + + {ins.model ?? '—'} + + {tin != null || tout != null ? ( + <> + {fmtNum(tin ?? 0)} / {fmtNum(tout ?? 0)} + {tcached ? ( + + {' '} + (+{fmtNum(tcached)} cached) + + ) : null} + + ) : ( + '—' + )} + + + {ins.costUsd != null ? ( + <> + {fmtUsd(ins.costUsd)} + {ins.estimated ? ( + est. + ) : null} + + ) : ( + '—' + )} + + + ); + })} + + + )}
@@ -362,6 +424,16 @@ export default function FleetJobDetailPage() { ({(a.sizeBytes / 1024).toFixed(1)} KB) + ))} @@ -384,6 +456,54 @@ function MetaCard({ label, value }: { label: string; value: string }) { ); } +/** Compact stat chip for the per-job cost/token/time totals. */ +function Stat({ label, value }: { label: string; value: string }) { + return ( + + {label}: + {value} + + ); +} + +/** Sum cost / tokens / wall-time across a job's runs. */ +function runTotals(runs: FleetRun[]) { + let costUsd = 0; + let tokensIn = 0; + let tokensOut = 0; + let durationMs = 0; + for (const r of runs) { + const ins = r.insights ?? {}; + costUsd += ins.costUsd ?? 0; + tokensIn += ins.tokensIn ?? 0; + tokensOut += ins.tokensOut ?? 0; + if (r.endedAt) { + const d = Date.parse(r.endedAt) - Date.parse(r.startedAt); + if (Number.isFinite(d) && d > 0) durationMs += d; + } + } + return { costUsd, tokensIn, tokensOut, durationMs }; +} + +function fmtUsd(n: number): string { + return n < 0.01 && n > 0 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`; +} + +function fmtNum(n: number): string { + return n.toLocaleString(); +} + +function fmtDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + const s = Math.round(ms / 1000); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const rem = s % 60; + if (m < 60) return rem ? `${m}m ${rem}s` : `${m}m`; + const h = Math.floor(m / 60); + return `${h}h ${m % 60}m`; +} + function ReviewGateCard({ job, reviewing, diff --git a/dashboards/tracker-web/src/lib/fleet-client.ts b/dashboards/tracker-web/src/lib/fleet-client.ts index 674e2686..155ff383 100644 --- a/dashboards/tracker-web/src/lib/fleet-client.ts +++ b/dashboards/tracker-web/src/lib/fleet-client.ts @@ -38,6 +38,21 @@ export interface FleetFactory { lastHeartbeatAt: string; } +/** Per-run cost / token / effort metrics reported by a factory. */ +export interface FleetRunInsights { + model?: string; + tokensIn?: number; + tokensOut?: number; + tokensCached?: number; + costUsd?: number; + estimated?: boolean; + turns?: number; + toolCalls?: number; + filesChanged?: number; + linesAdded?: number; + linesDeleted?: number; +} + export interface FleetRun { id: string; jobId: string; @@ -47,7 +62,7 @@ export interface FleetRun { startedAt: string; endedAt?: string; result?: string; - insights: Record; + insights: FleetRunInsights; } export interface FleetEvent { @@ -417,6 +432,12 @@ export async function getJobArtifacts(jobId: string): Promise<{ artifacts: Fleet return apiFetch(`/jobs/${jobId}/artifacts`); } +/** Resolve a short-lived signed download URL for an artifact (e.g. a `log`). */ +export async function getArtifactDownloadUrl(artifactId: string): Promise { + const res = await apiFetchOptional<{ downloadUrl?: string }>(`/artifacts/${artifactId}`); + return res?.downloadUrl ?? null; +} + export async function getJobDag(jobId: string): Promise<{ dag: DagNode } | null> { return apiFetchOptional(`/jobs/${jobId}/dag`); }