From 6e09888a44b21f06a20808cbb0155e457736da31 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 25 Oct 2026 10:52:34 +0100 Subject: [PATCH] fix(tasks): eliminate duplicate task entries by stabilizing WS listener and deduplicating task state --- frontend/src/App.tsx | 7 +++- frontend/src/features/triage/TasksView.tsx | 47 ++++++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f27f611..212b349 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -208,7 +208,7 @@ export const App: React.FC = () => { } else { disconnectWebSocket(); } - }, [user]); + }, [user?.id]); // Refresh stats on task/doc real-time events useEffect(() => { @@ -253,7 +253,10 @@ export const App: React.FC = () => { target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox' ) => { if (target === 'inbox' && thought.status === 'inbox') { - setDeck((prev) => [thought, ...prev]); + setDeck((prev) => { + if (prev.some((t) => t.id === thought.id)) return prev; + return [thought, ...prev]; + }); setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 })); } else if (target === 'document' && thought.convertedRefId) { setSelectedDocId(thought.convertedRefId); diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index 6d6516f..0f1a057 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef, useMemo } from 'react'; import { Check, Plus, @@ -96,7 +96,16 @@ export const TasksView: React.FC = ({ loadData(); }, [refreshTrigger, selectedListId]); - // Live-Sync WebSocket listener for tasks + const selectedListIdRef = useRef(selectedListId); + selectedListIdRef.current = selectedListId; + + const activeNotesTaskIdRef = useRef(activeNotesTaskId); + activeNotesTaskIdRef.current = activeNotesTaskId; + + const onRefreshStatsRef = useRef(onRefreshStats); + onRefreshStatsRef.current = onRefreshStats; + + // Live-Sync WebSocket listener for tasks (registered once on mount) useEffect(() => { const handleWs = (e: any) => { const msg = e.detail; @@ -113,10 +122,10 @@ export const TasksView: React.FC = ({ ) { setTasks((prev) => { if (prev.some((t) => t.id === created.id)) return prev; - if (selectedListId !== 'all' && created.taskListId !== selectedListId) return prev; + if (selectedListIdRef.current !== 'all' && created.taskListId !== selectedListIdRef.current) return prev; return [created, ...prev]; }); - onRefreshStats(); + onRefreshStatsRef.current(); } } else if (msg.event === 'TASK_UPDATED') { const updated = msg.payload as Thought; @@ -129,37 +138,37 @@ export const TasksView: React.FC = ({ updated.kanbanStatus || updated.taskListId || updated.isCompleted !== undefined) && - (selectedListId === 'all' || updated.taskListId === selectedListId) + (selectedListIdRef.current === 'all' || updated.taskListId === selectedListIdRef.current) ) { return [updated, ...prev]; } return prev; } - if (selectedListId !== 'all' && updated.taskListId !== selectedListId) { + if (selectedListIdRef.current !== 'all' && updated.taskListId !== selectedListIdRef.current) { return prev.filter((t) => t.id !== updated.id); } return prev.map((t) => { if (t.id !== updated.id) return t; return { ...updated, - notes: activeNotesTaskId === updated.id ? t.notes : updated.notes, + notes: activeNotesTaskIdRef.current === updated.id ? t.notes : updated.notes, }; }); }); - onRefreshStats(); + onRefreshStatsRef.current(); } } else if (msg.event === 'TASK_DELETED') { const deletedId = typeof msg.payload === 'string' ? msg.payload : msg.payload?.id; if (deletedId) { setTasks((prev) => prev.filter((t) => t.id !== deletedId)); - onRefreshStats(); + onRefreshStatsRef.current(); } } }; window.addEventListener('mindsift:ws', handleWs); return () => window.removeEventListener('mindsift:ws', handleWs); - }, [selectedListId, activeNotesTaskId, onRefreshStats]); + }, []); const handleToggleDone = async (task: Thought) => { triggerHaptic(15); @@ -400,7 +409,10 @@ export const TasksView: React.FC = ({ target: 'task', taskListId: selectedListId === 'all' ? '' : selectedListId, }); - setTasks((prev) => [created, ...prev]); + setTasks((prev) => { + if (prev.some((t) => t.id === created.id)) return prev; + return [created, ...prev]; + }); onRefreshStats(); } catch { loadData(); @@ -466,7 +478,18 @@ export const TasksView: React.FC = ({ } }; - const safeTasks = Array.isArray(tasks) ? tasks : []; + const safeTasks = useMemo(() => { + if (!Array.isArray(tasks)) return []; + const seen = new Set(); + const unique: Thought[] = []; + for (const t of tasks) { + if (t && t.id && !seen.has(t.id)) { + seen.add(t.id); + unique.push(t); + } + } + return unique; + }, [tasks]); const safeTaskLists = Array.isArray(taskLists) ? taskLists : []; const safeSpaces = Array.isArray(spaces) ? spaces : []; const safeDiscarded = Array.isArray(discarded) ? discarded : [];