feat(tracker-web): consolidated fleet overview — breaker panel, budget guardrail, dead-letter triage

Surfaces the §2/§3 signal on the fleet overview page (read-only over existing
endpoints):

- Budget guardrail card: spend vs ceiling bar, projected end-of-window burn
  (highlighted when over ceiling), and per-engine sub-ceiling breakdown — from
  the new /fleet/metrics budget summary.
- Engine circuit-breaker panel: lists only tripped/probing (factory, engine)
  pairs from metrics.engineBreakers.
- Dead-letter triage table with a Re-drive button wired to the redrive operator
  action; filtered client-side so it is correct regardless of server filtering.

All panels render only when their data is present, so the page is unchanged for
fleets without budgets/breakers/dead-letters. Adds a happy-dom page test.

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:
saravanakumardb1 2026-06-01 13:29:52 -07:00
parent bcd806c6ff
commit 705d8e8eaa
3 changed files with 372 additions and 1 deletions

View File

@ -0,0 +1,178 @@
// @vitest-environment happy-dom
/**
* Fleet overview page (§1) the consolidated operations surface.
*
* Verifies the panels that surface the §2/§3 signal: the engine circuit-breaker
* panel, the budget guardrail, and the dead-letter triage list with its Re-drive
* button (which calls the operator action). Uses the jsdom render harness +
* mocked fleet-client/auth-context (no network).
*/
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
const { operatorAction, listFactories, getFleetMetrics, listJobs } = vi.hoisted(() => ({
operatorAction: vi.fn(),
listFactories: vi.fn(),
getFleetMetrics: vi.fn(),
listJobs: vi.fn(),
}));
vi.mock('@/lib/auth-context', () => ({ useAuth: () => ({ token: 'tok' }) }));
vi.mock('@/lib/fleet-client', () => ({
listFactories,
getFleetMetrics,
listJobs,
operatorAction,
}));
import FleetOverviewPage from '@/app/dashboard/fleet/page';
beforeAll(() => {
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
});
const metricsBase = {
productId: 'p',
generatedAt: '2026-06-01T00:00:00.000Z',
jobs: { total: 1, byStage: {}, queueDepth: 0, blocked: 0, active: 0, oldestQueuedAgeMs: null },
factories: {
total: 0,
live: 0,
stale: 0,
byHealth: { ok: 0, degraded: 0, down: 0 },
seatsUsed: 0,
seatsTotal: 0,
utilizationPct: 0,
},
alerts: [],
};
const deadLetterJob = {
id: 'j-dead',
productId: 'p',
stage: 'dead_letter',
idempotencyKey: 'broken-task',
bodyMd: '',
priority: 'medium',
priorityOrder: 2,
capabilities: [],
kind: 'leaf',
attempts: 3,
leaseEpoch: 3,
engine: 'codex',
createdAt: '2026-06-01T00:00:00.000Z',
updatedAt: '2026-06-01T00:00:00.000Z',
};
async function renderPage(): Promise<{ root: Root; container: HTMLDivElement }> {
const container = document.createElement('div');
document.body.appendChild(container);
let root!: Root;
await act(async () => {
root = createRoot(container);
root.render(createElement(FleetOverviewPage));
});
// Flush the async refresh() (Promise.all of the mocked client calls).
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
return { root, container };
}
beforeEach(() => {
vi.clearAllMocks();
listFactories.mockResolvedValue({ factories: [] });
operatorAction.mockResolvedValue({});
// Default: recent jobs empty, dead-letter query returns the broken job.
listJobs.mockImplementation(async (params?: { stage?: string }) =>
params?.stage === 'dead_letter' ? { jobs: [deadLetterJob] } : { jobs: [] }
);
});
describe('fleet overview §1 — consolidated panels', () => {
it('renders the budget guardrail with projection and per-engine breakdown', async () => {
getFleetMetrics.mockResolvedValue({
...metricsBase,
budget: {
ceilingUsd: 100,
spentUsd: 40,
status: 'active',
window: 'monthly',
projectedUsd: 400,
engines: [{ engine: 'codex', spentUsd: 30, ceilingUsd: 30, exhausted: true }],
},
});
const { root, container } = await renderPage();
const budget = container.querySelector('[data-testid="fleet-budget"]');
expect(budget).not.toBeNull();
expect(budget!.textContent).toContain('$40.00 / $100.00');
expect(budget!.textContent).toContain('Projected monthly: $400.00');
expect(budget!.textContent).toContain('codex');
act(() => root.unmount());
container.remove();
});
it('renders only tripped circuit breakers', async () => {
getFleetMetrics.mockResolvedValue({
...metricsBase,
engineBreakers: [
{
factoryId: 'fac_1',
engine: 'codex',
state: 'OPEN',
failureCount: 3,
lastFailureAt: null,
},
{
factoryId: 'fac_1',
engine: 'devin',
state: 'CLOSED',
failureCount: 0,
lastFailureAt: null,
},
],
});
const { root, container } = await renderPage();
const panel = container.querySelector('[data-testid="fleet-breakers"]');
expect(panel).not.toBeNull();
expect(panel!.textContent).toContain('fac_1 · codex');
expect(panel!.textContent).toContain('OPEN');
expect(panel!.textContent).not.toContain('devin'); // CLOSED pairs are hidden
act(() => root.unmount());
container.remove();
});
it('lists dead-letter jobs and re-drives one via the operator action', async () => {
getFleetMetrics.mockResolvedValue({ ...metricsBase });
const { root, container } = await renderPage();
const triage = container.querySelector('[data-testid="fleet-dead-letter"]');
expect(triage).not.toBeNull();
expect(triage!.textContent).toContain('broken-task');
const btn = container.querySelector(
'[aria-label="Re-drive job broken-task"]'
) as HTMLButtonElement;
expect(btn).not.toBeNull();
await act(async () => {
btn.click();
await Promise.resolve();
});
expect(operatorAction).toHaveBeenCalledWith('j-dead', 'redrive');
act(() => root.unmount());
container.remove();
});
it('hides the budget and breaker panels when the metrics omit them', async () => {
getFleetMetrics.mockResolvedValue({ ...metricsBase });
listJobs.mockResolvedValue({ jobs: [] }); // no dead letters either
const { root, container } = await renderPage();
expect(container.querySelector('[data-testid="fleet-budget"]')).toBeNull();
expect(container.querySelector('[data-testid="fleet-breakers"]')).toBeNull();
expect(container.querySelector('[data-testid="fleet-dead-letter"]')).toBeNull();
act(() => root.unmount());
container.remove();
});
});

View File

@ -8,6 +8,7 @@ import {
listFactories,
listJobs,
getFleetMetrics,
operatorAction,
type FleetFactory,
type FleetJob,
type FleetMetrics,
@ -56,23 +57,114 @@ function StageBadge({ stage }: { stage: string }) {
);
}
const usd = (n: number) => `$${n.toFixed(2)}`;
/** Budget guardrail: spend vs ceiling + projected end-of-window burn (§3). */
function BudgetGuardrail({ budget }: { budget: NonNullable<FleetMetrics['budget']> }) {
const pct =
budget.ceilingUsd > 0 ? Math.min(100, (budget.spentUsd / budget.ceilingUsd) * 100) : 0;
const overProjected = budget.projectedUsd !== null && budget.projectedUsd > budget.ceilingUsd;
const exhausted = budget.spentUsd >= budget.ceilingUsd;
return (
<section aria-label="Budget guardrail" data-testid="fleet-budget">
<h2 className="text-lg font-semibold mb-3">Budget</h2>
<div className="rounded-lg border p-4 space-y-3">
<div className="flex items-center justify-between text-sm">
<span>
{usd(budget.spentUsd)} / {usd(budget.ceilingUsd)}{' '}
<span className="text-muted-foreground">({budget.window})</span>
</span>
<span className="text-xs text-muted-foreground capitalize">{budget.status}</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className={`h-full ${exhausted ? 'bg-red-500' : overProjected ? 'bg-yellow-500' : 'bg-green-500'}`}
style={{ width: `${pct}%` }}
/>
</div>
{budget.projectedUsd !== null && (
<p
className={`text-xs ${overProjected ? 'text-yellow-700 dark:text-yellow-400' : 'text-muted-foreground'}`}
>
Projected {budget.window}: {usd(budget.projectedUsd)}
{overProjected && ' — over ceiling at current burn'}
</p>
)}
{budget.engines.length > 0 && (
<ul className="text-xs text-muted-foreground space-y-1">
{budget.engines.map(e => (
<li key={e.engine} className="flex justify-between">
<span className="font-mono">{e.engine}</span>
<span className={e.exhausted ? 'text-red-600 dark:text-red-400' : ''}>
{usd(e.spentUsd)} / {usd(e.ceilingUsd)}
{e.exhausted && ' (capped)'}
</span>
</li>
))}
</ul>
)}
</div>
</section>
);
}
/** Circuit-breaker panel: (factory, engine) pairs currently being routed around (§2). */
function BreakerPanel({ breakers }: { breakers: NonNullable<FleetMetrics['engineBreakers']> }) {
const tripped = breakers.filter(b => b.state !== 'CLOSED');
if (tripped.length === 0) return null;
return (
<section aria-label="Engine circuit breakers" data-testid="fleet-breakers">
<h2 className="text-lg font-semibold mb-3">Engine circuit breakers</h2>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{tripped.map(b => (
<div
key={`${b.factoryId}:${b.engine}`}
className="rounded-lg border border-red-500/30 bg-red-500/5 p-3 text-sm"
>
<div className="flex items-center justify-between">
<span className="font-mono truncate">
{b.factoryId} · {b.engine}
</span>
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${b.state === 'OPEN' ? 'bg-red-500/20 text-red-700 dark:text-red-400' : 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-400'}`}
>
{b.state}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{b.failureCount} failure(s)
{b.lastFailureAt && ` · last ${new Date(b.lastFailureAt).toLocaleTimeString()}`}
</p>
</div>
))}
</div>
</section>
);
}
export default function FleetOverviewPage() {
const { token } = useAuth();
const [factories, setFactories] = useState<FleetFactory[]>([]);
const [jobs, setJobs] = useState<FleetJob[]>([]);
const [metrics, setMetrics] = useState<FleetMetrics | null>(null);
const [deadLetters, setDeadLetters] = useState<FleetJob[]>([]);
const [redriving, setRedriving] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const refresh = useCallback(async () => {
try {
const [facRes, jobRes, metricsRes] = await Promise.all([
const [facRes, jobRes, metricsRes, dlRes] = await Promise.all([
listFactories(),
listJobs({ limit: 10 }),
getFleetMetrics().catch(() => null),
listJobs({ stage: 'dead_letter', limit: 50 }).catch(() => ({ jobs: [] as FleetJob[] })),
]);
setFactories(facRes.factories);
setJobs(jobRes.jobs);
setMetrics(metricsRes);
// Filter client-side too, so the triage list is correct even if the server
// ignores the stage filter.
setDeadLetters(dlRes.jobs.filter(j => j.stage === 'dead_letter'));
} catch {
/* degrade gracefully */
} finally {
@ -80,6 +172,21 @@ export default function FleetOverviewPage() {
}
}, []);
const handleRedrive = useCallback(
async (id: string) => {
setRedriving(id);
try {
await operatorAction(id, 'redrive');
await refresh();
} catch {
/* surfaced on next poll */
} finally {
setRedriving(null);
}
},
[refresh]
);
useEffect(() => {
if (!token) return;
refresh();
@ -134,6 +241,67 @@ export default function FleetOverviewPage() {
</section>
)}
{/* Budget guardrail */}
{metrics?.budget && <BudgetGuardrail budget={metrics.budget} />}
{/* Engine circuit breakers */}
{metrics?.engineBreakers && metrics.engineBreakers.length > 0 && (
<BreakerPanel breakers={metrics.engineBreakers} />
)}
{/* Dead-letter triage */}
{deadLetters.length > 0 && (
<section aria-label="Dead-letter triage" data-testid="fleet-dead-letter">
<h2 className="text-lg font-semibold mb-3">
Dead-letter triage{' '}
<span className="text-sm font-normal text-muted-foreground">
({deadLetters.length})
</span>
</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm" aria-label="Dead-letter jobs">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 pr-4">Key</th>
<th className="pb-2 pr-4">Engine</th>
<th className="pb-2 pr-4">Attempts</th>
<th className="pb-2">Action</th>
</tr>
</thead>
<tbody>
{deadLetters.map(j => (
<tr key={j.id} className="border-b last:border-0 hover:bg-muted/50">
<td className="py-2 pr-4">
<Link
href={`/dashboard/fleet/jobs/${j.id}`}
className="hover:underline font-mono text-xs"
>
{j.idempotencyKey}
</Link>
</td>
<td className="py-2 pr-4 font-mono text-xs">
{j.engine ?? j.engineClass ?? '—'}
</td>
<td className="py-2 pr-4 tabular-nums">{j.attempts}</td>
<td className="py-2">
<button
type="button"
onClick={() => handleRedrive(j.id)}
disabled={redriving === j.id}
aria-label={`Re-drive job ${j.idempotencyKey}`}
className="rounded-md border px-2 py-1 text-xs font-medium hover:bg-muted disabled:opacity-50"
>
{redriving === j.id ? 'Re-driving…' : 'Re-drive'}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)}
{/* Factory cards */}
<section>
<h2 className="text-lg font-semibold mb-3">Factories</h2>

View File

@ -31,6 +31,8 @@ export interface FleetJob {
reviewDecisions?: ReviewDecision[];
/** Concrete engine to run (overrides engineClass); falls back to factory default. */
engine?: FleetEngine;
/** Abstract engine class resolved on the runner when no concrete `engine` is set. */
engineClass?: string;
repo?: string;
baseBranch?: string;
/** PR mode: verify command run in the checkout before the PR opens. */
@ -346,6 +348,25 @@ export interface FleetAlert {
message: string;
}
/** One tripped/probing (factory, engine) circuit-breaker pair (§2). */
export interface EngineBreakerEntry {
factoryId: string;
engine: string;
state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
failureCount: number;
lastFailureAt: string | null;
}
/** Budget guardrail summary surfaced on metrics (§3). */
export interface FleetBudgetSummary {
ceilingUsd: number;
spentUsd: number;
status: string;
window: string;
projectedUsd: number | null;
engines: { engine: string; spentUsd: number; ceilingUsd: number; exhausted: boolean }[];
}
export interface FleetMetrics {
productId: string;
generatedAt: string;
@ -366,6 +387,10 @@ export interface FleetMetrics {
seatsTotal: number;
utilizationPct: number;
};
/** Budget guardrail summary (§3) — null when no budget is configured. */
budget?: FleetBudgetSummary | null;
/** Per-(factory, engine) circuit-breaker snapshot (§2) — process-wide. */
engineBreakers?: EngineBreakerEntry[];
alerts: FleetAlert[];
}