"use client"; import React, { useMemo, useState } from "react"; import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Workflow, Search, Clock, CheckCircle2, XCircle, Loader2, CirclePause, Circle, PlayCircle, Activity, ChevronRight, Send, Tag, Hash, } from "lucide-react"; import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/Card"; import { Button } from "@/components/ui/Button"; import { Badge } from "@/components/ui/Badge"; import { EmptyState } from "@/components/ui/EmptyState"; import { jobs, projects } from "@/lib/api"; import { cn, fmtDate, shortId } from "@/lib/utils"; import type { Job, JobCreate, JobStatus, JobType } from "@/lib/types"; import { JOB_STATUSES, JOB_TYPES } from "@/lib/types"; const DEMO_JOBS: Job[] = [ { id: "j-demo-1", tenant_id: "t1", project_id: "p1", type: "ai_interactive", priority: "normal", status: "completed", payload: { prompt: "Summarize the spec.", temperature: 0.2 }, correlation_id: "cid-summ-001", requested_by: "u1", worker: "asf-worker-3", model: "asf-mock-1", result: { summary: "Mock AI summary for demo purposes." }, usage: { input_tokens: 112, output_tokens: 48, cpu_seconds: 0.02 }, progress_percent: 100, created_at: "2025-11-15T09:00:00Z", started_at: "2025-11-15T09:00:01Z", completed_at: "2025-11-15T09:00:06Z", }, { id: "j-demo-2", tenant_id: "t1", project_id: "p1", type: "testing", priority: "high", status: "running", payload: { suite: "smoke", pattern: "**/test_*.py" }, requested_by: "u2", worker: "asf-worker-1", progress_percent: 42, created_at: "2025-11-15T09:18:00Z", started_at: "2025-11-15T09:18:10Z", }, { id: "j-demo-3", tenant_id: "t1", project_id: "p2", type: "code_analysis", priority: "normal", status: "failed", payload: { repository: "asf/core", branch: "main" }, requested_by: "u1", worker: "asf-worker-2", error: "Repository clone failed", error_detail: { exit_code: 128, stderr: "fatal: repository not found" }, created_at: "2025-11-15T08:55:00Z", started_at: "2025-11-15T08:55:15Z", completed_at: "2025-11-15T08:56:30Z", }, ]; const STATUS_ICON: Record> = { queued: Clock, running: PlayCircle, completed: CheckCircle2, failed: XCircle, cancelled: CirclePause, }; const STATUS_TONE: Record = { queued: "neutral", running: "blue", completed: "green", failed: "red", cancelled: "amber", }; const TYPE_TONE: Record = { ai_interactive: "violet", ai_background: "violet", indexing: "blue", code_analysis: "brand", testing: "green", devops: "amber", notification: "neutral", custom: "red", }; function queueForJobType(t: JobType | string): string { // 1-to-1 mapping with asf.jobs DIRECT exchange queues switch (t) { case "ai_interactive": return "asf.ai.interactive"; case "ai_background": return "asf.ai.background"; case "indexing": return "asf.indexing"; case "code_analysis": return "asf.code_analysis"; case "testing": return "asf.testing"; case "devops": return "asf.devops"; case "notification": return "asf.notifications"; default: return `asf.${t ?? "custom"}`; } } export default function JobsPage() { const queryClient = useQueryClient(); const [q, setQ] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [typeFilter, setTypeFilter] = useState("all"); const [selectedId, setSelectedId] = useState(null); // Create modal state const [showNew, setShowNew] = useState(false); const [newType, setNewType] = useState("testing"); const [newProjectId, setNewProjectId] = useState(""); const [newPriority, setNewPriority] = useState<"low" | "normal" | "high" | "urgent">("normal"); const [newSequence, setNewSequence] = useState("hello-asf"); const projectsQ = useQuery({ queryKey: ["jobs-projects"], queryFn: () => projects.list({ limit: 100 }), retry: false, refetchOnWindowFocus: false, }); const listQ = useQuery({ queryKey: ["jobs-list", { status: statusFilter, type: typeFilter, q: q.trim() }], queryFn: () => jobs.list({ status: statusFilter !== "all" ? (statusFilter as JobStatus) : undefined, type: typeFilter !== "all" ? (typeFilter as JobType) : undefined, limit: 200, }), retry: false, refetchOnWindowFocus: false, refetchInterval: 3000, // auto-refresh each 3s so running → completed appears without manual reload }); const submitM = useMutation({ mutationFn: async (body: JobCreate) => jobs.submit(body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["jobs-list"] }).catch(() => undefined); setShowNew(false); }, }); const apiResolved = listQ.isFetched && listQ.data !== undefined; const rows: Job[] = apiResolved ? listQ.data! : DEMO_JOBS; const isMock = !apiResolved; const projectNameById = useMemo(() => { const m = new Map(); for (const p of projectsQ.data ?? []) { if (p?.id) m.set(String(p.id), p.name ?? shortId(String(p.id))); } return m; }, [projectsQ.data]); // Default first project for new job dropdown (if any) React.useEffect(() => { if (!newProjectId && projectsQ.data?.length) setNewProjectId(String(projectsQ.data[0].id)); }, [newProjectId, projectsQ.data]); const filtered = useMemo(() => { const ql = q.trim().toLowerCase(); return rows.filter((j) => { if (statusFilter !== "all" && j.status !== statusFilter) return false; if (typeFilter !== "all" && j.type !== typeFilter) return false; if (ql) { const hay = [ String(j.id), String(j.correlation_id ?? ""), String(j.worker ?? ""), String(j.model ?? ""), String(j.error ?? ""), JSON.stringify(j.payload ?? {}), JSON.stringify(j.result ?? {}), ] .join(" ") .toLowerCase(); if (!hay.includes(ql)) return false; } return true; }); }, [rows, statusFilter, typeFilter, q]); const selected = (selectedId !== null ? filtered.find((j) => String(j.id) === String(selectedId)) : null) ?? filtered[0] ?? null; const statuses = Array.from(new Set(rows.map((j) => j.status))); return (

Jobs

Asynchronous ASF work: queued → running → completed / failed. Each job is routed to its dedicated RabbitMQ queue.

{isMock && Demo data}
{/* Stats strip */}
{(["queued", "running", "completed", "failed", "cancelled"] as JobStatus[]).map((s) => { const count = rows.filter((r) => r.status === s).length; const Icon = STATUS_ICON[s]; return ( ); })}
Total {rows.length}
setQ(e.target.value)} />
Job list Auto-refresh each 3s · click a row → {filtered.length === 0 ? (
) : ( {filtered.map((j) => { const active = selected && String(selected.id) === String(j.id); const StatusIcon = STATUS_ICON[j.status] ?? Circle; return ( setSelectedId(String(j.id))} className={cn( "cursor-pointer hover:bg-brand-50/40 transition-colors", active && "bg-brand-50/70", )} > ); })}
ID Type Status Progress Queue Updated
{shortId(String(j.id))} {j.type} {j.status}
{j.progress_percent ?? (j.status === "completed" ? 100 : j.status === "queued" ? 0 : "—")}%
{queueForJobType(j.type)} {fmtDate(j.completed_at ?? j.started_at ?? j.created_at)}
)}
Detail AI Agents {selected ? (
{shortId(String(selected.id))} {selected.type} {selected.status} {selected.priority && {selected.priority}}

{selected.type} job · {queueForJobType(selected.type)}

Project
{selected.project_id ? (projectNameById.get(String(selected.project_id)) ?? shortId(String(selected.project_id))) : "—"}
Queue
{queueForJobType(selected.type)}
Worker
{selected.worker ?? "—"}
Model
{selected.model ?? "—"}
Requested by
{selected.requested_by ? shortId(String(selected.requested_by)) : "—"}
Correlation
{selected.correlation_id ?? "—"}
Created
{fmtDate(selected.created_at)}
Completed
{selected.completed_at ? fmtDate(selected.completed_at) : "—"}
{selected.payload !== undefined && (
payload
                      {JSON.stringify(selected.payload, null, 2)}
                    
)} {selected.result !== undefined && (
result
                      {JSON.stringify(selected.result, null, 2)}
                    
)} {selected.error && (
error
{selected.error}
{selected.error_detail && (
{JSON.stringify(selected.error_detail, null, 2)}
)}
)} {selected.usage && (
usage
{Object.entries(selected.usage).map(([k, v]) => v === undefined ? null : (
{k} {JSON.stringify(v)}
), )}
)}
) : ( )}
{/* New job modal */} {showNew && (
setShowNew(false)}>
e.stopPropagation()} >

Enqueue new job

{ e.preventDefault(); submitM.mutate({ project_id: newProjectId || undefined, type: newType, priority: newPriority, payload: { sequence: newSequence, message: "hello-asf", ts: Date.now() }, }); }} >
setNewSequence(e.target.value)} placeholder="job-e2e-1" />
)}
); }