Files
netfelix-audio-fix/docs/superpowers/plans/2026-04-15-drop-verify-and-jobs-page.md
2026-04-15 06:49:34 +02:00

29 KiB
Raw Blame History

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:

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:

	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:

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:

SELECT j.*, mi.name, mi.series_name, mi.type,
    rp.job_type, rp.apple_compat, rp.verified

to:

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:

db.prepare("UPDATE review_plans SET status = 'pending', verified = 0, reviewed_at = NULL WHERE id = ?").run(plan.id);

to:

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:

rg "verified" server/

and remove each occurrence.

  • Step 9: Commit
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:

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:

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
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:

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:

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:

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
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:

	// 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:

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:

import type { Job, MediaItem, MediaStream, ReviewPlan, StreamDecision } from "~/shared/lib/types";
  • Step 4: Run lint

Run: bun run lint Expected: PASS.

  • Step 5: Commit
git add src/shared/lib/types.ts src/features/review/AudioDetailPage.tsx
git commit -m "client types: drop verified, add job on DetailData"

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

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:

<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:

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
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:

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
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:

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:

<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:

import { Button } from "~/shared/components/ui/button";
  • Step 3: Run lint

Run: bun run lint Expected: PASS (no unused-import errors).

  • Step 4: Commit
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:

// 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
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:

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:

{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
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:

"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
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.