/** * AdminTasksPanel.tsx — S370: pannello admin task agent * * Polling automatico GET /api/agent/tasks ogni 3s. * Mostra status badge colorato, isLive indicator, goal troncato, età. * Pulsante cancel per task attivi. Filtro per status. Shift+A per toggle. */ import { useCallback, useEffect, useRef, useState, memo } from "react"; import type * as React from "react"; // FIX11: React namespace types import { X, RefreshCw, Circle, Loader2, CheckCircle2, XCircle, AlertCircle, Ban, Clock } from "lucide-react"; import { backend, type AgentTaskRow } from "@/lib/backendClient"; import { Z_INDEX } from "@/lib/zindex"; import { makeTimedSignal } from "@/lib/agentLoop/networkConstants"; // Loop-12: iOS-safe // ─── Helpers ────────────────────────────────────────────────────────────────── function fmtAge(ms: number): string { if (ms < 0) return "–"; if (ms < 60_000) return `${Math.round(ms / 1000)}s fa`; if (ms < 3600_000) return `${Math.round(ms / 60_000)}m fa`; return `${Math.round(ms / 3600_000)}h fa`; } type StatusKey = "QUEUED" | "RUNNING" | "SUCCESS" | "ERROR" | "CANCELLED" | "TIMEOUT" | string; const STATUS_CFG: Record = { QUEUED: { bg: "#1a2a4a", fg: "#6ab3ff", label: "in coda" }, RUNNING: { bg: "#1a3a2a", fg: "#4ade80", label: "running" }, SUCCESS: { bg: "#1a3a2a", fg: "#4ade80", label: "ok" }, ERROR: { bg: "#3a1a1a", fg: "#f87171", label: "errore" }, CANCELLED: { bg: "#2a2a2a", fg: "#a0a0b0", label: "annullato" }, TIMEOUT: { bg: "#3a2a1a", fg: "#fb923c", label: "timeout" }, UNKNOWN: { bg: "#2a2a2a", fg: "#888", label: "unknown" }, }; function StatusBadge({ status }: { status: string }) { const cfg = STATUS_CFG[status] ?? STATUS_CFG.UNKNOWN; return ( {cfg.label} ); } function StatusIcon({ status, isLive }: { status: string; isLive: boolean }) { const s: React.CSSProperties = { flexShrink: 0 }; if (isLive || status === "RUNNING") return ; if (status === "SUCCESS") return ; if (status === "ERROR") return ; if (status === "CANCELLED")return ; if (status === "TIMEOUT") return ; if (status === "QUEUED") return ; return ; } // ─── Main component ─────────────────────────────────────────────────────────── interface AdminTasksPanelProps { onClose: () => void; } const AdminTasksPanel = memo(function AdminTasksPanel({ onClose }: AdminTasksPanelProps) { const [tasks, setTasks] = useState([]); const [meta, setMeta] = useState({ count: 0, memory: 0, supabase: 0 }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [filter, setFilter] = useState(""); const [lastRefresh,setLastRefresh]= useState(0); const cancellingRef = useRef>(new Set()); const intervalRef = useRef | null>(null); const fetchTasks = useCallback(async () => { if (!backend.isAvailable()) { setError("Backend non configurato"); setLoading(false); return; } try { const res = await (backend as unknown as { listAgentTasks: (limit?: number, status?: string) => Promise<{ count: number; memory: number; supabase: number; tasks: AgentTaskRow[] }>; }).listAgentTasks(100, filter || undefined); setTasks(res.tasks ?? []); setMeta({ count: res.count ?? 0, memory: res.memory ?? 0, supabase: res.supabase ?? 0 }); setError(null); } catch (e) { setError(e instanceof Error ? e.message : "Errore fetch task"); } finally { setLoading(false); setLastRefresh(Date.now()); } }, [filter]); useEffect(() => { fetchTasks(); intervalRef.current = setInterval(fetchTasks, 3_000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [fetchTasks]); const handleCancel = useCallback(async (taskId: string) => { if (cancellingRef.current.has(taskId)) return; cancellingRef.current.add(taskId); try { await fetch(`${import.meta.env.VITE_BACKEND_URL ?? ""}/api/agent/tasks/${taskId}`, { method: "DELETE", headers: { "Content-Type": "application/json", ...((import.meta.env.VITE_INTERNAL_TOKEN ?? "") ? { "X-Internal-Token": import.meta.env.VITE_INTERNAL_TOKEN } : {}) }, signal: makeTimedSignal(8_000), }); await fetchTasks(); } catch { /* silent */ } finally { cancellingRef.current.delete(taskId); } }, [fetchTasks]); const displayed = filter ? tasks.filter(t => t.status === filter.toUpperCase()) : tasks; const liveCount = tasks.filter(t => t.isLive || t.status === "RUNNING").length; return (
{ if (e.target === e.currentTarget) onClose(); }}>
{/* Header */}
Admin Tasks {liveCount > 0 && ( {liveCount} LIVE )} mem:{meta.memory} sb:{meta.supabase}
{/* Filter bar */}
{["", "RUNNING", "QUEUED", "SUCCESS", "ERROR", "CANCELLED"].map(f => ( ))}
{/* Body */}
{loading && (
Caricamento…
)} {!loading && error && (
⚠ {error}
)} {!loading && !error && displayed.length === 0 && (
Nessun task{filter ? ` con status ${filter}` : " trovato"}
)} {displayed.map(task => (
{ (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.03)"; }} onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.background = "transparent"; }} >
{task.goal || "(no goal)"}
{fmtAge(task.ageMs)} {task.taskId.slice(0, 12)}… {task.source === "supabase" && ( sb )}
{(task.isLive || task.status === "RUNNING" || task.status === "QUEUED") && ( )}
))}
{/* Footer */}
{lastRefresh > 0 ? `agg. ${fmtAge(Date.now() - lastRefresh)}` : ""} Shift+A per toggle · polling 3s
{/* spin keyframe */}
); }); export default AdminTasksPanel;