Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 65ad76b909588a5d218156f9ccda9026497e950c
parent 2fd9f84421cdc3e18bf453d3f65ceda42eacc7d9
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 02:33:49 -0400

editor: /jobs shows why the boot pass cancelled a job

`JobMeta.cancelReason` (release 9, B4b) was written for every meta the boot
pass closed, and shown nowhere but the job's log. It now travels
meta → `JobListEntry.cancelReason` (only on a `cancelled` meta) →
`fromEntry` → `JobRowView.cancelReason`, and is drawn where the cancelled
pill is: under the pill in the /jobs table's Status cell, beside it on a
card row, as the pill's title on a compact one, and as a full-width
"Cancelled because" cell on the job's page. `data-testid="cancel-reason"`.
No existing label changed.

Tests: listJobs.test.ts +1 (a cancelled meta's reason reaches the entry, a
`done` one's is dropped), jobRows.test.ts +1 (fromEntry carries it; no key
without one); jobs-filters.spec.ts +1 (the row and the page draw it, a plain
cancel draws none).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/jobs/listJobs.test.ts | 37+++++++++++++++++++++++++++++++++++++
Mcommon/jobs/listJobs.ts | 10++++++++++
Mcommon/views/jobRowView.ts | 4++++
Mcommon/views/jobRows.test.ts | 22++++++++++++++++++++++
Mcommon/views/jobRows.ts | 1+
Meditor/app/jobs/[id]/page.tsx | 27+++++++++++++++++++++++++--
Meditor/app/jobs/components/JobRow.tsx | 28++++++++++++++++++++++++++++
Meditor/e2e/jobs-filters.spec.ts | 49+++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 176 insertions(+), 2 deletions(-)

diff --git a/common/jobs/listJobs.test.ts b/common/jobs/listJobs.test.ts @@ -90,6 +90,43 @@ test("listAllJobs before-cursor skips newer entries", async () => { }); }); +// The boot pass (bootQueuedJobs.ts) closes a meta a restart left `queued` as +// `cancelled` with a `cancelReason`; /jobs draws it from the entry. +test("a cancelled meta's cancelReason reaches the entry; any other status drops it", async () => { + await withJobs(async (paths) => { + const write = async (id: string, status: string) => { + await writeFile(path.join(paths.jobsDir, `${id}.log`), "log\n"); + await writeFile( + path.join(paths.jobsDir, `${id}.meta.json`), + JSON.stringify({ + id, + kind: "whisper-all", + queueKey: "q", + status, + queuedAt: BASE, + endedAt: BASE + 1, + cancelReason: "queued before the last restart, stale", + }), + ); + }; + const cancelled = ulid(BASE); + const done = ulid(BASE + 1); + await write(cancelled, "cancelled"); + await write(done, "done"); + const one = await getJobEntry(paths, cancelled); + assert.equal(one?.status, "cancelled"); + assert.equal(one?.cancelReason, "queued before the last restart, stale"); + const page = await listAllJobs(paths); + const byId = new Map(page.entries.map((e) => [e.id, e])); + assert.equal( + byId.get(cancelled)?.cancelReason, + "queued before the last restart, stale", + ); + // A reason beside a status it does not explain is not carried. + assert.equal(byId.get(done)?.cancelReason, undefined); + }); +}); + test("getJobEntry resolves one job and null for unknown ids", async () => { await withJobs(async (paths, seed) => { const id = await seed(0, BASE); diff --git a/common/jobs/listJobs.ts b/common/jobs/listJobs.ts @@ -30,6 +30,11 @@ export type JobListEntry = { // A short phrase naming what this particular job is for, when its kind alone // does not say (a fetch-window job's requester and clip). See jobSpecDetail. detail?: string; + // Why a `cancelled` job was cancelled, when something other than a person + // pressing Cancel decided it — today only the boot pass (bootQueuedJobs.ts), + // e.g. "server restarted; the scheduler re-derives syncs". From the sidecar; + // absent on every other job. + cancelReason?: string; }; export type JobsPage = { @@ -135,6 +140,11 @@ async function buildEntry( inRegistry: false, replayable: Boolean(meta.spec), detail: jobSpecDetail(meta.kind, meta.spec), + // Only on a job that did end `cancelled`: a reason beside any other + // status would explain something that did not happen. + ...(meta.status === "cancelled" && meta.cancelReason + ? { cancelReason: meta.cancelReason } + : {}), logPath, logSize, }; diff --git a/common/views/jobRowView.ts b/common/views/jobRowView.ts @@ -94,6 +94,10 @@ export type JobRowView = { // jobs/jobDetail.ts from the job's own replay spec; absent for every kind // that has nothing to add, so no existing row changes. detail?: string; + // Why a `cancelled` row was cancelled when no person pressed Cancel — the + // boot pass's reason for a job a restart left queued ("server restarted; + // the scheduler re-derives syncs"). History rows only; see listJobs.ts. + cancelReason?: string; // Which adapter built it. Never rendered; tests and the merge read it — // except "runner": an auto-queue lane's in-flight unit (fromInFlight), which // links to a job page only when `inRegistry` says it has one. diff --git a/common/views/jobRows.test.ts b/common/views/jobRows.test.ts @@ -146,6 +146,28 @@ test("fromEntry keeps the history fields, archived included", () => { assert.equal(row.source, "archive"); // An old log with no sidecar has no kind at all; the cell renders "—". assert.equal(fromEntry({ ...e, kind: undefined }).kind, ""); + // No reason on the entry, no reason on the row (not even the key). + assert.equal("cancelReason" in row, false); +}); + +test("fromEntry carries a cancelled job's cancelReason to the row", () => { + const e: JobListEntry = { + id: newJobId(), + kind: "sync", + channelSlug: "teamrcn", + status: "cancelled", + queuedAt: 1, + endedAt: 9, + inRegistry: false, + replayable: true, + logPath: "/x.log", + logSize: 64, + cancelReason: "server restarted; the scheduler re-derives syncs", + }; + assert.equal( + fromEntry(e).cancelReason, + "server restarted; the scheduler re-derives syncs", + ); }); test("orderLiveRows: running, then queued IN QUEUE ORDER, then what just ended", () => { diff --git a/common/views/jobRows.ts b/common/views/jobRows.ts @@ -282,6 +282,7 @@ export function fromEntry(e: JobListEntry): JobRowView { inRegistry: e.inRegistry, replayable: e.replayable, detail: e.detail, + ...(e.cancelReason ? { cancelReason: e.cancelReason } : {}), source: "archive", }; } diff --git a/editor/app/jobs/[id]/page.tsx b/editor/app/jobs/[id]/page.tsx @@ -92,6 +92,16 @@ export default async function JobDetailPage({ label="Exit code" value={typeof job.exitCode === "number" ? String(job.exitCode) : "—"} /> + {/* Why, when no person pressed Cancel: the boot pass's reason for a + job a restart left queued (JobMeta.cancelReason). */} + {job.status === "cancelled" && job.cancelReason && ( + <Cell + label="Cancelled because" + value={job.cancelReason} + className="col-span-2 sm:col-span-4" + testId="cancel-reason" + /> + )} </dl> <p className="text-xs text-muted-foreground font-mono"> log: {path.relative(getPaths().monorepoRoot, job.logPath)} @@ -106,9 +116,22 @@ export default async function JobDetailPage({ ); } -function Cell({ label, value }: { label: string; value: string }) { +function Cell({ + label, + value, + className = "", + testId, +}: { + label: string; + value: string; + className?: string; + testId?: string; +}) { return ( - <div className="rounded border border-border px-3 py-2"> + <div + className={`rounded border border-border px-3 py-2 ${className}`} + data-testid={testId} + > <div className="text-xs uppercase tracking-wide text-muted-foreground"> {label} </div> diff --git a/editor/app/jobs/components/JobRow.tsx b/editor/app/jobs/components/JobRow.tsx @@ -122,6 +122,31 @@ export function statusColor(status: string): string { } } +// WHY A JOB WAS CANCELLED, when no person pressed Cancel: the boot pass's +// reason for a job a restart left queued (JobRowView.cancelReason, from the +// meta). Drawn wherever the `cancelled` pill is, as plain text under or beside +// it; absent on every other row. +function cancelReasonOf(job: JobRowView): string | undefined { + return job.status === "cancelled" ? job.cancelReason : undefined; +} + +function CancelReason({ job, inline }: { job: JobRowView; inline?: boolean }) { + const reason = cancelReasonOf(job); + if (!reason) return null; + return ( + <span + data-testid="cancel-reason" + className={ + inline + ? "text-xs text-muted-foreground" + : "self-start max-w-72 text-xs text-muted-foreground break-words" + } + > + {inline ? `· ${reason}` : reason} + </span> + ); +} + // A row with a page at /jobs/<id>. A runner's in-flight unit that is a task // on its runner's job (a transcription, a digest) is not a registry job, so it // does not link to one; a download unit is, and carries its id. @@ -221,10 +246,12 @@ function JobRowHeading({ ? `uppercase tracking-wide px-1.5 py-0.5 rounded text-[10px] ${statusColor(job.status)}` : `text-xs uppercase tracking-wide px-2 py-0.5 rounded ${statusColor(job.status)}` } + title={cancelReasonOf(job)} > {job.status} </span> )} + {statusPill && !compact && <CancelReason job={job} inline />} <span className={compact ? "shrink-0 font-mono" : "text-xs font-medium"} title={job.kind} @@ -512,6 +539,7 @@ function TableRow({ > {j.status} </span> + <CancelReason job={j} /> {j.stuck && ( <span data-stuck={j.stuck.reason} diff --git a/editor/e2e/jobs-filters.spec.ts b/editor/e2e/jobs-filters.spec.ts @@ -21,6 +21,9 @@ type SeedJob = { startedAt?: number; endedAt?: number; exitCode?: number; + // Written by the boot pass (common/jobs/bootQueuedJobs.ts) on a job a + // restart left queued. + cancelReason?: string; }; async function seedJob(job: SeedJob): Promise<void> { @@ -65,6 +68,52 @@ test("archived jobs keep their metadata from the sidecar", async ({ page }) => { ); }); +// Release 10 (L2): a job the boot pass closed says WHY, on its row and on its +// page — the reason used to be only in the meta and the log's last line. +test("a job the boot pass cancelled shows its reason on /jobs and on its page", async ({ + page, +}) => { + await resetData(); + const reason = "server restarted; the scheduler re-derives syncs"; + await seedJob({ + id: "boot-cancelled-1", + kind: "sync", + status: "cancelled", + channelSlug: "alpha", + queueKey: "platform:youtube", + queuedAt: 1000, + endedAt: 2000, + cancelReason: reason, + }); + // A cancel with no recorded reason (a person pressed Cancel) draws none. + await seedJob({ + id: "plain-cancelled-1", + kind: "sync", + status: "cancelled", + channelSlug: "alpha", + queueKey: "platform:youtube", + queuedAt: 1000, + startedAt: 1000, + endedAt: 2000, + }); + + await page.goto("/jobs"); + const row = rowById(page, "boot-cancelled-1"); + await expect(row).toContainText("cancelled"); + await expect(row.getByTestId("cancel-reason")).toHaveText(reason); + await expect( + rowById(page, "plain-cancelled-1").getByTestId("cancel-reason"), + ).toHaveCount(0); + + await page.goto("/jobs/boot-cancelled-1"); + const cell = page.getByTestId("cancel-reason"); + await expect(cell).toContainText("Cancelled because"); + await expect(cell).toContainText(reason); + await page.goto("/jobs/plain-cancelled-1"); + await expect(page.getByText("Exit code")).toBeVisible(); + await expect(page.getByTestId("cancel-reason")).toHaveCount(0); +}); + test("filters hide refresh-report by default, persist, and reset", async ({ page, }) => {