pipeline: stop wiping Review scroll state on every SSE tick
All checks were successful
Build and Push Docker Image / build (push) Successful in 3m22s
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:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user