diff --git a/apps/web/components/notes/note-editor.tsx b/apps/web/components/notes/note-editor.tsx index e3a1244..8e22ba0 100644 --- a/apps/web/components/notes/note-editor.tsx +++ b/apps/web/components/notes/note-editor.tsx @@ -128,7 +128,7 @@ export function NoteEditor({ content, onChange, onBlur }: NoteEditorProps) { {/* Editor content */}
- +
); diff --git a/apps/web/components/tasks/tasks-kanban-view.tsx b/apps/web/components/tasks/tasks-kanban-view.tsx index 253426a..c2301c2 100644 --- a/apps/web/components/tasks/tasks-kanban-view.tsx +++ b/apps/web/components/tasks/tasks-kanban-view.tsx @@ -76,7 +76,7 @@ function TaskCard({ onClick: () => void; }) { return ( - + { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }} aria-label={task.title}>
diff --git a/apps/web/components/tasks/tasks-list-view.tsx b/apps/web/components/tasks/tasks-list-view.tsx index 0d1d8f7..36eac05 100644 --- a/apps/web/components/tasks/tasks-list-view.tsx +++ b/apps/web/components/tasks/tasks-list-view.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import { Table, @@ -177,6 +177,7 @@ export function TasksListView({ const [offset, setOffset] = useState(0); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); + const lastErrorRef = useRef(null); const [selectedTask, setSelectedTask] = useState(null); const [deleteId, setDeleteId] = useState(null); const [deleting, setDeleting] = useState(false); @@ -249,9 +250,22 @@ export function TasksListView({ setOffset(0); } setTotalCount(data.totalItems || 0); + // Successful fetch — reset the dedup tracker so the next error + // class toasts fresh instead of being suppressed. + lastErrorRef.current = null; } catch (error) { console.error('Failed to fetch tasks:', error); - toast.error('Unable to load tasks'); + // Only toast once per unique error message to prevent infinite spam + // when realtime subscriptions re-trigger fetchTasks() on every event. + const message = error instanceof Error ? error.message : 'Unable to load tasks'; + if (message !== lastErrorRef.current) { + lastErrorRef.current = message; + if (message.includes('429') || message.toLowerCase().includes('rate')) { + toast.error('Rate limited — slowing down'); + } else { + toast.error('Unable to load tasks'); + } + } } finally { setLoading(false); setLoadingMore(false); @@ -286,13 +300,26 @@ export function TasksListView({ // Subscribe to realtime updates useEffect(() => { if (!domainId) return; - const unsubscribe = subscribe(['task'], (event: any) => { - if (event.type === 'task') { + // Debounce realtime-triggered refetches so a burst of events does + // not cause a flood of fetchTasks() calls + toasts. + let timer: ReturnType | null = null; + const debouncedRefetch = () => { + if (timer) clearTimeout(timer); + timer = setTimeout(() => { fetchTasks(); onRefresh?.(); + }, 750); + }; + debouncedRefetch.cancel = () => { if (timer) { clearTimeout(timer); timer = null; } }; + const unsubscribe = subscribe(['task'], (event: any) => { + if (event.type === 'task') { + debouncedRefetch(); } }); - return unsubscribe; + return () => { + debouncedRefetch.cancel(); + unsubscribe; + }; }, [domainId, subscribe, fetchTasks, onRefresh]); // Clear selection when tasks change