import { useQuery } from "@tanstack/react-query"; import { Activity, ArrowDown, ArrowUp, ArrowUpDown, CheckCircle, Clock, Play, Settings, XCircle, Zap, } from "lucide-react"; import { useState } from "react"; import api from "../utils/api"; const Automation = () => { const [activeTab, setActiveTab] = useState("overview"); const [sortField, setSortField] = useState("nextRunTimestamp"); const [sortDirection, setSortDirection] = useState("asc"); // Fetch automation overview data const { data: overview, isLoading: overviewLoading } = useQuery({ queryKey: ["automation-overview"], queryFn: async () => { const response = await api.get("/automation/overview"); return response.data.data; }, refetchInterval: 30000, // Refresh every 30 seconds }); // Fetch queue statistics useQuery({ queryKey: ["automation-stats"], queryFn: async () => { const response = await api.get("/automation/stats"); return response.data.data; }, refetchInterval: 30000, }); // Fetch recent jobs useQuery({ queryKey: ["automation-jobs"], queryFn: async () => { const jobs = await Promise.all([ api .get("/automation/jobs/github-update-check?limit=5") .then((r) => r.data.data || []), api .get("/automation/jobs/session-cleanup?limit=5") .then((r) => r.data.data || []), ]); return { githubUpdate: jobs[0], sessionCleanup: jobs[1], }; }, refetchInterval: 30000, }); const _getStatusIcon = (status) => { switch (status) { case "completed": return ; case "failed": return ; case "active": return ; default: return ; } }; const _getStatusColor = (status) => { switch (status) { case "completed": return "bg-green-100 text-green-800"; case "failed": return "bg-red-100 text-red-800"; case "active": return "bg-blue-100 text-blue-800"; default: return "bg-gray-100 text-gray-800"; } }; const _formatDate = (dateString) => { if (!dateString) return "N/A"; return new Date(dateString).toLocaleString(); }; const _formatDuration = (ms) => { if (!ms) return "N/A"; return `${ms}ms`; }; const getStatusBadge = (status) => { switch (status) { case "Success": return ( Success ); case "Failed": return ( Failed ); case "Never run": return ( Never run ); default: return ( {status} ); } }; const getNextRunTime = (schedule, _lastRun) => { if (schedule === "Manual only") return "Manual trigger only"; if (schedule.includes("Agent-driven")) return "Agent-driven (automatic)"; if (schedule === "Daily at midnight") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(0, 0, 0, 0); return tomorrow.toLocaleString([], { hour12: true, hour: "numeric", minute: "2-digit", day: "numeric", month: "numeric", year: "numeric", }); } if (schedule === "Daily at 2 AM") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(2, 0, 0, 0); return tomorrow.toLocaleString([], { hour12: true, hour: "numeric", minute: "2-digit", day: "numeric", month: "numeric", year: "numeric", }); } if (schedule === "Daily at 3 AM") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(3, 0, 0, 0); return tomorrow.toLocaleString([], { hour12: true, hour: "numeric", minute: "2-digit", day: "numeric", month: "numeric", year: "numeric", }); } if (schedule === "Daily at 4 AM") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(4, 0, 0, 0); return tomorrow.toLocaleString([], { hour12: true, hour: "numeric", minute: "2-digit", day: "numeric", month: "numeric", year: "numeric", }); } if (schedule === "Every hour") { const now = new Date(); const nextHour = new Date(now); nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0); return nextHour.toLocaleString([], { hour12: true, hour: "numeric", minute: "2-digit", day: "numeric", month: "numeric", year: "numeric", }); } return "Unknown"; }; const getNextRunTimestamp = (schedule) => { if (schedule === "Manual only") return Number.MAX_SAFE_INTEGER; // Manual tasks go to bottom if (schedule.includes("Agent-driven")) return Number.MAX_SAFE_INTEGER - 1; // Agent-driven tasks near bottom but above manual if (schedule === "Daily at midnight") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(0, 0, 0, 0); return tomorrow.getTime(); } if (schedule === "Daily at 2 AM") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(2, 0, 0, 0); return tomorrow.getTime(); } if (schedule === "Daily at 3 AM") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(3, 0, 0, 0); return tomorrow.getTime(); } if (schedule === "Daily at 4 AM") { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(4, 0, 0, 0); return tomorrow.getTime(); } if (schedule === "Every hour") { const now = new Date(); const nextHour = new Date(now); nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0); return nextHour.getTime(); } return Number.MAX_SAFE_INTEGER; // Unknown schedules go to bottom }; const openBullBoard = () => { const token = localStorage.getItem("token"); if (!token) { alert("Please log in to access the Queue Monitor"); return; } // Use the proxied URL through the frontend (port 3000) // This avoids CORS issues as everything goes through the same origin const url = `/bullboard?token=${encodeURIComponent(token)}`; // Open in a new tab instead of a new window const bullBoardWindow = window.open(url, "_blank"); // Add a message listener to handle authentication failures if (bullBoardWindow) { // Listen for authentication failures and refresh with token const checkAuth = () => { try { // Check if the Bull Board window is still open if (bullBoardWindow.closed) return; // Inject a script to handle authentication failures bullBoardWindow.postMessage( { type: "BULL_BOARD_TOKEN", token: token, }, window.location.origin, ); } catch (e) { console.log("Could not communicate with Bull Board window:", e); } }; // Send token after a short delay to ensure Bull Board is loaded setTimeout(checkAuth, 1000); } }; const triggerManualJob = async (jobType, data = {}) => { try { let endpoint; if (jobType === "github") { endpoint = "/automation/trigger/github-update"; } else if (jobType === "sessions") { endpoint = "/automation/trigger/session-cleanup"; } else if (jobType === "orphaned-repos") { endpoint = "/automation/trigger/orphaned-repo-cleanup"; } else if (jobType === "orphaned-packages") { endpoint = "/automation/trigger/orphaned-package-cleanup"; } else if (jobType === "docker-inventory") { endpoint = "/automation/trigger/docker-inventory-cleanup"; } else if (jobType === "agent-collection") { endpoint = "/automation/trigger/agent-collection"; } const _response = await api.post(endpoint, data); // Refresh data window.location.reload(); } catch (error) { console.error("Error triggering job:", error); alert( "Failed to trigger job: " + (error.response?.data?.error || error.message), ); } }; const handleSort = (field) => { if (sortField === field) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortDirection("asc"); } }; const getSortIcon = (field) => { if (sortField !== field) return ; return sortDirection === "asc" ? ( ) : ( ); }; // Sort automations based on current sort settings const sortedAutomations = overview?.automations ? [...overview.automations].sort((a, b) => { let aValue, bValue; switch (sortField) { case "name": aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); break; case "schedule": aValue = a.schedule.toLowerCase(); bValue = b.schedule.toLowerCase(); break; case "lastRun": // Convert "Never" to empty string for proper sorting aValue = a.lastRun === "Never" ? "" : a.lastRun; bValue = b.lastRun === "Never" ? "" : b.lastRun; break; case "lastRunTimestamp": aValue = a.lastRunTimestamp || 0; bValue = b.lastRunTimestamp || 0; break; case "nextRunTimestamp": aValue = getNextRunTimestamp(a.schedule); bValue = getNextRunTimestamp(b.schedule); break; case "status": aValue = a.status.toLowerCase(); bValue = b.status.toLowerCase(); break; default: aValue = a[sortField]; bValue = b[sortField]; } if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; return 0; }) : []; const tabs = [{ id: "overview", name: "Overview", icon: Settings }]; return (
{/* Page Header */}

Automation Management

Monitor and manage automated server operations, agent communications, and patch deployments

{/* Stats Cards */}
{/* Scheduled Tasks Card */}

Scheduled Tasks

{overviewLoading ? "..." : overview?.scheduledTasks || 0}

{/* Running Tasks Card */}

Running Tasks

{overviewLoading ? "..." : overview?.runningTasks || 0}

{/* Failed Tasks Card */}

Failed Tasks

{overviewLoading ? "..." : overview?.failedTasks || 0}

{/* Total Task Runs Card */}

Total Task Runs

{overviewLoading ? "..." : overview?.totalAutomations || 0}

{/* Tabs */}
{/* Tab Content */} {activeTab === "overview" && (
{overviewLoading ? (

Loading automations...

) : (
{sortedAutomations.map((automation) => ( ))}
Run handleSort("name")} >
Task {getSortIcon("name")}
handleSort("schedule")} >
Frequency {getSortIcon("schedule")}
handleSort("lastRunTimestamp")} >
Last Run {getSortIcon("lastRunTimestamp")}
handleSort("nextRunTimestamp")} >
Next Run {getSortIcon("nextRunTimestamp")}
handleSort("status")} >
Status {getSortIcon("status")}
{automation.schedule !== "Manual only" ? ( ) : ( Manual )}
{automation.name}
{automation.description}
{automation.schedule} {automation.lastRun} {getNextRunTime( automation.schedule, automation.lastRun, )} {getStatusBadge(automation.status)}
)}
)}
); }; export default Automation;