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 */}
|
{/* Prompt (the job body) + PR/target config */}
|
||||||
<PromptCard job={job} onChanged={refresh} />
|
<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) */}
|
{/* Review gate (multi-reviewer human gate) */}
|
||||||
{job.stage === 'review' && (
|
{job.stage === 'review' && (
|
||||||
<ReviewGateCard
|
<ReviewGateCard
|
||||||
@ -309,18 +342,36 @@ export default function FleetJobDetailPage() {
|
|||||||
<p className="text-muted-foreground text-sm">No events recorded.</p>
|
<p className="text-muted-foreground text-sm">No events recorded.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{events.map(e => (
|
{groupTimelineEvents(events).map(g =>
|
||||||
<li
|
g.kind === 'single' ? (
|
||||||
key={e.id}
|
<li
|
||||||
className="flex items-start gap-3 text-sm border-l-2 border-muted pl-3 py-1"
|
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(e.at).toLocaleTimeString()}
|
<span className="text-muted-foreground text-xs whitespace-nowrap">
|
||||||
</span>
|
{new Date(g.event.at).toLocaleTimeString()}
|
||||||
<span className="font-medium">{e.type}</span>
|
</span>
|
||||||
{e.actor && <span className="text-muted-foreground">by {e.actor}</span>}
|
<span className="font-medium">{g.event.type}</span>
|
||||||
</li>
|
{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>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</section>
|
</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. */
|
/** Sum cost / tokens / wall-time across a job's runs. */
|
||||||
function runTotals(runs: FleetRun[]) {
|
function runTotals(runs: FleetRun[]) {
|
||||||
let costUsd = 0;
|
let costUsd = 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user