fix approve-up-to: client sends explicit visible plan id list
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m12s
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m12s
The server's old /approve-up-to/:id re-ran its own SQL ORDER BY against
ALL pending plans (no LIMIT) to decide which rows fell 'before' the target.
The pipeline UI uses a different ordering — interleaving movies with
series cards, sorting by confidence tier without a name tiebreaker, and
collapsing every episode of a series into one card. Visible position
therefore did not map to the server's iteration position, and clicking
'Approve up to here' could approve far more (or different) items than
the user expected.
- replace POST /approve-up-to/:id with POST /approve-batch { planIds: [...] }
— server only approves the plans the client lists, idempotent: skips
ids that are no longer pending, were already approved, or are noop
- ReviewColumn now builds visiblePlanIds in actual render order
(each movie's id, then every episode id of each series in series order)
and 'approve up to here' on any card sends slice(0, idx+1) of that list
- works the same for both PipelineCard (movie) and SeriesCard (whole series
through its last episode)
This commit is contained in:
@@ -734,55 +734,46 @@ app.post("/:id/rescan", async (c) => {
|
||||
return c.json(detail);
|
||||
});
|
||||
|
||||
// ─── Pipeline: approve up to here ────────────────────────────────────────────
|
||||
// ─── 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-up-to/:id", (c) => {
|
||||
const targetId = parseId(c.req.param("id"));
|
||||
if (targetId == null) return c.json({ error: "invalid id" }, 400);
|
||||
const db = getDb();
|
||||
|
||||
const target = db.prepare("SELECT id FROM review_plans WHERE id = ?").get(targetId) as { id: number } | undefined;
|
||||
if (!target) return c.json({ error: "Plan not found" }, 404);
|
||||
|
||||
// Get all pending plans sorted by confidence (high first), then name
|
||||
const pendingPlans = db
|
||||
.prepare(`
|
||||
SELECT rp.id
|
||||
FROM review_plans rp
|
||||
JOIN media_items mi ON mi.id = rp.item_id
|
||||
WHERE rp.status = 'pending' AND rp.is_noop = 0
|
||||
ORDER BY
|
||||
CASE rp.confidence WHEN 'high' THEN 0 ELSE 1 END,
|
||||
COALESCE(mi.series_name, mi.name),
|
||||
mi.season_number,
|
||||
mi.episode_number,
|
||||
mi.name
|
||||
`)
|
||||
.all() as { id: number }[];
|
||||
|
||||
// Find the target and approve everything up to and including it
|
||||
const toApprove: number[] = [];
|
||||
for (const plan of pendingPlans) {
|
||||
toApprove.push(plan.id);
|
||||
if (plan.id === targetId) break;
|
||||
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;
|
||||
|
||||
// Batch approve and create jobs
|
||||
// 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 planRow = db.prepare("SELECT item_id, job_type FROM review_plans WHERE id = ?").get(planId) as {
|
||||
item_id: number;
|
||||
job_type: string;
|
||||
};
|
||||
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: toApprove.length });
|
||||
return c.json({ approved });
|
||||
});
|
||||
|
||||
// ─── Pipeline: series language ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user