fix approve-up-to: client sends explicit visible plan id list
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:
2026-04-13 10:16:58 +02:00
parent 4a378eb833
commit 2ada728e50
2 changed files with 46 additions and 47 deletions
+19 -11
View File
@@ -30,11 +30,6 @@ export function ReviewColumn({ items, total, jellyfinUrl, onMutate }: ReviewColu
seriesMap.get(key)!.episodes.push(item);
}
const approveUpTo = async (planId: number) => {
await api.post(`/api/review/approve-up-to/${planId}`);
onMutate();
};
// Interleave movies and series, sorted by confidence (high first)
const allItems = [
...movies.map((m: any) => ({ type: "movie" as const, item: m, sortKey: m.confidence === "high" ? 0 : 1 })),
@@ -45,10 +40,23 @@ export function ReviewColumn({ items, total, jellyfinUrl, onMutate }: ReviewColu
})),
].sort((a, b) => a.sortKey - b.sortKey);
// For "approve up to here" on series, use the last episode's plan ID
const lastPlanId = (series: { episodes: any[] }) => {
const eps = series.episodes;
return eps[eps.length - 1]?.id;
/** All plan IDs the user can see, in visible order — the source of truth for "up to here". */
const visiblePlanIds: number[] = allItems.flatMap((entry) =>
entry.type === "movie" ? [entry.item.id] : entry.item.episodes.map((e: any) => e.id),
);
/** Approve every visible plan from the top through (and including) the given index. */
const approveUpToIndex = async (visibleIndex: number) => {
const planIds = visiblePlanIds.slice(0, visibleIndex + 1);
if (planIds.length === 0) return;
await api.post("/api/review/approve-batch", { planIds });
onMutate();
};
/** Index of the last plan in this entry within the visible list — used as the "up to" boundary. */
const lastVisibleIndex = (entry: (typeof allItems)[number]): number => {
const lastId = entry.type === "movie" ? entry.item.id : entry.item.episodes[entry.item.episodes.length - 1]?.id;
return visiblePlanIds.lastIndexOf(lastId);
};
return (
@@ -69,7 +77,7 @@ export function ReviewColumn({ items, total, jellyfinUrl, onMutate }: ReviewColu
await api.patch(`/api/review/${entry.item.item_id}/language`, { language: lang });
onMutate();
}}
onApproveUpTo={() => approveUpTo(entry.item.id)}
onApproveUpTo={() => approveUpToIndex(lastVisibleIndex(entry))}
/>
);
} else {
@@ -82,7 +90,7 @@ export function ReviewColumn({ items, total, jellyfinUrl, onMutate }: ReviewColu
seriesJellyfinId={entry.item.jellyfinId}
episodes={entry.item.episodes}
onMutate={onMutate}
onApproveUpTo={() => approveUpTo(lastPlanId(entry.item))}
onApproveUpTo={() => approveUpToIndex(lastVisibleIndex(entry))}
/>
);
}