Refactor task progress to unified useTaskProgress hook

Replace WebSocket-only useTaskWebSocket with useTaskProgress that
provides a unified task state interface. TaskIndicator no longer
manages its own polling or auth — it receives task state from the
parent via props. Rename wsTasks prop to tasks throughout.
This commit is contained in:
Viktor Barzin 2026-02-09 23:02:24 +00:00
parent 3616e678ac
commit 2d86213db5
No known key found for this signature in database
GPG key ID: 0EB088298288D958
6 changed files with 130 additions and 363 deletions

View file

@ -17,13 +17,16 @@ import { Sheet, SheetContent, SheetTrigger } from './components/ui/sheet';
import { Button } from './components/ui/button';
import { Filter } from 'lucide-react';
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types';
import { refreshListings, fetchTasksForUser, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services';
import { refreshListings, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services';
import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils';
import { useTaskWebSocket } from '@/hooks/useTaskWebSocket';
import { useTaskProgress } from '@/hooks/useTaskProgress';
function isTerminalStatus(status: string): boolean {
return status === 'SUCCESS' || status === 'FAILURE' || status === 'REVOKED';
}
function App() {
const [listingData, setListingData] = useState<GeoJSONFeatureCollection | null>(null);
const [taskID, setTaskID] = useState<string | null>(null);
const [user, setUser] = useState<AuthUser | null>(null);
const [queryParameters, setQueryParameters] = useState<ParameterValues | null>(null);
const [submitError, setSubmitError] = useState<string | null>(null);
@ -44,8 +47,26 @@ function App() {
const [poiTravelFilters, setPoiTravelFilters] = useState<Record<number, POITravelFilter>>({});
const [currentMetric, setCurrentMetric] = useState<Metric>(DEFAULT_FILTER_VALUES.metric);
// WebSocket-based real-time task progress
const { tasks: wsTasks, isConnected: wsConnected, subscribe: wsSubscribe } = useTaskWebSocket(user);
// Explicit task ID set by fetch-data action (to track as "active")
const [explicitTaskId, setExplicitTaskId] = useState<string | null>(null);
// Unified task progress: WS primary, polling fallback
const { tasks, isConnected, subscribe, cancelTask, clearAllTasks } = useTaskProgress(user);
// Derive activeTaskId: explicit ID if set, else most recent non-terminal task
const activeTaskId = useMemo(() => {
if (explicitTaskId && tasks[explicitTaskId]) return explicitTaskId;
// Fall back to any non-terminal task
const nonTerminal = Object.entries(tasks).filter(
([, t]) => !isTerminalStatus(t.status),
);
if (nonTerminal.length > 0) return nonTerminal[0][0];
// Fall back to explicit even if terminal (to show final status)
if (explicitTaskId && tasks[explicitTaskId]) return explicitTaskId;
// Show most recent task if any
const allIds = Object.keys(tasks);
return allIds.length > 0 ? allIds[allIds.length - 1] : null;
}, [explicitTaskId, tasks]);
// Ref to track accumulated features during streaming
const accumulatedFeaturesRef = useRef<PropertyFeature[]>([]);
@ -77,17 +98,6 @@ function App() {
setUser(passkeyUser);
};
useEffect(() => {
if (!user) {
return;
}
fetchTasksForUser(user).then((tasks) => {
if (tasks && tasks.length > 0) {
setTaskID(tasks[0]);
}
});
}, [user, taskID]);
// Load user's POIs
useEffect(() => {
if (!user) return;
@ -235,6 +245,10 @@ function App() {
}
}, [queryParameters, loadListings]);
const handleTaskCancelled = useCallback(() => {
setExplicitTaskId(null);
}, []);
if (!user) {
return <LoginModal isOpen={user === null} onPasskeyLogin={handlePasskeyLogin} />;
}
@ -248,8 +262,8 @@ function App() {
setIsLoading(true);
try {
const data = await refreshListings(user!, parameters);
setTaskID(data.task_id);
if (data.task_id) wsSubscribe(data.task_id);
setExplicitTaskId(data.task_id);
if (data.task_id) subscribe(data.task_id);
} catch (error) {
if (error instanceof Error) {
setSubmitError(error.message);
@ -347,13 +361,9 @@ function App() {
);
};
const handleTaskCancelled = () => {
setTaskID(null);
};
const handlePOITaskCreated = (taskId: string) => {
setTaskID(taskId);
if (taskId) wsSubscribe(taskId);
setExplicitTaskId(taskId);
if (taskId) subscribe(taskId);
// Refresh POI list in case new ones were created
if (user) {
fetchUserPOIs(user).then(setUserPOIs).catch(() => {});
@ -379,12 +389,18 @@ function App() {
{/* Header */}
<Header
user={user}
taskID={taskID}
onTaskCancelled={handleTaskCancelled}
tasks={tasks}
activeTaskId={activeTaskId}
isConnected={isConnected}
onCancelTask={cancelTask}
onClearAllTasks={async () => {
const result = await clearAllTasks();
if (result) {
handleTaskCancelled();
}
return result;
}}
onTaskCompleted={handleTaskCompleted}
wsTasks={wsTasks}
wsConnected={wsConnected}
wsSubscribe={wsSubscribe}
/>
{/* Main content area */}