Task 007 add agent job admin runner follow-up
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import AdminJobsPage from "@/pages/admin-jobs";
|
||||
|
||||
export default AdminJobsPage;
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AgentJobSummary } from "@pipeline/shared";
|
||||
|
||||
export type AgentJobRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
status: string;
|
||||
attempt: number;
|
||||
parentJobId: string | null;
|
||||
workspacePath: string | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
durationMs: number | null;
|
||||
outputFiles: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export function buildAgentJobRows(jobs: readonly AgentJobSummary[]): AgentJobRow[] {
|
||||
return jobs.map((job) => ({
|
||||
id: job.id,
|
||||
type: job.job_type,
|
||||
status: job.status,
|
||||
attempt: job.attempt,
|
||||
parentJobId: job.parent_job_id ?? null,
|
||||
workspacePath: job.workspace_path ?? null,
|
||||
stdout: job.stdout,
|
||||
stderr: job.stderr,
|
||||
exitCode: job.exit_code ?? null,
|
||||
durationMs: job.duration_ms ?? null,
|
||||
outputFiles: job.output_files.map((file) => file.path).join(", "),
|
||||
error: job.error_category ?? job.error_message ?? "",
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { AgentJobRow } from "./model";
|
||||
|
||||
type AgentJobsTableProps = {
|
||||
rows: readonly AgentJobRow[];
|
||||
};
|
||||
|
||||
export function AgentJobsTable({ rows }: AgentJobsTableProps) {
|
||||
if (rows.length === 0) {
|
||||
return <p className="emptyState">No agent jobs yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job</th>
|
||||
<th>Status</th>
|
||||
<th>Attempt</th>
|
||||
<th>Runtime</th>
|
||||
<th>Workspace</th>
|
||||
<th>Logs</th>
|
||||
<th>Outputs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
<div>{row.type}</div>
|
||||
<small>{row.id}</small>
|
||||
{row.parentJobId ? <div>retry of {row.parentJobId}</div> : null}
|
||||
</td>
|
||||
<td>{row.status}</td>
|
||||
<td>{row.attempt}</td>
|
||||
<td>
|
||||
<div>exit: {row.exitCode ?? "pending"}</div>
|
||||
<div>{row.durationMs ?? 0} ms</div>
|
||||
{row.error ? <div>{row.error}</div> : null}
|
||||
</td>
|
||||
<td>{row.workspacePath ?? "pending"}</td>
|
||||
<td>
|
||||
<pre className="logBlock">{row.stdout || "stdout pending"}</pre>
|
||||
{row.stderr ? <pre className="logBlock">{row.stderr}</pre> : null}
|
||||
</td>
|
||||
<td>{row.outputFiles || "none"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { AgentJobsTable } from "@/features/agent-jobs/ui";
|
||||
import { buildAgentJobRows } from "@/features/agent-jobs/model";
|
||||
import { fetchAgentJobs } from "@/shared/pipeline-api";
|
||||
import { RoleNavigation } from "@/widgets/role-navigation";
|
||||
|
||||
export default async function AdminJobsPage() {
|
||||
const jobs = await fetchAgentJobs();
|
||||
const rows = buildAgentJobRows(jobs.jobs);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<header className="pageHeader">
|
||||
<div>
|
||||
<h1>Agent Jobs</h1>
|
||||
<p>Monitor runner status, logs, outputs, and retry lineage.</p>
|
||||
</div>
|
||||
<Link href="/">Back to dashboard</Link>
|
||||
<span className="roleBadge">Admin demo</span>
|
||||
</header>
|
||||
<RoleNavigation role="ADMIN" />
|
||||
<section className="panel">
|
||||
<AgentJobsTable rows={rows} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import type {
|
||||
ArticleCreateRequest,
|
||||
ArticleCreateResponse,
|
||||
ArticleDetailResponse,
|
||||
AgentJobListResponse,
|
||||
AgentJobResponse,
|
||||
AgentJobTestCodexRequest,
|
||||
ArticleListResponse,
|
||||
CurrentUserResponse,
|
||||
ScriptConfigVersionCreateRequest,
|
||||
@@ -149,6 +152,45 @@ export function fetchCurrentUser(): Promise<CurrentUserResponse> {
|
||||
return apiGet<CurrentUserResponse>("/api/me");
|
||||
}
|
||||
|
||||
export function fetchAgentJobs(
|
||||
userEmail = DEMO_ADMIN_EMAIL,
|
||||
): Promise<AgentJobListResponse> {
|
||||
return apiGet<AgentJobListResponse>("/api/agent-jobs", { userEmail });
|
||||
}
|
||||
|
||||
export function createTestCodexJob(
|
||||
request: AgentJobTestCodexRequest = { fake_result: "valid" },
|
||||
userEmail = DEMO_ADMIN_EMAIL,
|
||||
): Promise<AgentJobResponse> {
|
||||
return apiPost<AgentJobTestCodexRequest, AgentJobResponse>(
|
||||
"/api/agent-jobs/test-codex",
|
||||
request,
|
||||
{ userEmail },
|
||||
);
|
||||
}
|
||||
|
||||
export function retryAgentJob(
|
||||
jobId: string,
|
||||
userEmail = DEMO_ADMIN_EMAIL,
|
||||
): Promise<AgentJobResponse> {
|
||||
return apiPost<unknown, AgentJobResponse>(
|
||||
`/api/agent-jobs/${jobId}/retry`,
|
||||
undefined,
|
||||
{ userEmail },
|
||||
);
|
||||
}
|
||||
|
||||
export function cancelAgentJob(
|
||||
jobId: string,
|
||||
userEmail = DEMO_ADMIN_EMAIL,
|
||||
): Promise<AgentJobResponse> {
|
||||
return apiPost<unknown, AgentJobResponse>(
|
||||
`/api/agent-jobs/${jobId}/cancel`,
|
||||
undefined,
|
||||
{ userEmail },
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchTargetSites(): Promise<TargetSiteConfigResponse[]> {
|
||||
return apiGet<TargetSiteConfigResponse[]>("/api/sites");
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ export const ROLE_NAVIGATION_ITEMS = [
|
||||
href: "/admin/scripts",
|
||||
roles: ["ADMIN"],
|
||||
},
|
||||
{
|
||||
id: "agent-jobs",
|
||||
label: "Agent jobs",
|
||||
href: "/admin/jobs",
|
||||
roles: ["ADMIN"],
|
||||
},
|
||||
] as const satisfies readonly NavigationItem[];
|
||||
|
||||
export function getRoleNavigationItems(role: Role): NavigationItem[] {
|
||||
|
||||
Reference in New Issue
Block a user