feat(tracker-web): collapse heartbeat noise + surface PR on job detail
The job detail timeline was buried under hundreds of lease_renewed rows on long-running jobs. Collapse consecutive high-frequency events (lease_renewed) into one "type xN - over Nm" summary row; everything else renders verbatim. Add a prominent Pull Request banner (link + state) sourced from whichever run opened the PR, instead of only the per-attempt Runs column. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
5ad521ad4c
commit
5262583e8b
@ -260,6 +260,39 @@ export default function FleetJobDetailPage() {
|
||||
{/* Prompt (the job body) + PR/target config */}
|
||||
<PromptCard job={job} onChanged={refresh} />
|
||||
|
||||
{/* Pull request (surfaced from whichever run opened it) */}
|
||||
{(() => {
|
||||
const prRun = runs.find(r => r.prUrl);
|
||||
if (!prRun?.prUrl) return null;
|
||||
return (
|
||||
<section
|
||||
className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/20 p-3"
|
||||
aria-label="Pull request"
|
||||
>
|
||||
<span className="text-sm font-semibold">Pull Request</span>
|
||||
<a
|
||||
href={prRun.prUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline break-all"
|
||||
>
|
||||
{prRun.prUrl} ↗
|
||||
</a>
|
||||
{prRun.prState && (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
prRun.prState === 'merged'
|
||||
? 'bg-green-600/15 text-green-700 dark:text-green-400'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{prRun.prState}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Review gate (multi-reviewer human gate) */}
|
||||
{job.stage === 'review' && (
|
||||
<ReviewGateCard
|
||||
@ -309,18 +342,36 @@ export default function FleetJobDetailPage() {
|
||||
<p className="text-muted-foreground text-sm">No events recorded.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{events.map(e => (
|
||||
<li
|
||||
key={e.id}
|
||||
className="flex items-start gap-3 text-sm border-l-2 border-muted pl-3 py-1"
|
||||
>
|
||||
<span className="text-muted-foreground text-xs whitespace-nowrap">
|
||||
{new Date(e.at).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="font-medium">{e.type}</span>
|
||||
{e.actor && <span className="text-muted-foreground">by {e.actor}</span>}
|
||||
</li>
|
||||
))}
|
||||
{groupTimelineEvents(events).map(g =>
|
||||
g.kind === 'single' ? (
|
||||
<li
|
||||
key={g.event.id}
|
||||
className="flex items-start gap-3 text-sm border-l-2 border-muted pl-3 py-1"
|
||||
>
|
||||
<span className="text-muted-foreground text-xs whitespace-nowrap">
|
||||
{new Date(g.event.at).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="font-medium">{g.event.type}</span>
|
||||
{g.event.actor && (
|
||||
<span className="text-muted-foreground">by {g.event.actor}</span>
|
||||
)}
|
||||
</li>
|
||||
) : (
|
||||
<li
|
||||
key={g.key}
|
||||
className="flex items-start gap-3 text-sm border-l-2 border-dashed border-muted pl-3 py-1 text-muted-foreground"
|
||||
title={`${g.count} ${g.type} events from ${new Date(g.first).toLocaleTimeString()} to ${new Date(g.last).toLocaleTimeString()}`}
|
||||
>
|
||||
<span className="text-xs whitespace-nowrap">
|
||||
{new Date(g.first).toLocaleTimeString()}
|
||||
</span>
|
||||
<span>
|
||||
{g.type} <span className="tabular-nums">×{g.count}</span>
|
||||
<span className="text-xs"> · over {fmtDuration(g.last - g.first)}</span>
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user