plan: drop verify/checkmarks, merge jobs view into item details

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 06:49:34 +02:00
parent 9d7b76339b
commit 81b2990dca

View File

@@ -0,0 +1,813 @@
# Drop verify/checkmarks, merge jobs view into item details — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rip out the post-job verification path entirely (DB column, SSE event, handoff function), delete the standalone `/execute` page, and surface per-item job info (status, command, log, run/cancel) on the item details page. Batch queue controls move into the Pipeline column headers.
**Architecture:** Rescan becomes the single source of truth for "is this file still done?" — the verified flag and the Jellyfin refresh handoff are no longer needed. The Jobs page disappears; its per-item info is enriched onto `GET /api/review/:id` and rendered inline on the details page. Batch controls (`Run all`, `Clear queue`, `Clear`) sit in the existing `ColumnShell` `actions` slot.
**Tech Stack:** Bun + Hono (server), React 19 + TanStack Router (client), bun:sqlite.
---
## File Structure
**Backend:**
- `server/db/index.ts` — add `DROP COLUMN verified` migration
- `server/db/schema.ts` — remove `verified` from `review_plans` DDL
- `server/services/rescan.ts` — remove `verified` from INSERT/UPDATE logic
- `server/api/review.ts` — drop `rp.verified` from pipeline SELECT, drop `verified = 0` from unapprove, enrich `loadItemDetail` with latest job
- `server/api/execute.ts` — delete `handOffToJellyfin`, `emitPlanUpdate`, `POST /verify-unverified`, `GET /` (list endpoint) and the plan_update emissions from job lifecycle
- `server/types.ts` — drop `verified` from `ReviewPlan`, add job shape on detail response
- `server/services/__tests__/webhook.test.ts` — delete the `webhook_verified flag` describe block
**Frontend:**
- `src/routes/execute.tsx` — delete (file)
- `src/features/execute/ExecutePage.tsx` — delete (file)
- `src/shared/lib/types.ts` — drop `verified` from `PipelineJobItem` and `ReviewPlan`; add `job` field to `DetailData`
- `src/routes/__root.tsx` — remove `Jobs` nav link
- `src/features/pipeline/PipelinePage.tsx` — remove `plan_update` SSE listener, remove the `Start queue` header button
- `src/features/pipeline/DoneColumn.tsx` — remove verify button, `unverifiedCount`, `verified`/`✓✓` glyph
- `src/features/pipeline/QueueColumn.tsx` — add `Run all` + `Clear queue` actions
- `src/features/review/AudioDetailPage.tsx` — add JobSection
**Plan ordering rationale:** Backend DB migration first (Task 1) so the schema drift from the `verified` column doesn't break tests. Then server logic deletions (Task 2). Then server additions (Task 3). Frontend follows in dependency order: types → route deletion → column updates → details enrichment.
---
## Task 1: Drop `verified` column from DB + backend references
**Files:**
- Modify: `server/db/index.ts` (migration block)
- Modify: `server/db/schema.ts:77`
- Modify: `server/services/rescan.ts:233-270`
- Modify: `server/api/review.ts:330, 773`
- Modify: `server/types.ts` (ReviewPlan interface)
- Modify: `server/services/__tests__/webhook.test.ts:186-240`
- [ ] **Step 1: Add idempotent migration in `server/db/index.ts`**
Locate the existing block of `alter(...)` calls (around line 76 where `webhook_verified` was added and renamed). Append a new call at the end so it runs on the next startup for existing databases:
```ts
alter("ALTER TABLE review_plans DROP COLUMN verified");
```
The `alter()` helper wraps each statement in try/catch, so on a fresh DB (where `verified` never existed because we'll remove it from schema.ts) the DROP is a no-op, and on an existing DB it removes the column once.
- [ ] **Step 2: Remove `verified` from schema.ts**
Open `server/db/schema.ts`. Find the `review_plans` CREATE TABLE block (around line 77) and delete the line:
```ts
verified INTEGER NOT NULL DEFAULT 0,
```
- [ ] **Step 3: Remove `verified` from rescan.ts INSERT/UPDATE**
Open `server/services/rescan.ts` around lines 223272.
First, trim the block comment immediately above the `db.prepare(...)` call. Delete the paragraph that starts `` `verified` tracks whether we have independent confirmation... `` (lines 232238 in the current file). Keep the "Status transition rules" paragraph above it.
Then replace the INSERT/ON CONFLICT statement and its `.run(...)` args with the variant that has no `verified` column:
```ts
db
.prepare(`
INSERT INTO review_plans (item_id, status, is_noop, confidence, apple_compat, job_type, notes)
VALUES (?, 'pending', ?, ?, ?, ?, ?)
ON CONFLICT(item_id) DO UPDATE SET
status = CASE
WHEN excluded.is_noop = 1 THEN 'done'
WHEN review_plans.status = 'done' AND ? = 'webhook' THEN 'pending'
WHEN review_plans.status = 'done' THEN 'done'
WHEN review_plans.status = 'error' THEN 'pending'
ELSE review_plans.status
END,
is_noop = excluded.is_noop,
confidence = excluded.confidence,
apple_compat = excluded.apple_compat,
job_type = excluded.job_type,
notes = excluded.notes
`)
.run(
itemId,
analysis.is_noop ? 1 : 0,
confidence,
analysis.apple_compat,
analysis.job_type,
analysis.notes.length > 0 ? analysis.notes.join("\n") : null,
source, // for the CASE WHEN ? = 'webhook' branch
);
```
Note: the parameter list drops the two `verified`-related bindings (was `analysis.is_noop ? 1 : 0` passed twice for the verified CASE, and the source passed twice). Verify by counting `?` placeholders in the SQL (7) matches `.run()` argument count (7).
- [ ] **Step 4: Remove `rp.verified` from the pipeline SELECT**
Open `server/api/review.ts` around line 330. In the `done` query, change:
```ts
SELECT j.*, mi.name, mi.series_name, mi.type,
rp.job_type, rp.apple_compat, rp.verified
```
to:
```ts
SELECT j.*, mi.name, mi.series_name, mi.type,
rp.job_type, rp.apple_compat
```
- [ ] **Step 5: Remove `verified = 0` from unapprove UPDATE**
Open `server/api/review.ts` around line 773. Change:
```ts
db.prepare("UPDATE review_plans SET status = 'pending', verified = 0, reviewed_at = NULL WHERE id = ?").run(plan.id);
```
to:
```ts
db.prepare("UPDATE review_plans SET status = 'pending', reviewed_at = NULL WHERE id = ?").run(plan.id);
```
- [ ] **Step 6: Remove `verified` from the `ReviewPlan` type**
Open `server/types.ts`. Find the `ReviewPlan` interface and delete the `verified: number;` line (around line 68).
- [ ] **Step 7: Delete the webhook_verified test block**
Open `server/services/__tests__/webhook.test.ts`. Find the block `describe("processWebhookEvent — webhook_verified flag", …)` starting at line 186 and delete through its closing `});` at line 240.
- [ ] **Step 8: Run the test suite**
Run: `bun test`
Expected: PASS with the full suite green (the remaining webhook tests, analyzer tests, etc.).
If any test fails with "no such column: verified", grep for remaining references:
```bash
rg "verified" server/
```
and remove each occurrence.
- [ ] **Step 9: Commit**
```bash
git add server/db/index.ts server/db/schema.ts server/services/rescan.ts server/api/review.ts server/types.ts server/services/__tests__/webhook.test.ts
git commit -m "drop review_plans.verified column and all its references"
```
---
## Task 2: Delete verification path in `server/api/execute.ts`
**Files:**
- Modify: `server/api/execute.ts`
- [ ] **Step 1: Delete `handOffToJellyfin` function**
Open `server/api/execute.ts`. Delete the entire `handOffToJellyfin` function and its JSDoc, spanning roughly lines 2898 (from the block comment starting `/**\n * Post-job verification…` through the closing brace of the function).
Also delete the now-unused imports at the top that only this function used:
```ts
import { getItem, refreshItem } from "../services/jellyfin";
import { loadRadarrLibrary, radarrUsable } from "../services/radarr";
import { loadSonarrLibrary, sonarrUsable } from "../services/sonarr";
import { upsertJellyfinItem } from "../services/rescan";
import type { RescanConfig } from "../services/rescan";
import { getAllConfig } from "../db";
```
(Only delete the ones not used elsewhere in the file. Run the TS check in Step 6 to catch any that are still needed.)
- [ ] **Step 2: Delete `emitPlanUpdate` function**
In the same file, find `emitPlanUpdate` (around line 183) and delete the function and its block comment (lines 176186).
- [ ] **Step 3: Remove calls to `handOffToJellyfin` from the job lifecycle**
There are two call sites at lines 492 and 609, each wrapped in `.catch(...)`. Find both instances that look like:
```ts
handOffToJellyfin(job.item_id).catch((err) =>
logError(`handOffToJellyfin for item ${job.item_id} failed:`, err),
);
```
Delete both blocks entirely.
- [ ] **Step 4: Delete the `/verify-unverified` endpoint**
In the same file, find and delete the whole block starting with the comment `// ─── Verify all unverified done plans ───` and the `app.post("/verify-unverified", …)` handler below it (approximately lines 357389).
- [ ] **Step 5: Delete the `GET /` list endpoint**
Find the handler mounted at `app.get("/", (c) => { ... })` that returns the filtered jobs list (the one used by the Execute page). Delete the whole block including its preceding comment.
To locate: it reads `filter` from `c.req.query("filter")`, runs a SELECT joining `jobs` with `media_items`, and returns `{ jobs, filter, totalCounts }`.
- [ ] **Step 6: Run TypeScript compile**
Run: `bun --bun tsc --noEmit --project tsconfig.server.json`
Expected: PASS with no unused import warnings.
If the compiler complains about unused imports, remove them.
- [ ] **Step 7: Run lint**
Run: `bun run lint`
Expected: PASS.
- [ ] **Step 8: Commit**
```bash
git add server/api/execute.ts
git commit -m "rip out jellyfin handoff verification path and verify-unverified endpoint"
```
---
## Task 3: Enrich `loadItemDetail` with the latest job
**Files:**
- Modify: `server/api/review.ts:111-126`
- Modify: `server/types.ts` (add exported `DetailJob` shape or similar if helpful)
- [ ] **Step 1: Add latest-job query to `loadItemDetail`**
Open `server/api/review.ts` around line 111. Replace the body with the job enrichment:
```ts
function loadItemDetail(db: ReturnType<typeof getDb>, itemId: number) {
const item = db.prepare("SELECT * FROM media_items WHERE id = ?").get(itemId) as MediaItem | undefined;
if (!item) return { item: null, streams: [], plan: null, decisions: [], command: null, job: null };
const streams = db
.prepare("SELECT * FROM media_streams WHERE item_id = ? ORDER BY stream_index")
.all(itemId) as MediaStream[];
const plan = db.prepare("SELECT * FROM review_plans WHERE item_id = ?").get(itemId) as ReviewPlan | undefined | null;
const decisions = plan
? (db.prepare("SELECT * FROM stream_decisions WHERE plan_id = ?").all(plan.id) as StreamDecision[])
: [];
const command = plan && !plan.is_noop ? buildCommand(item, streams, decisions) : null;
const job = db
.prepare(
`SELECT id, item_id, command, job_type, status, output, exit_code,
created_at, started_at, completed_at
FROM jobs WHERE item_id = ? ORDER BY created_at DESC LIMIT 1`,
)
.get(itemId) as Job | undefined;
return { item, streams, plan: plan ?? null, decisions, command, job: job ?? null };
}
```
Add the `Job` type import at the top of the file if not already imported:
```ts
import type { Job, MediaItem, MediaStream, ReviewPlan, StreamDecision } from "../types";
```
- [ ] **Step 2: Run the test suite**
Run: `bun test`
Expected: PASS.
- [ ] **Step 3: Smoke-test the endpoint manually**
Start the server: `bun run dev:server`
In another terminal:
```bash
curl -s http://localhost:3000/api/review/1 | jq '.job'
```
Expected: either `null` (no jobs ever) or a job object with the fields above.
Kill the dev server with Ctrl-C after confirming.
- [ ] **Step 4: Commit**
```bash
git add server/api/review.ts
git commit -m "enrich GET /api/review/:id with the latest job row"
```
---
## Task 4: Update client types (drop verified, add job on DetailData)
**Files:**
- Modify: `src/shared/lib/types.ts`
- Modify: `src/features/review/AudioDetailPage.tsx:13-19` (local `DetailData` interface)
- [ ] **Step 1: Remove `verified` from `ReviewPlan`**
In `src/shared/lib/types.ts`, find the `ReviewPlan` interface (lines 4456). This client-side type doesn't currently include `verified` — confirm by reading lines 4456. If it does, delete the line. If not, skip this sub-step.
- [ ] **Step 2: Remove `verified` from `PipelineJobItem`**
In the same file around line 161, delete:
```ts
// 1 when an independent post-hoc check confirms the on-disk file matches
// the plan (ffprobe after a job, or is_noop=1 on the very first scan).
// Renders as the second checkmark in the Done column.
verified?: number;
```
- [ ] **Step 3: Update `DetailData` in `AudioDetailPage.tsx`**
Open `src/features/review/AudioDetailPage.tsx` at line 13 and replace the interface with:
```ts
interface DetailData {
item: MediaItem;
streams: MediaStream[];
plan: ReviewPlan | null;
decisions: StreamDecision[];
command: string | null;
job: Job | null;
}
```
Add `Job` to the imports at line 9:
```ts
import type { Job, MediaItem, MediaStream, ReviewPlan, StreamDecision } from "~/shared/lib/types";
```
- [ ] **Step 4: Run lint**
Run: `bun run lint`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/shared/lib/types.ts src/features/review/AudioDetailPage.tsx
git commit -m "client types: drop verified, add job on DetailData"
```
---
## Task 5: Delete the Execute page, route, and nav link
**Files:**
- Delete: `src/features/execute/ExecutePage.tsx`
- Delete: `src/routes/execute.tsx`
- Modify: `src/routes/__root.tsx:72`
- Delete (if empty after file removal): `src/features/execute/`
- [ ] **Step 1: Delete the files**
```bash
rm src/features/execute/ExecutePage.tsx src/routes/execute.tsx
rmdir src/features/execute 2>/dev/null || true
```
- [ ] **Step 2: Remove the `Jobs` nav link**
Open `src/routes/__root.tsx` at line 72 and delete:
```tsx
<NavLink to="/execute">Jobs</NavLink>
```
- [ ] **Step 3: Regenerate the TanStack Router tree**
The router typegen runs in dev. Start the dev client briefly to regenerate `src/routeTree.gen.ts`:
Run: `bun run dev:client &`
Wait 3 seconds for Vite to finish the initial build and regenerate the tree, then kill it:
```bash
sleep 3 && kill %1
```
Alternatively, if TSR has a CLI: `bunx @tanstack/router-cli generate`. Either works.
- [ ] **Step 4: Run build to confirm no dangling imports**
Run: `bun run build`
Expected: PASS with no errors about missing `/execute` route or missing `ExecutePage` import.
- [ ] **Step 5: Commit**
```bash
git add -A src/
git commit -m "delete /execute page, route, and Jobs nav link"
```
---
## Task 6: Simplify DoneColumn (remove verify button + checkmark glyph)
**Files:**
- Modify: `src/features/pipeline/DoneColumn.tsx`
- [ ] **Step 1: Rewrite DoneColumn with the glyph and verify button removed**
Replace the entire file contents with:
```tsx
import { Link } from "@tanstack/react-router";
import { Badge } from "~/shared/components/ui/badge";
import { api } from "~/shared/lib/api";
import type { PipelineJobItem } from "~/shared/lib/types";
import { ColumnShell } from "./ColumnShell";
interface DoneColumnProps {
items: PipelineJobItem[];
onMutate: () => void;
}
export function DoneColumn({ items, onMutate }: DoneColumnProps) {
const clear = async () => {
await api.post("/api/execute/clear-completed");
onMutate();
};
const reopen = async (itemId: number) => {
await api.post(`/api/review/${itemId}/reopen`);
onMutate();
};
const actions = items.length > 0 ? [{ label: "Clear", onClick: clear }] : undefined;
return (
<ColumnShell title="Done" count={items.length} actions={actions}>
{items.map((item) => (
<div key={item.id} className="group rounded border bg-white p-2">
<Link
to="/review/audio/$id"
params={{ id: String(item.item_id) }}
className="text-xs font-medium truncate block hover:text-blue-600 hover:underline"
>
{item.name}
</Link>
<div className="flex items-center gap-1.5 mt-0.5">
<Badge variant={item.status === "done" ? "done" : "error"}>{item.status}</Badge>
<div className="flex-1" />
<button
type="button"
onClick={() => reopen(item.item_id)}
title="Send this item back to the Review column to redecide and re-queue"
className="text-[0.68rem] px-1.5 py-0.5 rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
>
← Back to review
</button>
</div>
</div>
))}
{items.length === 0 && <p className="text-sm text-gray-400 text-center py-8">No completed items</p>}
</ColumnShell>
);
}
```
- [ ] **Step 2: Run lint**
Run: `bun run lint`
Expected: PASS.
- [ ] **Step 3: Commit**
```bash
git add src/features/pipeline/DoneColumn.tsx
git commit -m "done column: drop checkmark glyph and verify-unverified button"
```
---
## Task 7: Add batch controls to QueueColumn header, remove Start queue from Pipeline header
**Files:**
- Modify: `src/features/pipeline/QueueColumn.tsx`
- Modify: `src/features/pipeline/PipelinePage.tsx`
- [ ] **Step 1: Update QueueColumn to expose `Run all` + `Clear queue`**
Replace the entire file with:
```tsx
import { api } from "~/shared/lib/api";
import type { PipelineJobItem } from "~/shared/lib/types";
import { ColumnShell } from "./ColumnShell";
import { PipelineCard } from "./PipelineCard";
interface QueueColumnProps {
items: PipelineJobItem[];
jellyfinUrl: string;
onMutate: () => void;
}
export function QueueColumn({ items, jellyfinUrl, onMutate }: QueueColumnProps) {
const runAll = async () => {
await api.post("/api/execute/start");
onMutate();
};
const clear = async () => {
if (!confirm(`Cancel all ${items.length} pending jobs?`)) return;
await api.post("/api/execute/clear");
onMutate();
};
const unapprove = async (itemId: number) => {
await api.post(`/api/review/${itemId}/unapprove`);
onMutate();
};
const actions =
items.length > 0
? [
{ label: "Run all", onClick: runAll, primary: true },
{ label: "Clear", onClick: clear },
]
: undefined;
return (
<ColumnShell title="Queued" count={items.length} actions={actions}>
<div className="space-y-2">
{items.map((item) => (
<PipelineCard key={item.id} item={item} jellyfinUrl={jellyfinUrl} onUnapprove={() => unapprove(item.item_id)} />
))}
{items.length === 0 && <p className="text-sm text-gray-400 text-center py-8">Queue empty</p>}
</div>
</ColumnShell>
);
}
```
- [ ] **Step 2: Remove `Start queue` button from PipelinePage header**
Open `src/features/pipeline/PipelinePage.tsx`. In the header JSX around line 8997, delete the `Start queue` `<Button>` and the `startQueue` callback (around lines 3437). The header should become:
```tsx
<div className="flex items-center justify-between px-6 py-3 border-b shrink-0">
<h1 className="text-lg font-semibold">Pipeline</h1>
<span className="text-sm text-gray-500">{data.doneCount} files in desired state</span>
</div>
```
Also remove the `Button` import at the top of the file if it's no longer used:
```tsx
import { Button } from "~/shared/components/ui/button";
```
- [ ] **Step 3: Run lint**
Run: `bun run lint`
Expected: PASS (no unused-import errors).
- [ ] **Step 4: Commit**
```bash
git add src/features/pipeline/QueueColumn.tsx src/features/pipeline/PipelinePage.tsx
git commit -m "pipeline: batch controls move to queued column header"
```
---
## Task 8: Remove `plan_update` SSE listener
**Files:**
- Modify: `src/features/pipeline/PipelinePage.tsx`
- [ ] **Step 1: Delete the plan_update listener**
In `src/features/pipeline/PipelinePage.tsx` around lines 6772, delete:
```tsx
// plan_update lands ~15s after a job finishes — the post-job jellyfin
// verification writes verified=1 (or flips the plan back to pending).
// Without refreshing here the Done column would never promote ✓ to ✓✓.
es.addEventListener("plan_update", () => {
scheduleReload();
});
```
The other listeners (`job_update`, `job_progress`, `queue_status`) stay untouched.
- [ ] **Step 2: Run lint**
Run: `bun run lint`
Expected: PASS.
- [ ] **Step 3: Commit**
```bash
git add src/features/pipeline/PipelinePage.tsx
git commit -m "pipeline: remove plan_update SSE listener (feature gone)"
```
---
## Task 9: Add JobSection to AudioDetailPage
**Files:**
- Modify: `src/features/review/AudioDetailPage.tsx`
- [ ] **Step 1: Add a `JobSection` component in the same file**
Near the bottom of `src/features/review/AudioDetailPage.tsx`, after the `TitleInput` component and before the `AudioDetailPage` export, add:
```tsx
interface JobSectionProps {
itemId: number;
job: Job;
onMutate: () => void;
}
function JobSection({ itemId, job, onMutate }: JobSectionProps) {
const [showCmd, setShowCmd] = useState(false);
const [showLog, setShowLog] = useState(job.status === "error");
const [liveStatus, setLiveStatus] = useState(job.status);
const [liveOutput, setLiveOutput] = useState(job.output ?? "");
const [progress, setProgress] = useState<{ seconds: number; total: number } | null>(null);
// Keep local state in sync when parent fetches fresh data
useEffect(() => {
setLiveStatus(job.status);
setLiveOutput(job.output ?? "");
}, [job.status, job.output, job.id]);
// Subscribe to SSE for live updates on this specific job id
useEffect(() => {
const es = new EventSource("/api/execute/events");
es.addEventListener("job_update", (e) => {
const d = JSON.parse((e as MessageEvent).data) as { id: number; status: string; output?: string };
if (d.id !== job.id) return;
setLiveStatus(d.status as Job["status"]);
if (d.output !== undefined) setLiveOutput(d.output);
if (d.status === "done" || d.status === "error") onMutate();
});
es.addEventListener("job_progress", (e) => {
const d = JSON.parse((e as MessageEvent).data) as { id: number; seconds: number; total: number };
if (d.id !== job.id) return;
setProgress({ seconds: d.seconds, total: d.total });
});
return () => es.close();
}, [job.id, onMutate]);
const runJob = async () => {
await api.post(`/api/execute/job/${job.id}/run`);
onMutate();
};
const cancelJob = async () => {
await api.post(`/api/execute/job/${job.id}/cancel`);
onMutate();
};
const stopJob = async () => {
await api.post("/api/execute/stop");
onMutate();
};
const typeLabel = job.job_type === "transcode" ? "Audio Transcode" : "Audio Remux";
const exitBadge = job.exit_code != null && job.exit_code !== 0 ? job.exit_code : null;
return (
<div className="mt-6 pt-4 border-t border-gray-200">
<div className="text-gray-400 text-[0.75rem] uppercase tracking-[0.05em] mb-2">Job</div>
<div className="flex items-center gap-2 flex-wrap mb-3">
<Badge variant={liveStatus}>{liveStatus}</Badge>
<Badge variant={job.job_type === "transcode" ? "manual" : "noop"}>{typeLabel}</Badge>
{exitBadge != null && <Badge variant="error">exit {exitBadge}</Badge>}
{job.started_at && (
<span className="text-gray-500 text-[0.72rem]">started {job.started_at}</span>
)}
{job.completed_at && (
<span className="text-gray-500 text-[0.72rem]">completed {job.completed_at}</span>
)}
<div className="flex-1" />
<Button size="sm" variant="secondary" onClick={() => setShowCmd((v) => !v)}>
Cmd
</Button>
{liveOutput && (
<Button size="sm" variant="secondary" onClick={() => setShowLog((v) => !v)}>
Log
</Button>
)}
{liveStatus === "pending" && (
<>
<Button size="sm" onClick={runJob}>
▶ Run
</Button>
<Button size="sm" variant="secondary" onClick={cancelJob}>
✕ Cancel
</Button>
</>
)}
{liveStatus === "running" && (
<Button size="sm" variant="secondary" onClick={stopJob}>
✕ Stop
</Button>
)}
</div>
{liveStatus === "running" && progress && progress.total > 0 && (
<div className="h-1.5 bg-gray-200 rounded mb-3 overflow-hidden">
<div
className="h-full bg-blue-500 transition-[width] duration-500"
style={{ width: `${Math.min(100, (progress.seconds / progress.total) * 100).toFixed(1)}%` }}
/>
</div>
)}
{showCmd && (
<div className="font-mono text-[0.74rem] bg-gray-50 text-gray-700 px-3 py-2 rounded max-h-[120px] overflow-y-auto whitespace-pre-wrap break-all mb-2">
{job.command}
</div>
)}
{showLog && liveOutput && (
<div className="font-mono text-[0.74rem] bg-[#1a1a1a] text-[#d4d4d4] px-3 py-2 rounded max-h-[260px] overflow-y-auto whitespace-pre-wrap break-all">
{liveOutput}
</div>
)}
</div>
);
}
```
Note: `Badge`'s `variant` prop must accept each of `"pending" | "running" | "done" | "error" | "manual" | "noop"`. Verify by opening `src/shared/components/ui/badge.tsx` — these variants already exist per the Execute page's use. If any are missing, add them there.
- [ ] **Step 2: Render `JobSection` inside `AudioDetailPage`**
In the same file, in the `AudioDetailPage` component's JSX, place the JobSection between the FFmpeg command textarea and the Approve/Skip buttons. Locate the existing block around lines 338348 (the `{command && (...)}` section with the textarea) and add immediately below it:
```tsx
{data.job && <JobSection itemId={item.id} job={data.job} onMutate={load} />}
```
- [ ] **Step 3: Run lint**
Run: `bun run lint`
Expected: PASS.
- [ ] **Step 4: Run the dev server and verify manually**
Run: `bun run dev`
Open `http://localhost:5173`:
- Navigate to an item that has a pending job (approve one from Review, then go to its details page via the Queued card link) → confirm the Job section shows status `pending` and working `▶ Run` / `✕ Cancel` buttons.
- Click `▶ Run` → the status badge flips to `running` and the progress bar appears.
- When the job finishes → status flips to `done` and the Log button becomes available.
- Navigate to a done item → confirm Job section shows status `done`, `Cmd` and `Log` toggles work.
Kill the dev server with Ctrl-C.
- [ ] **Step 5: Commit**
```bash
git add src/features/review/AudioDetailPage.tsx
git commit -m "details: surface job status, command, log, and run/cancel inline"
```
---
## Task 10: Version bump, final build, CalVer commit
**Files:**
- Modify: `package.json` (version field)
- [ ] **Step 1: Bump the CalVer version**
Today is 2026-04-15. Read the current version in `package.json`; if it's already `2026.04.15.N`, increment `N`. Otherwise, set it to `2026.04.15.1`.
Edit `package.json`:
```json
"version": "2026.04.15.1"
```
(Use the next free `.N` suffix if `.1` was already used today.)
- [ ] **Step 2: Run the full build**
Run: `bun run build`
Expected: PASS — Vite produces `dist/` cleanly.
- [ ] **Step 3: Run tests once more**
Run: `bun test`
Expected: PASS.
- [ ] **Step 4: Run lint**
Run: `bun run lint`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add package.json
git commit -m "v2026.04.15.1 — drop verify/checkmarks, merge jobs view into item details"
```
---
## Guided Gates (user-verified after deploy)
- **GG-1:** Done column shows cards with only a `done`/`error` badge — no ✓ or ✓✓ glyph.
- **GG-2:** Clicking a Done item → details page shows Job section below the FFmpeg command box, with `Cmd` and `Log` toggles.
- **GG-3:** Clicking a Queued item → details page shows a pending job with working `▶ Run` and `✕ Cancel`; running it updates the badge live.
- **GG-4:** `/execute` returns 404 in the browser.
- **GG-5:** `Run all` + `Clear` buttons appear in the Queued column header; `Clear` stays in the Done column header; the previous `Start queue` button in the Pipeline page header is gone.
- **GG-6:** `PRAGMA table_info(review_plans);` in the SQLite DB no longer lists `verified`.