mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-10-31 20:13:50 +00:00 
			
		
		
		
	Added support for the new agent mechanism and Binary
Added bullMQ + redis to the platform for automation and queue mechanism Added new tabs in host details
This commit is contained in:
		
							
								
								
									
										23
									
								
								frontend/public/assets/bull-board-logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/public/assets/bull-board-logo.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"> | ||||
|     <circle fill="#DD2E44" cx="18" cy="18" r="18" /> | ||||
|     <circle fill="#FFF" cx="18" cy="18" r="13.5" /> | ||||
|     <circle fill="#DD2E44" cx="18" cy="18" r="10" /> | ||||
|     <circle fill="#FFF" cx="18" cy="18" r="6" /> | ||||
|     <circle fill="#DD2E44" cx="18" cy="18" r="3" /> | ||||
|     <path | ||||
|             opacity=".2" | ||||
|             d="M18.24 18.282l13.144 11.754s-2.647 3.376-7.89 5.109L17.579 18.42l.661-.138z" | ||||
|     /> | ||||
|     <path | ||||
|             fill="#FFAC33" | ||||
|             d="M18.294 19a.994.994 0 01-.704-1.699l.563-.563a.995.995 0 011.408 1.407l-.564.563a.987.987 0 01-.703.292z" | ||||
|     /> | ||||
|     <path | ||||
|             fill="#55ACEE" | ||||
|             d="M24.016 6.981c-.403 2.079 0 4.691 0 4.691l7.054-7.388c.291-1.454-.528-3.932-1.718-4.238-1.19-.306-4.079.803-5.336 6.935zm5.003 5.003c-2.079.403-4.691 0-4.691 0l7.388-7.054c1.454-.291 3.932.528 4.238 1.718.306 1.19-.803 4.079-6.935 5.336z" | ||||
|     /> | ||||
|     <path | ||||
|             fill="#3A87C2" | ||||
|             d="M32.798 4.485L21.176 17.587c-.362.362-1.673.882-2.51.046-.836-.836-.419-2.08-.057-2.443L31.815 3.501s.676-.635 1.159-.152-.176 1.136-.176 1.136z" | ||||
|     /> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.1 KiB | 
| @@ -1,20 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { | ||||
| 	Activity, | ||||
| 	AlertCircle, | ||||
| 	ArrowDown, | ||||
| 	ArrowUp, | ||||
| 	ArrowUpDown, | ||||
| 	Bot, | ||||
| 	CheckCircle, | ||||
| 	Clock, | ||||
| 	Play, | ||||
| 	RefreshCw, | ||||
| 	Settings, | ||||
| 	XCircle, | ||||
| 	Zap, | ||||
| } from "lucide-react"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useState } from "react"; | ||||
| import api from "../utils/api"; | ||||
|  | ||||
| const Automation = () => { | ||||
| @@ -33,7 +30,7 @@ const Automation = () => { | ||||
| 	}); | ||||
|  | ||||
| 	// Fetch queue statistics | ||||
| 	const { data: queueStats, isLoading: statsLoading } = useQuery({ | ||||
| 	useQuery({ | ||||
| 		queryKey: ["automation-stats"], | ||||
| 		queryFn: async () => { | ||||
| 			const response = await api.get("/automation/stats"); | ||||
| @@ -43,7 +40,7 @@ const Automation = () => { | ||||
| 	}); | ||||
|  | ||||
| 	// Fetch recent jobs | ||||
| 	const { data: recentJobs, isLoading: jobsLoading } = useQuery({ | ||||
| 	useQuery({ | ||||
| 		queryKey: ["automation-jobs"], | ||||
| 		queryFn: async () => { | ||||
| 			const jobs = await Promise.all([ | ||||
| @@ -62,7 +59,7 @@ const Automation = () => { | ||||
| 		refetchInterval: 30000, | ||||
| 	}); | ||||
|  | ||||
| 	const getStatusIcon = (status) => { | ||||
| 	const _getStatusIcon = (status) => { | ||||
| 		switch (status) { | ||||
| 			case "completed": | ||||
| 				return <CheckCircle className="h-4 w-4 text-green-500" />; | ||||
| @@ -75,7 +72,7 @@ const Automation = () => { | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const getStatusColor = (status) => { | ||||
| 	const _getStatusColor = (status) => { | ||||
| 		switch (status) { | ||||
| 			case "completed": | ||||
| 				return "bg-green-100 text-green-800"; | ||||
| @@ -88,12 +85,12 @@ const Automation = () => { | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const formatDate = (dateString) => { | ||||
| 	const _formatDate = (dateString) => { | ||||
| 		if (!dateString) return "N/A"; | ||||
| 		return new Date(dateString).toLocaleString(); | ||||
| 	}; | ||||
|  | ||||
| 	const formatDuration = (ms) => { | ||||
| 	const _formatDuration = (ms) => { | ||||
| 		if (!ms) return "N/A"; | ||||
| 		return `${ms}ms`; | ||||
| 	}; | ||||
| @@ -127,7 +124,7 @@ const Automation = () => { | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const getNextRunTime = (schedule, lastRun) => { | ||||
| 	const getNextRunTime = (schedule, _lastRun) => { | ||||
| 		if (schedule === "Manual only") return "Manual trigger only"; | ||||
| 		if (schedule === "Daily at midnight") { | ||||
| 			const now = new Date(); | ||||
| @@ -198,6 +195,19 @@ const Automation = () => { | ||||
| 		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 = `/admin/queues?token=${encodeURIComponent(token)}`; | ||||
| 		window.open(url, "_blank", "width=1200,height=800"); | ||||
| 	}; | ||||
|  | ||||
| 	const triggerManualJob = async (jobType, data = {}) => { | ||||
| 		try { | ||||
| 			let endpoint; | ||||
| @@ -206,13 +216,11 @@ const Automation = () => { | ||||
| 				endpoint = "/automation/trigger/github-update"; | ||||
| 			} else if (jobType === "sessions") { | ||||
| 				endpoint = "/automation/trigger/session-cleanup"; | ||||
| 			} else if (jobType === "echo") { | ||||
| 				endpoint = "/automation/trigger/echo-hello"; | ||||
| 			} else if (jobType === "orphaned-repos") { | ||||
| 				endpoint = "/automation/trigger/orphaned-repo-cleanup"; | ||||
| 			} | ||||
|  | ||||
| 			const response = await api.post(endpoint, data); | ||||
| 			const _response = await api.post(endpoint, data); | ||||
|  | ||||
| 			// Refresh data | ||||
| 			window.location.reload(); | ||||
| @@ -303,34 +311,40 @@ const Automation = () => { | ||||
| 				<div className="flex items-center gap-3"> | ||||
| 					<button | ||||
| 						type="button" | ||||
| 						onClick={() => triggerManualJob("github")} | ||||
| 						onClick={openBullBoard} | ||||
| 						className="btn-outline flex items-center gap-2" | ||||
| 						title="Trigger manual GitHub update check" | ||||
| 						title="Open Bull Board Queue Monitor" | ||||
| 					> | ||||
| 						<RefreshCw className="h-4 w-4" /> | ||||
| 						Check Updates | ||||
| 					</button> | ||||
| 					<button | ||||
| 						type="button" | ||||
| 						onClick={() => triggerManualJob("sessions")} | ||||
| 						className="btn-outline flex items-center gap-2" | ||||
| 						title="Trigger manual session cleanup" | ||||
| 					> | ||||
| 						<RefreshCw className="h-4 w-4" /> | ||||
| 						Clean Sessions | ||||
| 					</button> | ||||
| 					<button | ||||
| 						type="button" | ||||
| 						onClick={() => | ||||
| 							triggerManualJob("echo", { | ||||
| 								message: "Hello from Automation Page!", | ||||
| 							}) | ||||
| 						} | ||||
| 						className="btn-outline flex items-center gap-2" | ||||
| 						title="Trigger echo hello task" | ||||
| 					> | ||||
| 						<RefreshCw className="h-4 w-4" /> | ||||
| 						Echo Hello | ||||
| 						<svg | ||||
| 							className="h-4 w-4" | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 36 36" | ||||
| 							role="img" | ||||
| 							aria-label="Bull Board" | ||||
| 						> | ||||
| 							<circle fill="#DD2E44" cx="18" cy="18" r="18" /> | ||||
| 							<circle fill="#FFF" cx="18" cy="18" r="13.5" /> | ||||
| 							<circle fill="#DD2E44" cx="18" cy="18" r="10" /> | ||||
| 							<circle fill="#FFF" cx="18" cy="18" r="6" /> | ||||
| 							<circle fill="#DD2E44" cx="18" cy="18" r="3" /> | ||||
| 							<path | ||||
| 								opacity=".2" | ||||
| 								d="M18.24 18.282l13.144 11.754s-2.647 3.376-7.89 5.109L17.579 18.42l.661-.138z" | ||||
| 							/> | ||||
| 							<path | ||||
| 								fill="#FFAC33" | ||||
| 								d="M18.294 19a.994.994 0 01-.704-1.699l.563-.563a.995.995 0 011.408 1.407l-.564.563a.987.987 0 01-.703.292z" | ||||
| 							/> | ||||
| 							<path | ||||
| 								fill="#55ACEE" | ||||
| 								d="M24.016 6.981c-.403 2.079 0 4.691 0 4.691l7.054-7.388c.291-1.454-.528-3.932-1.718-4.238-1.19-.306-4.079.803-5.336 6.935zm5.003 5.003c-2.079.403-4.691 0-4.691 0l7.388-7.054c1.454-.291 3.932.528 4.238 1.718.306 1.19-.803 4.079-6.935 5.336z" | ||||
| 							/> | ||||
| 							<path | ||||
| 								fill="#3A87C2" | ||||
| 								d="M32.798 4.485L21.176 17.587c-.362.362-1.673.882-2.51.046-.836-.836-.419-2.08-.057-2.443L31.815 3.501s.676-.635 1.159-.152-.176 1.136-.176 1.136z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 						Queue Monitor | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -509,10 +523,6 @@ const Automation = () => { | ||||
| 																triggerManualJob("github"); | ||||
| 															} else if (automation.queue.includes("session")) { | ||||
| 																triggerManualJob("sessions"); | ||||
| 															} else if (automation.queue.includes("echo")) { | ||||
| 																triggerManualJob("echo", { | ||||
| 																	message: "Manual trigger from table", | ||||
| 																}); | ||||
| 															} else if ( | ||||
| 																automation.queue.includes("orphaned-repo") | ||||
| 															) { | ||||
| @@ -525,20 +535,7 @@ const Automation = () => { | ||||
| 														<Play className="h-3 w-3" /> | ||||
| 													</button> | ||||
| 												) : ( | ||||
| 													<button | ||||
| 														type="button" | ||||
| 														onClick={() => { | ||||
| 															if (automation.queue.includes("echo")) { | ||||
| 																triggerManualJob("echo", { | ||||
| 																	message: "Manual trigger from table", | ||||
| 																}); | ||||
| 															} | ||||
| 														}} | ||||
| 														className="inline-flex items-center justify-center w-6 h-6 border border-transparent rounded text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-colors duration-200" | ||||
| 														title="Trigger" | ||||
| 													> | ||||
| 														<Play className="h-3 w-3" /> | ||||
| 													</button> | ||||
| 													<span className="text-gray-400 text-xs">Manual</span> | ||||
| 												)} | ||||
| 											</td> | ||||
| 											<td className="px-4 py-2 whitespace-nowrap"> | ||||
|   | ||||
| @@ -1161,7 +1161,7 @@ const Dashboard = () => { | ||||
| 							try { | ||||
| 								const date = new Date(`${label}:00:00`); | ||||
| 								// Check if date is valid | ||||
| 								if (isNaN(date.getTime())) { | ||||
| 								if (Number.isNaN(date.getTime())) { | ||||
| 									return label; // Return original label if date is invalid | ||||
| 								} | ||||
| 								return date.toLocaleDateString("en-US", { | ||||
| @@ -1171,7 +1171,7 @@ const Dashboard = () => { | ||||
| 									minute: "2-digit", | ||||
| 									hour12: true, | ||||
| 								}); | ||||
| 							} catch (error) { | ||||
| 							} catch (_error) { | ||||
| 								return label; // Return original label if parsing fails | ||||
| 							} | ||||
| 						} | ||||
| @@ -1180,14 +1180,14 @@ const Dashboard = () => { | ||||
| 						try { | ||||
| 							const date = new Date(label); | ||||
| 							// Check if date is valid | ||||
| 							if (isNaN(date.getTime())) { | ||||
| 							if (Number.isNaN(date.getTime())) { | ||||
| 								return label; // Return original label if date is invalid | ||||
| 							} | ||||
| 							return date.toLocaleDateString("en-US", { | ||||
| 								month: "short", | ||||
| 								day: "numeric", | ||||
| 							}); | ||||
| 						} catch (error) { | ||||
| 						} catch (_error) { | ||||
| 							return label; // Return original label if parsing fails | ||||
| 						} | ||||
| 					}, | ||||
| @@ -1222,7 +1222,7 @@ const Dashboard = () => { | ||||
| 								const hourNum = parseInt(hour, 10); | ||||
|  | ||||
| 								// Validate hour number | ||||
| 								if (isNaN(hourNum) || hourNum < 0 || hourNum > 23) { | ||||
| 								if (Number.isNaN(hourNum) || hourNum < 0 || hourNum > 23) { | ||||
| 									return hour; // Return original hour if invalid | ||||
| 								} | ||||
|  | ||||
| @@ -1233,7 +1233,7 @@ const Dashboard = () => { | ||||
| 										: hourNum === 12 | ||||
| 											? "12 PM" | ||||
| 											: `${hourNum - 12} PM`; | ||||
| 							} catch (error) { | ||||
| 							} catch (_error) { | ||||
| 								return label; // Return original label if parsing fails | ||||
| 							} | ||||
| 						} | ||||
| @@ -1242,14 +1242,14 @@ const Dashboard = () => { | ||||
| 						try { | ||||
| 							const date = new Date(label); | ||||
| 							// Check if date is valid | ||||
| 							if (isNaN(date.getTime())) { | ||||
| 							if (Number.isNaN(date.getTime())) { | ||||
| 								return label; // Return original label if date is invalid | ||||
| 							} | ||||
| 							return date.toLocaleDateString("en-US", { | ||||
| 								month: "short", | ||||
| 								day: "numeric", | ||||
| 							}); | ||||
| 						} catch (error) { | ||||
| 						} catch (_error) { | ||||
| 							return label; // Return original label if parsing fails | ||||
| 						} | ||||
| 					}, | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||||
| import { | ||||
| 	Activity, | ||||
| 	AlertCircle, | ||||
| 	AlertTriangle, | ||||
| 	ArrowLeft, | ||||
| 	Calendar, | ||||
| 	CheckCircle, | ||||
| 	CheckCircle2, | ||||
| 	Clock, | ||||
| 	Clock3, | ||||
| 	Copy, | ||||
| 	Cpu, | ||||
| 	Database, | ||||
| @@ -46,6 +49,7 @@ const HostDetail = () => { | ||||
| 	const [activeTab, setActiveTab] = useState("host"); | ||||
| 	const [historyPage, setHistoryPage] = useState(0); | ||||
| 	const [historyLimit] = useState(10); | ||||
| 	const [notes, setNotes] = useState(""); | ||||
|  | ||||
| 	const { | ||||
| 		data: host, | ||||
| @@ -87,6 +91,13 @@ const HostDetail = () => { | ||||
| 		} | ||||
| 	}, [host]); | ||||
|  | ||||
| 	// Sync notes state with host data | ||||
| 	useEffect(() => { | ||||
| 		if (host) { | ||||
| 			setNotes(host.notes || ""); | ||||
| 		} | ||||
| 	}, [host]); | ||||
|  | ||||
| 	const deleteHostMutation = useMutation({ | ||||
| 		mutationFn: (hostId) => adminHostsAPI.delete(hostId), | ||||
| 		onSuccess: () => { | ||||
| @@ -292,10 +303,10 @@ const HostDetail = () => { | ||||
| 					<button | ||||
| 						type="button" | ||||
| 						onClick={() => setShowDeleteModal(true)} | ||||
| 						className="btn-danger flex items-center gap-2 text-sm" | ||||
| 						className="btn-danger flex items-center justify-center p-2 text-sm" | ||||
| 						title="Delete host" | ||||
| 					> | ||||
| 						<Trash2 className="h-4 w-4" /> | ||||
| 						Delete | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -426,7 +437,18 @@ const HostDetail = () => { | ||||
| 									: "text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300" | ||||
| 							}`} | ||||
| 						> | ||||
| 							Agent History | ||||
| 							Package Reports | ||||
| 						</button> | ||||
| 						<button | ||||
| 							type="button" | ||||
| 							onClick={() => handleTabChange("queue")} | ||||
| 							className={`px-4 py-2 text-sm font-medium ${ | ||||
| 								activeTab === "queue" | ||||
| 									? "text-primary-600 dark:text-primary-400 border-b-2 border-primary-500" | ||||
| 									: "text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300" | ||||
| 							}`} | ||||
| 						> | ||||
| 							Agent Queue | ||||
| 						</button> | ||||
| 						<button | ||||
| 							type="button" | ||||
| @@ -1097,12 +1119,8 @@ const HostDetail = () => { | ||||
| 								</div> | ||||
| 								<div className="bg-secondary-50 dark:bg-secondary-700 rounded-lg p-4"> | ||||
| 									<textarea | ||||
| 										value={host.notes || ""} | ||||
| 										onChange={(e) => { | ||||
| 											// Update local state immediately for better UX | ||||
| 											const updatedHost = { ...host, notes: e.target.value }; | ||||
| 											queryClient.setQueryData(["host", hostId], updatedHost); | ||||
| 										}} | ||||
| 										value={notes} | ||||
| 										onChange={(e) => setNotes(e.target.value)} | ||||
| 										placeholder="Add notes about this host... (e.g., purpose, special configurations, maintenance notes)" | ||||
| 										className="w-full h-32 p-3 border border-secondary-200 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-white placeholder-secondary-500 dark:placeholder-secondary-400 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 resize-none" | ||||
| 										maxLength={1000} | ||||
| @@ -1114,14 +1132,14 @@ const HostDetail = () => { | ||||
| 										</p> | ||||
| 										<div className="flex items-center gap-2"> | ||||
| 											<span className="text-xs text-secondary-400 dark:text-secondary-500"> | ||||
| 												{(host.notes || "").length}/1000 | ||||
| 												{notes.length}/1000 | ||||
| 											</span> | ||||
| 											<button | ||||
| 												type="button" | ||||
| 												onClick={() => { | ||||
| 													updateNotesMutation.mutate({ | ||||
| 														hostId: host.id, | ||||
| 														notes: host.notes || "", | ||||
| 														notes: notes, | ||||
| 													}); | ||||
| 												}} | ||||
| 												disabled={updateNotesMutation.isPending} | ||||
| @@ -1136,6 +1154,9 @@ const HostDetail = () => { | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						)} | ||||
|  | ||||
| 						{/* Agent Queue */} | ||||
| 						{activeTab === "queue" && <AgentQueueTab hostId={hostId} />} | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -1659,4 +1680,250 @@ const DeleteConfirmationModal = ({ | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| // Agent Queue Tab Component | ||||
| const AgentQueueTab = ({ hostId }) => { | ||||
| 	const { | ||||
| 		data: queueData, | ||||
| 		isLoading, | ||||
| 		error, | ||||
| 		refetch, | ||||
| 	} = useQuery({ | ||||
| 		queryKey: ["host-queue", hostId], | ||||
| 		queryFn: () => dashboardAPI.getHostQueue(hostId).then((res) => res.data), | ||||
| 		staleTime: 30 * 1000, // 30 seconds | ||||
| 		refetchInterval: 30 * 1000, // Auto-refresh every 30 seconds | ||||
| 	}); | ||||
|  | ||||
| 	if (isLoading) { | ||||
| 		return ( | ||||
| 			<div className="flex items-center justify-center h-32"> | ||||
| 				<RefreshCw className="h-6 w-6 animate-spin text-primary-600" /> | ||||
| 			</div> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	if (error) { | ||||
| 		return ( | ||||
| 			<div className="text-center py-8"> | ||||
| 				<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" /> | ||||
| 				<p className="text-red-600 dark:text-red-400"> | ||||
| 					Failed to load queue data | ||||
| 				</p> | ||||
| 				<button | ||||
| 					type="button" | ||||
| 					onClick={() => refetch()} | ||||
| 					className="mt-2 px-4 py-2 text-sm bg-primary-600 text-white rounded-md hover:bg-primary-700" | ||||
| 				> | ||||
| 					Retry | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	const { waiting, active, delayed, failed, jobHistory } = queueData.data; | ||||
|  | ||||
| 	const getStatusIcon = (status) => { | ||||
| 		switch (status) { | ||||
| 			case "completed": | ||||
| 				return <CheckCircle2 className="h-4 w-4 text-green-500" />; | ||||
| 			case "failed": | ||||
| 				return <AlertCircle className="h-4 w-4 text-red-500" />; | ||||
| 			case "active": | ||||
| 				return <Clock3 className="h-4 w-4 text-blue-500" />; | ||||
| 			default: | ||||
| 				return <Clock className="h-4 w-4 text-gray-500" />; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const getStatusColor = (status) => { | ||||
| 		switch (status) { | ||||
| 			case "completed": | ||||
| 				return "text-green-600 dark:text-green-400"; | ||||
| 			case "failed": | ||||
| 				return "text-red-600 dark:text-red-400"; | ||||
| 			case "active": | ||||
| 				return "text-blue-600 dark:text-blue-400"; | ||||
| 			default: | ||||
| 				return "text-gray-600 dark:text-gray-400"; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const formatJobType = (type) => { | ||||
| 		switch (type) { | ||||
| 			case "settings_update": | ||||
| 				return "Settings Update"; | ||||
| 			case "report_now": | ||||
| 				return "Report Now"; | ||||
| 			case "update_agent": | ||||
| 				return "Agent Update"; | ||||
| 			default: | ||||
| 				return type; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="space-y-6"> | ||||
| 			<div className="flex items-center justify-between"> | ||||
| 				<h3 className="text-lg font-medium text-secondary-900 dark:text-white"> | ||||
| 					Agent Queue Status | ||||
| 				</h3> | ||||
| 				<button | ||||
| 					type="button" | ||||
| 					onClick={() => refetch()} | ||||
| 					className="btn-outline flex items-center gap-2" | ||||
| 					title="Refresh queue data" | ||||
| 				> | ||||
| 					<RefreshCw className="h-4 w-4" /> | ||||
| 					Refresh | ||||
| 				</button> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Queue Summary */} | ||||
| 			<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | ||||
| 				<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4"> | ||||
| 					<div className="flex items-center gap-3"> | ||||
| 						<Server className="h-8 w-8 text-blue-600 dark:text-blue-400" /> | ||||
| 						<div> | ||||
| 							<p className="text-sm text-blue-600 dark:text-blue-400 font-medium"> | ||||
| 								Waiting | ||||
| 							</p> | ||||
| 							<p className="text-2xl font-bold text-blue-700 dark:text-blue-300"> | ||||
| 								{waiting} | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | ||||
| 				<div className="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4"> | ||||
| 					<div className="flex items-center gap-3"> | ||||
| 						<Clock3 className="h-8 w-8 text-yellow-600 dark:text-yellow-400" /> | ||||
| 						<div> | ||||
| 							<p className="text-sm text-yellow-600 dark:text-yellow-400 font-medium"> | ||||
| 								Active | ||||
| 							</p> | ||||
| 							<p className="text-2xl font-bold text-yellow-700 dark:text-yellow-300"> | ||||
| 								{active} | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | ||||
| 				<div className="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4"> | ||||
| 					<div className="flex items-center gap-3"> | ||||
| 						<Clock className="h-8 w-8 text-purple-600 dark:text-purple-400" /> | ||||
| 						<div> | ||||
| 							<p className="text-sm text-purple-600 dark:text-purple-400 font-medium"> | ||||
| 								Delayed | ||||
| 							</p> | ||||
| 							<p className="text-2xl font-bold text-purple-700 dark:text-purple-300"> | ||||
| 								{delayed} | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | ||||
| 				<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4"> | ||||
| 					<div className="flex items-center gap-3"> | ||||
| 						<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" /> | ||||
| 						<div> | ||||
| 							<p className="text-sm text-red-600 dark:text-red-400 font-medium"> | ||||
| 								Failed | ||||
| 							</p> | ||||
| 							<p className="text-2xl font-bold text-red-700 dark:text-red-300"> | ||||
| 								{failed} | ||||
| 							</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			{/* Job History */} | ||||
| 			<div> | ||||
| 				{jobHistory.length === 0 ? ( | ||||
| 					<div className="text-center py-8"> | ||||
| 						<Server className="h-12 w-12 text-gray-400 mx-auto mb-4" /> | ||||
| 						<p className="text-gray-500 dark:text-gray-400"> | ||||
| 							No job history found | ||||
| 						</p> | ||||
| 					</div> | ||||
| 				) : ( | ||||
| 					<div className="overflow-x-auto"> | ||||
| 						<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600"> | ||||
| 							<thead className="bg-secondary-50 dark:bg-secondary-700"> | ||||
| 								<tr> | ||||
| 									<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"> | ||||
| 										Job ID | ||||
| 									</th> | ||||
| 									<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"> | ||||
| 										Job Name | ||||
| 									</th> | ||||
| 									<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"> | ||||
| 										Status | ||||
| 									</th> | ||||
| 									<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"> | ||||
| 										Attempt | ||||
| 									</th> | ||||
| 									<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"> | ||||
| 										Date/Time | ||||
| 									</th> | ||||
| 									<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider"> | ||||
| 										Error/Output | ||||
| 									</th> | ||||
| 								</tr> | ||||
| 							</thead> | ||||
| 							<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600"> | ||||
| 								{jobHistory.map((job) => ( | ||||
| 									<tr | ||||
| 										key={job.id} | ||||
| 										className="hover:bg-secondary-50 dark:hover:bg-secondary-700" | ||||
| 									> | ||||
| 										<td className="px-4 py-2 whitespace-nowrap text-xs font-mono text-secondary-900 dark:text-white"> | ||||
| 											{job.job_id} | ||||
| 										</td> | ||||
| 										<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white"> | ||||
| 											{formatJobType(job.job_name)} | ||||
| 										</td> | ||||
| 										<td className="px-4 py-2 whitespace-nowrap"> | ||||
| 											<div className="flex items-center gap-2"> | ||||
| 												{getStatusIcon(job.status)} | ||||
| 												<span | ||||
| 													className={`text-xs font-medium ${getStatusColor(job.status)}`} | ||||
| 												> | ||||
| 													{job.status.charAt(0).toUpperCase() + | ||||
| 														job.status.slice(1)} | ||||
| 												</span> | ||||
| 											</div> | ||||
| 										</td> | ||||
| 										<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white"> | ||||
| 											{job.attempt_number} | ||||
| 										</td> | ||||
| 										<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white"> | ||||
| 											{new Date(job.created_at).toLocaleString()} | ||||
| 										</td> | ||||
| 										<td className="px-4 py-2 text-xs"> | ||||
| 											{job.error_message ? ( | ||||
| 												<span className="text-red-600 dark:text-red-400"> | ||||
| 													{job.error_message} | ||||
| 												</span> | ||||
| 											) : job.output ? ( | ||||
| 												<span className="text-green-600 dark:text-green-400"> | ||||
| 													{JSON.stringify(job.output)} | ||||
| 												</span> | ||||
| 											) : ( | ||||
| 												<span className="text-secondary-500 dark:text-secondary-400"> | ||||
| 													- | ||||
| 												</span> | ||||
| 											)} | ||||
| 										</td> | ||||
| 									</tr> | ||||
| 								))} | ||||
| 							</tbody> | ||||
| 						</table> | ||||
| 					</div> | ||||
| 				)} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| export default HostDetail; | ||||
|   | ||||
| @@ -56,6 +56,11 @@ export const dashboardAPI = { | ||||
| 		const url = `/dashboard/hosts/${hostId}${queryString ? `?${queryString}` : ""}`; | ||||
| 		return api.get(url); | ||||
| 	}, | ||||
| 	getHostQueue: (hostId, params = {}) => { | ||||
| 		const queryString = new URLSearchParams(params).toString(); | ||||
| 		const url = `/dashboard/hosts/${hostId}/queue${queryString ? `?${queryString}` : ""}`; | ||||
| 		return api.get(url); | ||||
| 	}, | ||||
| 	getPackageTrends: (params = {}) => { | ||||
| 		const queryString = new URLSearchParams(params).toString(); | ||||
| 		const url = `/dashboard/package-trends${queryString ? `?${queryString}` : ""}`; | ||||
|   | ||||
| @@ -37,6 +37,11 @@ export default defineConfig({ | ||||
| 							} | ||||
| 						: undefined, | ||||
| 			}, | ||||
| 			"/admin": { | ||||
| 				target: `http://${process.env.BACKEND_HOST || "localhost"}:${process.env.BACKEND_PORT || "3001"}`, | ||||
| 				changeOrigin: true, | ||||
| 				secure: false, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	build: { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user