Replace 5s HTTP polling with WebSocket-based real-time updates for task progress. Celery workers publish progress to Redis pub/sub channels; a FastAPI WebSocket endpoint subscribes and forwards to the browser. Polling is kept as a 30s fallback when WebSocket is unavailable. The task progress drawer now supports multiple concurrent jobs with a tab bar for switching between scrape and POI distance tasks. Backend: - Add services/task_progress_publisher.py (Redis pub/sub bridge) - Add api/ws_routes.py (WebSocket endpoint with JWT auth) - Publish progress from listing_tasks and poi_tasks - Publish REVOKED via pub/sub on cancel/clear to fix stuck UI Frontend: - Add useTaskWebSocket hook with reconnection and keepalive - Add TaskState and WS message types - TaskIndicator: WS-driven updates with polling fallback - TaskProgressDrawer: multi-job tabs, POI phase timeline - Guard against WS overwriting local cancel state
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import type { AuthUser } from '@/auth/types';
|
|
import type { TaskState } from '@/types';
|
|
import { Button } from './ui/button';
|
|
import { Separator } from './ui/separator';
|
|
import { LogOut, Home, Filter } from 'lucide-react';
|
|
import { logout } from '@/auth/authService';
|
|
import { clearPasskeyUser } from '@/auth/passkeyService';
|
|
import { HealthIndicator } from './HealthIndicator';
|
|
import { TaskIndicator } from './TaskIndicator';
|
|
|
|
interface HeaderProps {
|
|
user: AuthUser;
|
|
activeFilterCount?: number;
|
|
taskID?: string | null;
|
|
isLoading?: boolean;
|
|
onToggleFilters?: () => void;
|
|
showFilterToggle?: boolean;
|
|
onTaskCancelled?: () => void;
|
|
onTaskCompleted?: () => void;
|
|
wsTasks?: Record<string, TaskState>;
|
|
wsConnected?: boolean;
|
|
wsSubscribe?: (taskId: string) => void;
|
|
}
|
|
|
|
export function Header({
|
|
user,
|
|
activeFilterCount = 0,
|
|
taskID,
|
|
onToggleFilters,
|
|
showFilterToggle = false,
|
|
onTaskCancelled,
|
|
onTaskCompleted,
|
|
wsTasks,
|
|
wsConnected,
|
|
wsSubscribe,
|
|
}: HeaderProps) {
|
|
const handleLogout = async () => {
|
|
if (user.provider === 'passkey') {
|
|
clearPasskeyUser();
|
|
window.location.reload();
|
|
} else {
|
|
await logout();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<header className="flex h-14 shrink-0 items-center gap-3 border-b bg-background px-4">
|
|
{/* Logo / Brand */}
|
|
<div className="flex items-center gap-2">
|
|
<Home className="h-5 w-5 text-primary" />
|
|
<span className="font-semibold text-lg hidden sm:inline">Wrongmove</span>
|
|
</div>
|
|
|
|
<Separator orientation="vertical" className="h-6" />
|
|
|
|
{/* Health Indicator */}
|
|
<HealthIndicator />
|
|
|
|
{/* Task Indicator */}
|
|
<TaskIndicator
|
|
taskID={taskID ?? null}
|
|
onTaskCancelled={onTaskCancelled}
|
|
onTaskCompleted={onTaskCompleted}
|
|
wsTasks={wsTasks}
|
|
wsConnected={wsConnected}
|
|
/>
|
|
|
|
{/* Filter Toggle (mobile) */}
|
|
{showFilterToggle && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="sm:hidden"
|
|
onClick={onToggleFilters}
|
|
>
|
|
<Filter className="h-4 w-4" />
|
|
{activeFilterCount > 0 && (
|
|
<span className="ml-1 bg-primary text-primary-foreground text-xs px-1.5 py-0.5 rounded-full">
|
|
{activeFilterCount}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
)}
|
|
|
|
{/* Spacer */}
|
|
<div className="flex-1" />
|
|
|
|
{/* User Menu */}
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm text-muted-foreground hidden md:inline">
|
|
{user.email}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleLogout}
|
|
className="gap-2"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Logout</span>
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|