diff --git a/app/api/tasks/[id]/route.ts b/app/api/tasks/[id]/route.ts index acef7d5..0356d6e 100644 --- a/app/api/tasks/[id]/route.ts +++ b/app/api/tasks/[id]/route.ts @@ -1,6 +1,24 @@ import { NextResponse } from "next/server"; -import { updateTask, validateTaskPayload } from "@/lib/tasks"; +import { findTask, updateTask, validateTaskPayload } from "@/lib/tasks"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + return NextResponse.json({ error: "invalid_task_id" }, { status: 400 }); + } + + const task = await findTask(numericId); + if (!task) { + return NextResponse.json({ error: "task_not_found" }, { status: 404 }); + } + + return NextResponse.json(task); +} export async function PATCH( request: Request, diff --git a/app/tasks/[id]/page.tsx b/app/tasks/[id]/page.tsx new file mode 100644 index 0000000..6faa8fc --- /dev/null +++ b/app/tasks/[id]/page.tsx @@ -0,0 +1,336 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; + +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { listFleetAgents } from "@/lib/agents"; +import { findTask, listTaskEvents } from "@/lib/tasks"; +import { formatDateTime } from "@/lib/utils"; +import type { FleetAgent, TaskEvent, TaskRecord } from "@/lib/types"; + +export const dynamic = "force-dynamic"; + +function familyVariant(family: TaskRecord["family"]) { + if (family === "zeroclaw") { + return "success"; + } + if (family === "direct") { + return "warning"; + } + return "default"; +} + +function dispatchVariant(state: TaskRecord["dispatch_state"]) { + return state === "failed" ? "warning" : state === "completed" ? "success" : "secondary"; +} + +function collectDependencyRows(task: TaskRecord) { + const rows = [ + { label: "Template", value: task.template_key }, + { label: "Repository", value: task.repo_slug }, + { label: "Base Branch", value: task.base_branch }, + { label: "Preferred Agent", value: task.preferred_agent }, + { label: "Model Hint", value: task.model_hint }, + { label: "Reasoning Effort", value: task.reasoning_effort }, + ]; + + return rows.filter((row) => row.value); +} + +function collectRequirementRows(task: TaskRecord) { + const rows = [ + { label: "Assignee", value: task.assignee || "Unassigned" }, + { label: "Family", value: task.family || "manual" }, + { label: "Target Host", value: task.target_host || "n/a" }, + { label: "Target Channel", value: task.target_channel || "n/a" }, + { label: "Dispatch Method", value: task.dispatch_method }, + { label: "Dispatch State", value: task.dispatch_state }, + { label: "Priority", value: task.priority }, + { label: "Status", value: task.status }, + ]; + + return rows; +} + +function findAssignedAgent(task: TaskRecord, agents: FleetAgent[]) { + return agents.find( + (agent) => + agent.assignmentKey === task.assignee || + agent.slug === task.assignee || + agent.aliases.includes(task.assignee), + ); +} + +function renderEventTone(event: TaskEvent["event_type"]) { + if (event === "dispatch_failed") { + return "border-amber-400/20 bg-amber-500/5"; + } + if (event === "dispatch_succeeded" || event === "acknowledged") { + return "border-emerald-400/20 bg-emerald-500/5"; + } + return "border-white/10 bg-slate-950/40"; +} + +export default async function TaskDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + notFound(); + } + + const [task, events, agents] = await Promise.all([ + findTask(numericId), + listTaskEvents(numericId, 100), + listFleetAgents(), + ]); + + if (!task) { + notFound(); + } + + const requirementRows = collectRequirementRows(task); + const dependencyRows = collectDependencyRows(task); + const assignedAgent = findAssignedAgent(task, agents); + + return ( +
Latest Result
+{task.result_summary || "No result has been posted yet."}
+Completion
+Execution Detail
++ {task.result_detail || task.last_error || "No detailed execution transcript has been recorded yet."} +
+Latest Failure
+{task.last_error}
+{event.summary}
++ {event.event_type.replace(/_/g, " ")} + {event.state ? ` • ${event.state}` : ""} +
+{formatDateTime(event.created_at)}
+{event.detail}
+ ) : null} +No explicit dependencies or repository context were captured for this task.
+ )} + +Tags
+{assignedAgent.name}
+{assignedAgent.role}
+No configured fleet agent matched this task assignee.
+ )} +