diff --git a/docs/superpowers/plans/2026-04-15-drop-verify-and-jobs-page.md b/docs/superpowers/plans/2026-04-15-drop-verify-and-jobs-page.md new file mode 100644 index 0000000..935d455 --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-drop-verify-and-jobs-page.md @@ -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 223–272. + +First, trim the block comment immediately above the `db.prepare(...)` call. Delete the paragraph that starts `` `verified` tracks whether we have independent confirmation... `` (lines 232–238 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 28–98 (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 176–186). + +- [ ] **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 357–389). + +- [ ] **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, 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 44–56). This client-side type doesn't currently include `verified` — confirm by reading lines 44–56. 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 +Jobs +``` + +- [ ] **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 ( + + {items.map((item) => ( +
+ + {item.name} + +
+ {item.status} +
+ +
+
+ ))} + {items.length === 0 &&

No completed items

} + + ); +} +``` + +- [ ] **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 ( + +
+ {items.map((item) => ( + unapprove(item.item_id)} /> + ))} + {items.length === 0 &&

Queue empty

} +
+
+ ); +} +``` + +- [ ] **Step 2: Remove `Start queue` button from PipelinePage header** + +Open `src/features/pipeline/PipelinePage.tsx`. In the header JSX around line 89–97, delete the `Start queue` ` + {liveOutput && ( + + )} + {liveStatus === "pending" && ( + <> + + + + )} + {liveStatus === "running" && ( + + )} +
+ {liveStatus === "running" && progress && progress.total > 0 && ( +
+
+
+ )} + {showCmd && ( +
+ {job.command} +
+ )} + {showLog && liveOutput && ( +
+ {liveOutput} +
+ )} +
+ ); +} +``` + +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 338–348 (the `{command && (...)}` section with the textarea) and add immediately below it: + +```tsx +{data.job && } +``` + +- [ ] **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`.