pipeline: stop wiping Review scroll state on every SSE tick
All checks were successful
Build and Push Docker Image / build (push) Successful in 3m22s

Splitting the loader: SSE job_update events now only refetch the
pipeline payload (queue/processing/done), not the review groups.
loadAll (pipeline + groups) is still used for first mount and user-
driven mutations (approve/skip) via onMutate.

Before: a running job flushed stdout → job_update SSE → 1s debounced
load() refetched /groups?offset=0&limit=25 → ReviewColumn's
useEffect([initialResponse]) reset groups to page 0, wiping any
pages the user had scrolled through. Lazy load appeared to block
because every second the column snapped back to the top.

v2026.04.15.4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 12:26:04 +02:00
parent 07c98f36f0
commit ab65909e6e
2 changed files with 29 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "netfelix-audio-fix",
"version": "2026.04.15.3",
"version": "2026.04.15.4",
"scripts": {
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
"dev:client": "vite",

View File

@@ -25,43 +25,50 @@ export function PipelinePage() {
const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
const [pipelineRes, groupsRes] = await Promise.all([
api.get<PipelineData>("/api/review/pipeline"),
api.get<ReviewGroupsResponse>("/api/review/groups?offset=0&limit=25"),
]);
const loadPipeline = useCallback(async () => {
const pipelineRes = await api.get<PipelineData>("/api/review/pipeline");
setData(pipelineRes);
setInitialGroups(groupsRes);
setLoading(false);
}, []);
const loadReviewGroups = useCallback(async () => {
const groupsRes = await api.get<ReviewGroupsResponse>("/api/review/groups?offset=0&limit=25");
setInitialGroups(groupsRes);
}, []);
// Full refresh: used on first mount and after user-driven mutations
// (approve/skip). SSE-driven refreshes during a running job call
// loadPipeline only, so the Review column's scroll-loaded pages don't get
// wiped every second by job_update events.
const loadAll = useCallback(async () => {
await Promise.all([loadPipeline(), loadReviewGroups()]);
setLoading(false);
}, [loadPipeline, loadReviewGroups]);
useEffect(() => {
load();
}, [load]);
loadAll();
}, [loadAll]);
// SSE for live updates. job_update fires on every status change and per-line
// stdout flush of the running job — without coalescing, the pipeline endpoint
// (a 500-row review query + counts) would re-run several times per second.
// stdout flush — coalesce via 1s debounce so the pipeline endpoint doesn't
// re-run several times per second.
const reloadTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const scheduleReload = () => {
const schedulePipelineReload = () => {
if (reloadTimer.current) return;
reloadTimer.current = setTimeout(() => {
reloadTimer.current = null;
load();
loadPipeline();
}, 1000);
};
const es = new EventSource("/api/execute/events");
es.addEventListener("job_update", (e) => {
// When a job leaves 'running' (done / error / cancelled), drop any
// stale progress so the bar doesn't linger on the next job's card.
try {
const upd = JSON.parse((e as MessageEvent).data) as { id: number; status: string };
if (upd.status !== "running") setProgress(null);
} catch {
/* ignore malformed events */
}
scheduleReload();
schedulePipelineReload();
});
es.addEventListener("job_progress", (e) => {
setProgress(JSON.parse((e as MessageEvent).data));
@@ -73,7 +80,7 @@ export function PipelinePage() {
es.close();
if (reloadTimer.current) clearTimeout(reloadTimer.current);
};
}, [load]);
}, [loadPipeline]);
if (loading || !data || !initialGroups) return <div className="p-6 text-gray-500">Loading pipeline...</div>;
@@ -88,11 +95,11 @@ export function PipelinePage() {
initialResponse={initialGroups}
totalItems={data.reviewItemsTotal}
jellyfinUrl={data.jellyfinUrl}
onMutate={load}
onMutate={loadAll}
/>
<QueueColumn items={data.queued} jellyfinUrl={data.jellyfinUrl} onMutate={load} />
<ProcessingColumn items={data.processing} progress={progress} queueStatus={queueStatus} onMutate={load} />
<DoneColumn items={data.done} onMutate={load} />
<QueueColumn items={data.queued} jellyfinUrl={data.jellyfinUrl} onMutate={loadAll} />
<ProcessingColumn items={data.processing} progress={progress} queueStatus={queueStatus} onMutate={loadAll} />
<DoneColumn items={data.done} onMutate={loadAll} />
</div>
</div>
);