Flatten repo structure: move crawler/ to root, remove vqa/ and immoweb/
The crawler subdirectory was the only active project. Moving it to the repo root simplifies paths and removes the unnecessary nesting. The vqa/ and immoweb/ directories were legacy/unused and have been removed. Updated .drone.yml, .gitignore, .claude/ docs, and skills to reflect the new flat structure.
This commit is contained in:
parent
e2247be700
commit
eafbc1ac52
221 changed files with 70 additions and 146140 deletions
|
|
@ -1,294 +0,0 @@
|
|||
import { getUser } from '@/auth/authService';
|
||||
import { getStoredPasskeyUser } from '@/auth/passkeyService';
|
||||
import { fromOidcUser, type AuthUser } from '@/auth/types';
|
||||
import { POLLING_INTERVALS } from '@/constants';
|
||||
import { fetchTaskStatus, cancelTask, clearAllTasks } from '@/services';
|
||||
import { TaskStatus, type TaskResult } from '@/types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
||||
import { Button } from './ui/button';
|
||||
import { Loader2, CheckCircle2, XCircle, X, Trash2 } from 'lucide-react';
|
||||
import { TaskProgressDrawer } from './TaskProgressDrawer';
|
||||
|
||||
interface TaskIndicatorProps {
|
||||
taskID: string | null;
|
||||
onTaskCancelled?: () => void;
|
||||
}
|
||||
|
||||
export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [progressPercentage, setProgressPercentage] = useState<number>(0);
|
||||
const [processed, setProcessed] = useState<number | null>(null);
|
||||
const [total, setTotal] = useState<number | null>(null);
|
||||
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(null);
|
||||
const [taskResult, setTaskResult] = useState<TaskResult | null>(null);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const [isClearing, setIsClearing] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const passkeyUser = getStoredPasskeyUser();
|
||||
if (passkeyUser) {
|
||||
setUser(passkeyUser);
|
||||
} else {
|
||||
getUser().then((oidcUser) => {
|
||||
if (oidcUser) setUser(fromOidcUser(oidcUser));
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !taskID) {
|
||||
setTaskStatus(null);
|
||||
setTaskResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state for new task
|
||||
setTaskStatus(TaskStatus.PENDING);
|
||||
setProgressPercentage(0);
|
||||
setProcessed(null);
|
||||
setTotal(null);
|
||||
setTaskResult(null);
|
||||
|
||||
const pollTaskStatus = async () => {
|
||||
try {
|
||||
const data = await fetchTaskStatus(user, taskID);
|
||||
const status = data.status as TaskStatus;
|
||||
setTaskStatus(status);
|
||||
|
||||
if (status === TaskStatus.SUCCESS) {
|
||||
setProgressPercentage(100);
|
||||
// Parse final result for the drawer to show completed state.
|
||||
// Only update taskResult if the new result has phase info;
|
||||
// otherwise keep the last in-progress result which has richer data
|
||||
// than the bare SUCCESS return value.
|
||||
if (data.result) {
|
||||
try {
|
||||
const parsedResult: TaskResult = JSON.parse(data.result);
|
||||
if (parsedResult.phase) {
|
||||
setTaskResult(parsedResult);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
return true; // Stop polling
|
||||
}
|
||||
|
||||
if (status === TaskStatus.FAILURE || status === TaskStatus.REVOKED) {
|
||||
return true; // Stop polling
|
||||
}
|
||||
|
||||
// Parse progress for in-progress tasks
|
||||
if (data.result) {
|
||||
try {
|
||||
const parsedResult: TaskResult = JSON.parse(data.result);
|
||||
// Only update taskResult if the parsed data has a phase field.
|
||||
// This prevents blanking the drawer when the backend sends a
|
||||
// state update without phase info (e.g. during brief transitions).
|
||||
if (parsedResult.phase) {
|
||||
setTaskResult(parsedResult);
|
||||
}
|
||||
// Only update progress/processed/total when the fields are
|
||||
// actually present — otherwise keep the previous values so
|
||||
// the UI doesn't flash back to 0 during phase transitions.
|
||||
if (parsedResult.progress !== undefined) {
|
||||
setProgressPercentage(parsedResult.progress * 100);
|
||||
}
|
||||
if (parsedResult.processed !== undefined) {
|
||||
setProcessed(parsedResult.processed);
|
||||
}
|
||||
if (parsedResult.total !== undefined) {
|
||||
setTotal(parsedResult.total);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
return false; // Continue polling
|
||||
} catch {
|
||||
setTaskStatus(TaskStatus.FAILURE);
|
||||
return true; // Stop polling on error
|
||||
}
|
||||
};
|
||||
|
||||
// Initial poll
|
||||
pollTaskStatus();
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
const shouldStop = await pollTaskStatus();
|
||||
if (shouldStop) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, POLLING_INTERVALS.TASK_STATUS_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [taskID, user]);
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (!user || !taskID || isCancelling) return;
|
||||
|
||||
setIsCancelling(true);
|
||||
try {
|
||||
const result = await cancelTask(user, taskID);
|
||||
if (result.success) {
|
||||
setTaskStatus(TaskStatus.REVOKED);
|
||||
onTaskCancelled?.();
|
||||
}
|
||||
} catch {
|
||||
// Ignore cancel errors
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = async () => {
|
||||
if (!user || isClearing) return;
|
||||
|
||||
setIsClearing(true);
|
||||
try {
|
||||
const result = await clearAllTasks(user);
|
||||
if (result.success) {
|
||||
setTaskStatus(null);
|
||||
setTaskResult(null);
|
||||
onTaskCancelled?.();
|
||||
}
|
||||
} catch {
|
||||
// Ignore clear errors
|
||||
} finally {
|
||||
setIsClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!taskID || !taskStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isInProgress = taskStatus !== TaskStatus.SUCCESS &&
|
||||
taskStatus !== TaskStatus.FAILURE &&
|
||||
taskStatus !== TaskStatus.REVOKED;
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (isInProgress) {
|
||||
return <Loader2 className="h-3.5 w-3.5 animate-spin text-blue-500" />;
|
||||
}
|
||||
if (taskStatus === TaskStatus.SUCCESS) {
|
||||
return <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />;
|
||||
}
|
||||
return <XCircle className="h-3.5 w-3.5 text-red-500" />;
|
||||
};
|
||||
|
||||
const getProgressText = () => {
|
||||
if (processed !== null && total !== null && total > 0) {
|
||||
return `${processed} / ${total}`;
|
||||
}
|
||||
if (taskResult?.phase && taskResult.phase !== 'processing') {
|
||||
const phaseLabels: Record<string, string> = {
|
||||
splitting: 'Splitting',
|
||||
splitting_complete: 'Split done',
|
||||
fetching: 'Fetching',
|
||||
};
|
||||
return phaseLabels[taskResult.phase] ?? `${Math.round(progressPercentage)}%`;
|
||||
}
|
||||
return `${Math.round(progressPercentage)}%`;
|
||||
};
|
||||
|
||||
const getTooltipContent = () => {
|
||||
if (isInProgress) {
|
||||
if (processed !== null && total !== null && total > 0) {
|
||||
return `Processing: ${processed} / ${total} listings (${Math.round(progressPercentage)}%) — click for details`;
|
||||
}
|
||||
return `Task running: ${getProgressText()} — click for details`;
|
||||
}
|
||||
if (taskStatus === TaskStatus.SUCCESS) {
|
||||
return 'Task completed successfully — click for details';
|
||||
}
|
||||
if (taskStatus === TaskStatus.REVOKED) {
|
||||
return 'Task was cancelled';
|
||||
}
|
||||
return 'Task failed';
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
{getStatusIcon()}
|
||||
{isInProgress && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-24 h-1.5 bg-primary/20 rounded-full overflow-hidden hidden sm:block">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300 ease-out rounded-full"
|
||||
style={{ width: `${Math.min(progressPercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground hidden sm:inline min-w-[60px]">
|
||||
{getProgressText()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!isInProgress && (
|
||||
<span className="text-xs text-muted-foreground hidden sm:inline">
|
||||
{taskStatus}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{getTooltipContent()}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">ID: {taskID.slice(0, 8)}...</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{isInProgress && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCancel}
|
||||
disabled={isCancelling}
|
||||
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Cancel task</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleClearAll}
|
||||
disabled={isClearing}
|
||||
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Clear all tasks</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<TaskProgressDrawer
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
taskResult={taskResult}
|
||||
taskStatus={taskStatus}
|
||||
taskID={taskID}
|
||||
onCancel={handleCancel}
|
||||
isCancelling={isCancelling}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue