From ab65909e6e95b9f5f105da27bdd67f58292b1ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Wed, 15 Apr 2026 12:26:04 +0200 Subject: [PATCH] pipeline: stop wiping Review scroll state on every SSE tick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- package.json | 2 +- src/features/pipeline/PipelinePage.tsx | 49 +++++++++++++++----------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index de96fea..4178f38 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/features/pipeline/PipelinePage.tsx b/src/features/pipeline/PipelinePage.tsx index fb905f4..39d8041 100644 --- a/src/features/pipeline/PipelinePage.tsx +++ b/src/features/pipeline/PipelinePage.tsx @@ -25,43 +25,50 @@ export function PipelinePage() { const [queueStatus, setQueueStatus] = useState(null); const [loading, setLoading] = useState(true); - const load = useCallback(async () => { - const [pipelineRes, groupsRes] = await Promise.all([ - api.get("/api/review/pipeline"), - api.get("/api/review/groups?offset=0&limit=25"), - ]); + const loadPipeline = useCallback(async () => { + const pipelineRes = await api.get("/api/review/pipeline"); setData(pipelineRes); - setInitialGroups(groupsRes); - setLoading(false); }, []); + const loadReviewGroups = useCallback(async () => { + const groupsRes = await api.get("/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 | 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
Loading pipeline...
; @@ -88,11 +95,11 @@ export function PipelinePage() { initialResponse={initialGroups} totalItems={data.reviewItemsTotal} jellyfinUrl={data.jellyfinUrl} - onMutate={load} + onMutate={loadAll} /> - - - + + + );