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 48c2d6e5..5ca3a04c 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 @@ -260,6 +260,39 @@ export default function FleetJobDetailPage() { {/* Prompt (the job body) + PR/target config */} + {/* Pull request (surfaced from whichever run opened it) */} + {(() => { + const prRun = runs.find(r => r.prUrl); + if (!prRun?.prUrl) return null; + return ( +
+ Pull Request + + {prRun.prUrl} ↗ + + {prRun.prState && ( + + {prRun.prState} + + )} +
+ ); + })()} + {/* Review gate (multi-reviewer human gate) */} {job.stage === 'review' && ( No events recorded.

) : ( )} @@ -665,6 +716,53 @@ function Stat({ label, value }: { label: string; value: string }) { ); } +type TimelineGroup = + | { kind: 'single'; event: FleetEvent } + | { kind: 'collapsed'; key: string; type: string; count: number; first: number; last: number }; + +/** High-frequency event types collapsed into one summary row so a long-running + * job's timeline isn't buried under hundreds of heartbeats. */ +const COLLAPSIBLE_EVENT_TYPES = new Set(['lease_renewed']); + +/** Fold consecutive runs of a collapsible event type (e.g. lease_renewed) into a + * single "type ×N · over Xm" row; everything else renders verbatim, in order. */ +function groupTimelineEvents(events: FleetEvent[]): TimelineGroup[] { + const out: TimelineGroup[] = []; + let run: FleetEvent[] = []; + const flush = () => { + if (run.length === 0) return; + if (run.length === 1) { + out.push({ kind: 'single', event: run[0]! }); + } else { + const first = run[0]!; + const last = run[run.length - 1]!; + out.push({ + kind: 'collapsed', + key: `grp-${first.id}-${last.id}`, + type: first.type, + count: run.length, + first: Date.parse(first.at), + last: Date.parse(last.at), + }); + } + run = []; + }; + for (const e of events) { + if (COLLAPSIBLE_EVENT_TYPES.has(e.type)) { + if (run.length > 0 && run[0]!.type === e.type) run.push(e); + else { + flush(); + run = [e]; + } + } else { + flush(); + out.push({ kind: 'single', event: e }); + } + } + flush(); + return out; +} + /** Sum cost / tokens / wall-time across a job's runs. */ function runTotals(runs: FleetRun[]) { let costUsd = 0;