Skip to content
42 changes: 40 additions & 2 deletions apps/dsa-web/src/hooks/useDashboardLifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import { analysisApi } from '../api/analysis';
import type { TaskInfo } from '../types/analysis';
import { useTaskStream } from './useTaskStream';

Expand All @@ -23,13 +24,50 @@ export function useDashboardLifecycle({
}: UseDashboardLifecycleOptions): void {
const removalTimeoutsRef = useRef<number[]>([]);

// Sync active tasks from the API to reconcile stale store state.
// This handles the case where tasks completed while the component was unmounted.
const syncActiveTasksFromApi = useCallback(async () => {
try {
const response = await analysisApi.getTasks({ limit: 50 });
const serverTasks = response.tasks ?? [];
const serverActiveIds = new Set<string>();

for (const task of serverTasks) {
if (task.status === 'pending' || task.status === 'processing') {
serverActiveIds.add(task.taskId);
// Ensure task exists in store with latest state
syncTaskCreated(task);
syncTaskUpdated(task);
} else if (task.status === 'completed') {
// Task completed while we were away - remove it from store
removeTask(task.taskId);
} else if (task.status === 'failed') {
removeTask(task.taskId);
}
}

// Remove tasks from store that are no longer in the server response
// (they completed/failed while the component was unmounted)
const { useStockPoolStore } = await import('../stores/stockPoolStore');
const { activeTasks } = useStockPoolStore.getState();
for (const storeTask of activeTasks) {
if (!serverActiveIds.has(storeTask.taskId)) {
removeTask(storeTask.taskId);
Comment on lines +53 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile store only against a complete active-task snapshot

analysisApi.getTasks({ limit: 50 }) returns only the newest tasks regardless of status, so serverActiveIds can be incomplete when many recent completed/failed tasks exist. In that case this loop removes still-running tasks from activeTasks, and removeTask marks them dismissed in stockPoolStore, so later SSE task_progress/task_started updates are ignored and the UI can permanently lose in-flight task progress after channel switches. Restrict this reconciliation to a server response that is guaranteed to include all active tasks (e.g. status filter for pending/processing without truncating them).

Useful? React with 👍 / 👎.

}
}
} catch {
// Silently ignore - SSE will eventually sync state
}
}, [syncTaskCreated, syncTaskUpdated, removeTask]);

useEffect(() => {
if (!enabled) {
return;
}

void loadInitialHistory();
}, [enabled, loadInitialHistory]);
void syncActiveTasksFromApi();
}, [enabled, loadInitialHistory, syncActiveTasksFromApi]);

useEffect(() => {
if (!enabled) {
Expand Down
Loading