review column: 'approve above' on hover, wrap long audio titles
All checks were successful
Build and Push Docker Image / build (push) Successful in 55s

each top-level card now shows a secondary button on hover ('↑ approve
above') that approves every card listed above this one in one
round-trip. uses a new POST /api/review/approve-batch { itemIds } that
ignores non-pending items so stale client state can't 409. series cards
get the same affordance scoped via a named tailwind group so it
doesn't collide with the inner episode cards' own hover state.

fix the horizontal-scroll glitch: long unbreakable audio titles (e.g.
the raw release filename) now line-wrap inside the card via
[overflow-wrap:anywhere] + min-w-0 on the span. previously
break-words was a no-op since there were no whitespace break points
in the release string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 18:11:57 +02:00
parent 1de5b8a89e
commit 47781e04f9
5 changed files with 102 additions and 6 deletions

View File

@@ -575,6 +575,44 @@ app.post("/approve-all", (c) => {
return c.json({ ok: true, count: pending.length });
});
// ─── Batch approve (by item id list) ─────────────────────────────────────────
// Used by the "approve up to here" affordance in the review column. The
// client knows the visible order (movies + series sort-key) and passes in
// the prefix of item ids it wants approved in one round-trip. Items that
// aren't pending (already approved / skipped / done) are silently ignored
// so the endpoint is idempotent against stale client state.
app.post("/approve-batch", async (c) => {
const db = getDb();
const body = await c.req.json<{ itemIds?: unknown }>().catch(() => ({ itemIds: undefined }));
if (
!Array.isArray(body.itemIds) ||
!body.itemIds.every((v) => typeof v === "number" && Number.isInteger(v) && v > 0)
) {
return c.json({ ok: false, error: "itemIds must be an array of positive integers" }, 400);
}
const ids = body.itemIds as number[];
if (ids.length === 0) return c.json({ ok: true, count: 0 });
const placeholders = ids.map(() => "?").join(",");
const pending = db
.prepare(
`SELECT rp.*, mi.id as item_id FROM review_plans rp JOIN media_items mi ON mi.id = rp.item_id
WHERE rp.status = 'pending' AND rp.is_noop = 0 AND mi.id IN (${placeholders})`,
)
.all(...ids) as (ReviewPlan & { item_id: number })[];
let count = 0;
for (const plan of pending) {
db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(plan.id);
const { item, streams, decisions } = loadItemDetail(db, plan.item_id);
if (item) {
enqueueAudioJob(db, plan.item_id, buildCommand(item, streams, decisions));
count++;
}
}
return c.json({ ok: true, count });
});
// ─── Auto-approve high-confidence ────────────────────────────────────────────
// Approves every pending plan whose original language came from an authoritative
// source (radarr/sonarr). Anything with low confidence keeps needing a human.