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:
813
docs/superpowers/plans/2026-04-15-drop-verify-and-jobs-page.md
Normal file
813
docs/superpowers/plans/2026-04-15-drop-verify-and-jobs-page.md
Normal 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 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<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 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
|
||||
<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 89–97, delete the `Start queue` `<Button>` and the `startQueue` callback (around lines 34–37). 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 67–72, 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 338–348 (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`.
|
||||
Reference in New Issue
Block a user