Task 007 add agent job admin runner follow-up
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test:roles": "node tests/role-navigation.test.mjs",
|
||||
"test:ui": "node tests/role-navigation.test.mjs && node tests/article_dashboard.test.mjs && node tests/article_form_validation.test.mjs && node tests/admin_script_versions.model.test.mjs",
|
||||
"test:ui": "node tests/role-navigation.test.mjs && node tests/article_dashboard.test.mjs && node tests/article_form_validation.test.mjs && node tests/admin_script_versions.model.test.mjs && node tests/agent_jobs.model.test.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import ts from "typescript";
|
||||
|
||||
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const sourcePath = path.resolve(testDir, "../src/features/agent-jobs/model.ts");
|
||||
const source = readFileSync(sourcePath, "utf8");
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
});
|
||||
|
||||
const moduleExports = {};
|
||||
new Function("exports", compiled.outputText)(moduleExports);
|
||||
|
||||
const { buildAgentJobRows } = moduleExports;
|
||||
|
||||
const rows = buildAgentJobRows([
|
||||
{
|
||||
id: "11111111-1111-1111-1111-111111111111",
|
||||
article_id: null,
|
||||
parent_job_id: "00000000-0000-0000-0000-000000000000",
|
||||
attempt: 2,
|
||||
job_type: "TEST_CODEX",
|
||||
agent_profile: "fake-codex",
|
||||
status: "SUCCEEDED",
|
||||
workspace_path: "/tmp/job",
|
||||
input_files: [{ path: "inputs/job.json" }],
|
||||
output_files: [{ path: "outputs/result.json" }],
|
||||
error_category: null,
|
||||
error_message: null,
|
||||
stdout: "done",
|
||||
stderr: "",
|
||||
exit_code: 0,
|
||||
duration_ms: 5,
|
||||
queued_at: "2026-05-21T00:00:00Z",
|
||||
started_at: "2026-05-21T00:00:01Z",
|
||||
finished_at: "2026-05-21T00:00:02Z",
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0].status, "SUCCEEDED");
|
||||
assert.equal(rows[0].attempt, 2);
|
||||
assert.equal(rows[0].parentJobId, "00000000-0000-0000-0000-000000000000");
|
||||
assert.equal(rows[0].outputFiles, "outputs/result.json");
|
||||
assert.equal(rows[0].stdout, "done");
|
||||
@@ -36,5 +36,9 @@ assert.equal(
|
||||
adminItems.some((item) => item.id === "script-versions"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
adminItems.some((item) => item.id === "agent-jobs"),
|
||||
true,
|
||||
);
|
||||
|
||||
console.log("role navigation hides Admin-only items for Editors");
|
||||
|
||||
Reference in New Issue
Block a user