drop audio list tab, move per-item actions onto pipeline cards
All checks were successful
Build and Push Docker Image / build (push) Successful in 39s

The pipeline tab fully replaces the audio list: same items, better
workflow. What the old list contributed (per-item details + skip/approve)
now lives inline on each pipeline card.

- delete src/routes/review/audio/index.tsx + src/features/review/AudioListPage.tsx
- /review/ now redirects to /pipeline (was /review/audio, which no longer exists)
- AudioDetailPage back link goes to /pipeline
- nav: drop the Audio link
- PipelineCard: three buttons on every card — Details (TanStack Link to
  /review/audio/$id — the detail route stays, it's how you drill in),
  Skip (POST /api/review/:id/skip), Approve (POST /api/review/:id/approve).
  Remove the old 'Approve up to here' button (it was computing against
  frontend ordering we don't want to maintain, and it was broken).
- SeriesCard: drop onApproveUpTo, pass new approve/skip handlers through
  to each expanded episode card
- server: remove now-unused POST /api/review/approve-batch (no callers)
This commit is contained in:
2026-04-13 11:20:57 +02:00
parent d12dd80209
commit e3b241bef3
9 changed files with 70 additions and 580 deletions

View File

@@ -734,48 +734,6 @@ app.post("/:id/rescan", async (c) => {
return c.json(detail);
});
// ─── Pipeline: approve a batch of plan IDs ──────────────────────────────────
//
// The pipeline UI groups episodes into series cards and interleaves them
// with movies in a frontend-specific order, so we can't reconstruct
// "up to here" by re-running an ORDER BY on the server. The client knows
// exactly which plans are visually before (and including) the clicked card
// and sends them as an explicit list.
app.post("/approve-batch", async (c) => {
const body = await c.req.json<{ planIds: unknown }>().catch(() => ({ planIds: null }));
if (!Array.isArray(body.planIds) || !body.planIds.every((id) => typeof id === "number" && id > 0)) {
return c.json({ error: "planIds must be an array of positive integers" }, 400);
}
const planIds = body.planIds as number[];
if (planIds.length === 0) return c.json({ approved: 0 });
const db = getDb();
const toApprove = planIds;
// Only approve plans that are still pending and not noop. Skip silently
// if a plan was already approved/skipped or doesn't exist — keeps batch
// idempotent under concurrent edits.
let approved = 0;
for (const planId of toApprove) {
const planRow = db
.prepare(
"SELECT id, item_id, status, is_noop, job_type FROM review_plans WHERE id = ? AND status = 'pending' AND is_noop = 0",
)
.get(planId) as { id: number; item_id: number; job_type: string } | undefined;
if (!planRow) continue;
db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(planId);
const detail = loadItemDetail(db, planRow.item_id);
if (detail.item && detail.command) {
db
.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, ?, 'pending')")
.run(planRow.item_id, detail.command, planRow.job_type);
approved++;
}
}
return c.json({ approved });
});
// ─── Pipeline: series language ───────────────────────────────────────────────
app.patch("/series/:seriesKey/language", async (c) => {