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",
|
"name": "netfelix-audio-fix",
|
||||||
"version": "2026.04.15.3",
|
"version": "2026.04.15.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -25,43 +25,50 @@ export function PipelinePage() {
|
|||||||
const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
|
const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const loadPipeline = useCallback(async () => {
|
||||||
const [pipelineRes, groupsRes] = await Promise.all([
|
const pipelineRes = await api.get<PipelineData>("/api/review/pipeline");
|
||||||
api.get<PipelineData>("/api/review/pipeline"),
|
|
||||||
api.get<ReviewGroupsResponse>("/api/review/groups?offset=0&limit=25"),
|
|
||||||
]);
|
|
||||||
setData(pipelineRes);
|
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(() => {
|
useEffect(() => {
|
||||||
load();
|
loadAll();
|
||||||
}, [load]);
|
}, [loadAll]);
|
||||||
|
|
||||||
// SSE for live updates. job_update fires on every status change and per-line
|
// 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
|
// stdout flush — coalesce via 1s debounce so the pipeline endpoint doesn't
|
||||||
// (a 500-row review query + counts) would re-run several times per second.
|
// re-run several times per second.
|
||||||
const reloadTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const reloadTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scheduleReload = () => {
|
const schedulePipelineReload = () => {
|
||||||
if (reloadTimer.current) return;
|
if (reloadTimer.current) return;
|
||||||
reloadTimer.current = setTimeout(() => {
|
reloadTimer.current = setTimeout(() => {
|
||||||
reloadTimer.current = null;
|
reloadTimer.current = null;
|
||||||
load();
|
loadPipeline();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
const es = new EventSource("/api/execute/events");
|
const es = new EventSource("/api/execute/events");
|
||||||
es.addEventListener("job_update", (e) => {
|
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 {
|
try {
|
||||||
const upd = JSON.parse((e as MessageEvent).data) as { id: number; status: string };
|
const upd = JSON.parse((e as MessageEvent).data) as { id: number; status: string };
|
||||||
if (upd.status !== "running") setProgress(null);
|
if (upd.status !== "running") setProgress(null);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore malformed events */
|
/* ignore malformed events */
|
||||||
}
|
}
|
||||||
scheduleReload();
|
schedulePipelineReload();
|
||||||
});
|
});
|
||||||
es.addEventListener("job_progress", (e) => {
|
es.addEventListener("job_progress", (e) => {
|
||||||
setProgress(JSON.parse((e as MessageEvent).data));
|
setProgress(JSON.parse((e as MessageEvent).data));
|
||||||
@@ -73,7 +80,7 @@ export function PipelinePage() {
|
|||||||
es.close();
|
es.close();
|
||||||
if (reloadTimer.current) clearTimeout(reloadTimer.current);
|
if (reloadTimer.current) clearTimeout(reloadTimer.current);
|
||||||
};
|
};
|
||||||
}, [load]);
|
}, [loadPipeline]);
|
||||||
|
|
||||||
if (loading || !data || !initialGroups) return <div className="p-6 text-gray-500">Loading pipeline...</div>;
|
if (loading || !data || !initialGroups) return <div className="p-6 text-gray-500">Loading pipeline...</div>;
|
||||||
|
|
||||||
@@ -88,11 +95,11 @@ export function PipelinePage() {
|
|||||||
initialResponse={initialGroups}
|
initialResponse={initialGroups}
|
||||||
totalItems={data.reviewItemsTotal}
|
totalItems={data.reviewItemsTotal}
|
||||||
jellyfinUrl={data.jellyfinUrl}
|
jellyfinUrl={data.jellyfinUrl}
|
||||||
onMutate={load}
|
onMutate={loadAll}
|
||||||
/>
|
/>
|
||||||
<QueueColumn items={data.queued} jellyfinUrl={data.jellyfinUrl} onMutate={load} />
|
<QueueColumn items={data.queued} jellyfinUrl={data.jellyfinUrl} onMutate={loadAll} />
|
||||||
<ProcessingColumn items={data.processing} progress={progress} queueStatus={queueStatus} onMutate={load} />
|
<ProcessingColumn items={data.processing} progress={progress} queueStatus={queueStatus} onMutate={loadAll} />
|
||||||
<DoneColumn items={data.done} onMutate={load} />
|
<DoneColumn items={data.done} onMutate={loadAll} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user