From 6709862c1a24551d21fb491a3042dc3b99bd027f Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 31 May 2026 16:58:12 -0700 Subject: [PATCH] =?UTF-8?q?feat(tracker-web):=20redesign=20fleet=20jobs=20?= =?UTF-8?q?list=20=E2=80=94=20stage=20chips,=20filters,=20hide-shipped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clickable stage chips with live counts (color-coded) replace the plain dropdown; click to filter, click again to clear. - Search box (key / repo / id) + 'Hide shipped' checkbox (persisted) + 'N of M shown'. - Redesigned table: color-coded stage badges, priority emphasis, a Repo column (PR target), newest-first sort, bordered/zebra layout. - All filtering is client-side over a 100-job window (instant + accurate counts); list/links/New-Job form unchanged. FleetJob gains repo/baseBranch. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/app/dashboard/fleet/jobs/page.tsx | 182 +++++++++++++----- .../tracker-web/src/lib/fleet-client.ts | 2 + 2 files changed, 141 insertions(+), 43 deletions(-) diff --git a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx index cac0350f..5eb8a33a 100644 --- a/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx +++ b/dashboards/tracker-web/src/app/dashboard/fleet/jobs/page.tsx @@ -8,7 +8,6 @@ import { useAuth } from '@/lib/auth-context'; import { listJobs, submitJob, type FleetJob } from '@/lib/fleet-client'; const STAGES = [ - '', 'queued', 'blocked', 'assigned', @@ -18,7 +17,28 @@ const STAGES = [ 'shipped', 'failed', 'dead_letter', -]; +] as const; + +// Semantic badge styling per stage (token-backed Tailwind colors). +const STAGE_STYLE: Record = { + queued: 'bg-muted text-muted-foreground', + blocked: 'bg-amber-500/15 text-amber-700 dark:text-amber-400', + assigned: 'bg-sky-500/15 text-sky-700 dark:text-sky-400', + building: 'bg-blue-500/15 text-blue-700 dark:text-blue-400', + review: 'bg-amber-500/15 text-amber-700 dark:text-amber-400', + testing: 'bg-purple-500/15 text-purple-700 dark:text-purple-400', + shipped: 'bg-green-600/15 text-green-700 dark:text-green-400', + failed: 'bg-destructive/15 text-destructive', + dead_letter: 'bg-destructive/15 text-destructive', +}; +const stageStyle = (s: string) => STAGE_STYLE[s] ?? 'bg-muted text-muted-foreground'; +const stageLabel = (s: string) => s.replace(/_/g, ' '); +const PRIORITY_STYLE: Record = { + critical: 'text-destructive font-semibold', + high: 'text-amber-700 dark:text-amber-400', + medium: 'text-muted-foreground', + low: 'text-muted-foreground', +}; const POLL_INTERVAL = 30_000; // MVP: PR-mode target repos (local checkouts under the factory's repo base). @@ -59,6 +79,8 @@ export default function FleetJobsPage() { const { token } = useAuth(); const [jobs, setJobs] = useState([]); const [stage, setStage] = useState(''); + const [hideShipped, setHideShipped] = useState(false); + const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); // New-job form state @@ -73,18 +95,18 @@ export default function FleetJobsPage() { const [submitting, setSubmitting] = useState(false); const [submitMsg, setSubmitMsg] = useState<{ ok: boolean; text: string } | null>(null); + // Fetch the full recent window once; stage / hide-shipped / search are applied + // client-side so the stage counts stay accurate and filtering is instant. const refresh = useCallback(async () => { try { - const params: Record = { limit: '50' }; - if (stage) params.stage = stage; - const res = await listJobs(params as never); + const res = await listJobs({ limit: '100' } as never); setJobs(res.jobs); } catch { /* degrade */ } finally { setLoading(false); } - }, [stage]); + }, []); useEffect(() => { if (!token) return; @@ -93,6 +115,33 @@ export default function FleetJobsPage() { return () => clearInterval(id); }, [token, refresh]); + // Restore the hide-shipped preference (client-only; avoids hydration mismatch). + useEffect(() => { + setHideShipped(localStorage.getItem('fleet_hide_shipped') === '1'); + }, []); + const toggleHideShipped = useCallback((next: boolean) => { + setHideShipped(next); + localStorage.setItem('fleet_hide_shipped', next ? '1' : '0'); + }, []); + + // Stage counts (over everything fetched) + the filtered, sorted view. + const stageCounts = jobs.reduce>((acc, j) => { + acc[j.stage] = (acc[j.stage] ?? 0) + 1; + return acc; + }, {}); + const q = search.trim().toLowerCase(); + const visible = jobs + .filter(j => { + if (stage && j.stage !== stage) return false; + if (hideShipped && j.stage === 'shipped') return false; + if (q) { + const hay = `${j.idempotencyKey} ${j.repo ?? ''} ${j.id}`.toLowerCase(); + if (!hay.includes(q)) return false; + } + return true; + }) + .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)); + const handleSubmit = useCallback(async () => { if (!body.trim()) { setSubmitMsg({ ok: false, text: 'Job body is required.' }); @@ -291,64 +340,111 @@ export default function FleetJobsPage() { {/* Filters */} -
- - +
+ + {/* Search + hide-shipped */} +
+ setSearch(e.target.value)} + placeholder="Search key / repo / id…" + className="w-64 rounded border bg-background px-2 py-1 text-sm" + aria-label="Search jobs" + /> + + + {visible.length} of {jobs.length} shown + +
{/* Table */} {loading ? (

Loading jobs...

- ) : jobs.length === 0 ? ( -

No jobs match the current filter.

+ ) : visible.length === 0 ? ( +

+ {jobs.length === 0 ? 'No jobs yet — submit one above.' : 'No jobs match the filters.'} +

) : ( -
+
- - - - - - - + + + + + + + - {jobs.map(j => ( - - + - - - - - + + + diff --git a/dashboards/tracker-web/src/lib/fleet-client.ts b/dashboards/tracker-web/src/lib/fleet-client.ts index 5e041b57..e96ad552 100644 --- a/dashboards/tracker-web/src/lib/fleet-client.ts +++ b/dashboards/tracker-web/src/lib/fleet-client.ts @@ -25,6 +25,8 @@ export interface FleetJob { updatedAt: string; reviewPolicy?: ReviewPolicy; reviewDecisions?: ReviewDecision[]; + repo?: string; + baseBranch?: string; } export interface FleetFactory {
Idempotency KeyStagePriorityKindAttemptsCreated
JobStagePriorityRepoAttemptsCreated
+ {visible.map(j => ( +
{j.idempotencyKey} + {j.kind !== 'leaf' && ( + {j.kind} + )} - - {j.stage} + + + {stageLabel(j.stage)} {j.priority}{j.kind}{j.attempts} + + {j.priority} + + {j.repo ? ( + {j.repo} + ) : ( + + )} + {j.attempts} {new Date(j.createdAt).toLocaleString()}