import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Activity,
AlertCircle,
AlertTriangle,
ArrowLeft,
Calendar,
CheckCircle,
CheckCircle2,
Clock,
Clock3,
Copy,
Cpu,
Database,
Eye,
EyeOff,
HardDrive,
Key,
MemoryStick,
Monitor,
Package,
RefreshCw,
Server,
Shield,
Terminal,
Trash2,
Wifi,
X,
} from "lucide-react";
import { useEffect, useId, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import InlineEdit from "../components/InlineEdit";
import InlineMultiGroupEdit from "../components/InlineMultiGroupEdit";
import {
adminHostsAPI,
dashboardAPI,
formatDate,
formatRelativeTime,
hostGroupsAPI,
repositoryAPI,
settingsAPI,
} from "../utils/api";
import { OSIcon } from "../utils/osIcons.jsx";
const HostDetail = () => {
const { hostId } = useParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [showCredentialsModal, setShowCredentialsModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [activeTab, setActiveTab] = useState("host");
const [historyPage, setHistoryPage] = useState(0);
const [historyLimit] = useState(10);
const [notes, setNotes] = useState("");
const [notesMessage, setNotesMessage] = useState({ text: "", type: "" });
const {
data: host,
isLoading,
error,
refetch,
isFetching,
} = useQuery({
queryKey: ["host", hostId, historyPage, historyLimit],
queryFn: () =>
dashboardAPI
.getHostDetail(hostId, {
limit: historyLimit,
offset: historyPage * historyLimit,
})
.then((res) => res.data),
staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer
refetchOnWindowFocus: false, // Don't refetch when window regains focus
});
// WebSocket connection status using Server-Sent Events (SSE) for real-time push updates
const [wsStatus, setWsStatus] = useState(null);
useEffect(() => {
if (!host?.api_id) return;
const token = localStorage.getItem("token");
if (!token) return;
let eventSource = null;
let reconnectTimeout = null;
let isMounted = true;
const connect = () => {
if (!isMounted) return;
try {
// Create EventSource for SSE connection
eventSource = new EventSource(
`/api/v1/ws/status/${host.api_id}/stream?token=${encodeURIComponent(token)}`,
);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
setWsStatus(data);
} catch (_err) {
// Silently handle parse errors
}
};
eventSource.onerror = (_error) => {
console.log(`[SSE] Connection error for ${host.api_id}, retrying...`);
eventSource?.close();
// Automatic reconnection after 5 seconds
if (isMounted) {
reconnectTimeout = setTimeout(connect, 5000);
}
};
} catch (_err) {
// Silently handle connection errors
}
};
// Initial connection
connect();
// Cleanup on unmount or when api_id changes
return () => {
isMounted = false;
if (reconnectTimeout) clearTimeout(reconnectTimeout);
if (eventSource) {
eventSource.close();
}
};
}, [host?.api_id]);
// Fetch repository count for this host
const { data: repositories, isLoading: isLoadingRepos } = useQuery({
queryKey: ["host-repositories", hostId],
queryFn: () => repositoryAPI.getByHost(hostId).then((res) => res.data),
staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer
refetchOnWindowFocus: false, // Don't refetch when window regains focus
enabled: !!hostId,
});
// Fetch host groups for multi-select
const { data: hostGroups } = useQuery({
queryKey: ["host-groups"],
queryFn: () => hostGroupsAPI.list().then((res) => res.data),
staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer
refetchOnWindowFocus: false, // Don't refetch when window regains focus
});
// Tab change handler
const handleTabChange = (tabName) => {
setActiveTab(tabName);
};
// Auto-show credentials modal for new/pending hosts
useEffect(() => {
if (host && host.status === "pending") {
setShowCredentialsModal(true);
}
}, [host]);
// Sync notes state with host data
useEffect(() => {
if (host) {
setNotes(host.notes || "");
}
}, [host]);
const deleteHostMutation = useMutation({
mutationFn: (hostId) => adminHostsAPI.delete(hostId),
onSuccess: () => {
queryClient.invalidateQueries(["hosts"]);
navigate("/hosts");
},
});
// Toggle agent auto-update mutation (updates PatchMon agent script, not system packages)
const toggleAutoUpdateMutation = useMutation({
mutationFn: (auto_update) =>
adminHostsAPI
.toggleAutoUpdate(hostId, auto_update)
.then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries(["host", hostId]);
queryClient.invalidateQueries(["hosts"]);
},
});
const updateFriendlyNameMutation = useMutation({
mutationFn: (friendlyName) =>
adminHostsAPI
.updateFriendlyName(hostId, friendlyName)
.then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries(["host", hostId]);
queryClient.invalidateQueries(["hosts"]);
},
});
const updateHostGroupsMutation = useMutation({
mutationFn: ({ hostId, groupIds }) =>
adminHostsAPI.updateGroups(hostId, groupIds).then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries(["host", hostId]);
queryClient.invalidateQueries(["hosts"]);
},
});
const updateNotesMutation = useMutation({
mutationFn: ({ hostId, notes }) =>
adminHostsAPI.updateNotes(hostId, notes).then((res) => res.data),
onSuccess: () => {
queryClient.invalidateQueries(["host", hostId]);
queryClient.invalidateQueries(["hosts"]);
setNotesMessage({ text: "Notes saved successfully!", type: "success" });
// Clear message after 3 seconds
setTimeout(() => setNotesMessage({ text: "", type: "" }), 3000);
},
onError: (error) => {
setNotesMessage({
text: error.response?.data?.error || "Failed to save notes",
type: "error",
});
// Clear message after 5 seconds for errors
setTimeout(() => setNotesMessage({ text: "", type: "" }), 5000);
},
});
const handleDeleteHost = async () => {
if (
window.confirm(
`Are you sure you want to delete host "${host.friendly_name}"? This action cannot be undone.`,
)
) {
try {
await deleteHostMutation.mutateAsync(hostId);
} catch (error) {
console.error("Failed to delete host:", error);
alert("Failed to delete host");
}
}
};
if (isLoading) {
return (
);
}
if (error) {
return (
Error loading host
{error.message || "Failed to load host details"}
refetch()}
className="mt-2 btn-danger text-xs"
>
Try again
);
}
if (!host) {
return (
Host Not Found
The requested host could not be found.
);
}
const getStatusColor = (isStale, needsUpdate) => {
if (isStale) return "text-danger-600";
if (needsUpdate) return "text-warning-600";
return "text-success-600";
};
const getStatusIcon = (isStale, needsUpdate) => {
if (isStale) return ;
if (needsUpdate) return ;
return ;
};
const getStatusText = (isStale, needsUpdate) => {
if (isStale) return "Stale";
if (needsUpdate) return "Needs Updates";
return "Up to Date";
};
const isStale = Date.now() - new Date(host.last_update) > 24 * 60 * 60 * 1000;
return (
{/* Header */}
{/* Title row with friendly name, badge, and status */}
{host.friendly_name}
{wsStatus && (
{wsStatus.connected
? wsStatus.secure
? "WSS"
: "WS"
: "Offline"}
)}
0)}`}
>
{getStatusIcon(isStale, host.stats.outdated_packages > 0)}
{getStatusText(isStale, host.stats.outdated_packages > 0)}
{/* Info row with uptime and last updated */}
{host.system_uptime && (
Uptime:
{host.system_uptime}
)}
Last updated:
{formatRelativeTime(host.last_update)}
setShowCredentialsModal(true)}
className="btn-outline flex items-center gap-2 text-sm"
>
Deploy Agent
refetch()}
disabled={isFetching}
className="btn-outline flex items-center justify-center p-2 text-sm"
title="Refresh host data"
>
setShowDeleteModal(true)}
className="btn-danger flex items-center justify-center p-2 text-sm"
title="Delete host"
>
{/* Package Statistics Cards */}
navigate(`/packages?host=${hostId}`)}
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
title="View all packages for this host"
>
Total Installed
{host.stats.total_packages}
navigate(`/packages?host=${hostId}&filter=outdated`)}
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
title="View outdated packages for this host"
>
Outdated Packages
{host.stats.outdated_packages}
navigate(`/packages?host=${hostId}&filter=security`)}
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
title="View security packages for this host"
>
Security Updates
{host.stats.security_updates}
navigate(`/repositories?host=${hostId}`)}
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
title="View repositories for this host"
>
Repos
{isLoadingRepos ? "..." : repositories?.length || 0}
{/* Main Content - Full Width */}
{/* Host Info, Hardware, Network, System Info in Tabs */}
handleTabChange("host")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "host"
? "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"
}`}
>
Host Info
handleTabChange("network")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "network"
? "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"
}`}
>
Network
handleTabChange("system")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "system"
? "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"
}`}
>
System
handleTabChange("history")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "history"
? "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"
}`}
>
Package Reports
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
handleTabChange("notes")}
className={`px-4 py-2 text-sm font-medium ${
activeTab === "notes"
? "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"
}`}
>
Notes
{/* Host Information */}
{activeTab === "host" && (
Friendly Name
updateFriendlyNameMutation.mutate(newName)
}
placeholder="Enter friendly name..."
maxLength={100}
validate={(value) => {
if (!value.trim()) return "Friendly name is required";
if (value.trim().length < 1)
return "Friendly name must be at least 1 character";
if (value.trim().length > 100)
return "Friendly name must be less than 100 characters";
return null;
}}
className="w-full text-sm"
/>
{host.hostname && (
System Hostname
{host.hostname}
)}
{host.machine_id && (
Machine ID
{host.machine_id}
)}
Host Groups
{/* Extract group IDs from the new many-to-many structure */}
{(() => {
const groupIds =
host.host_group_memberships?.map(
(membership) => membership.host_groups.id,
) || [];
return (
updateHostGroupsMutation.mutate({
hostId: host.id,
groupIds: newGroupIds,
})
}
options={hostGroups || []}
placeholder="Select groups..."
className="w-full"
/>
);
})()}
Operating System
{host.os_type} {host.os_version}
Agent Version
{host.agent_version || "Unknown"}
Agent Auto-update
toggleAutoUpdateMutation.mutate(!host.auto_update)
}
disabled={toggleAutoUpdateMutation.isPending}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
host.auto_update
? "bg-primary-600 dark:bg-primary-500"
: "bg-secondary-200 dark:bg-secondary-600"
}`}
>
)}
{/* Network Information */}
{activeTab === "network" &&
(host.ip ||
host.gateway_ip ||
host.dns_servers ||
host.network_interfaces) && (
{host.ip && (
)}
{host.gateway_ip && (
Gateway IP
{host.gateway_ip}
)}
{host.dns_servers &&
Array.isArray(host.dns_servers) &&
host.dns_servers.length > 0 && (
DNS Servers
{host.dns_servers.map((dns) => (
{dns}
))}
)}
{host.network_interfaces &&
Array.isArray(host.network_interfaces) &&
host.network_interfaces.length > 0 && (
Network Interfaces
{host.network_interfaces.map((iface) => (
{iface.name}
))}
)}
)}
{/* System Information */}
{activeTab === "system" && (
{/* Basic System Information */}
{(host.kernel_version ||
host.selinux_status ||
host.architecture) && (
System Information
{host.architecture && (
Architecture
{host.architecture}
)}
{host.kernel_version && (
Kernel Version
{host.kernel_version}
)}
{/* Empty div to push SELinux status to the right */}
{host.selinux_status && (
SELinux Status
{host.selinux_status}
)}
)}
{/* Resource Information */}
{(host.system_uptime ||
host.cpu_model ||
host.cpu_cores ||
host.ram_installed ||
host.swap_size !== undefined ||
(host.load_average &&
Array.isArray(host.load_average) &&
host.load_average.length > 0 &&
host.load_average.some((load) => load != null)) ||
(host.disk_details &&
Array.isArray(host.disk_details) &&
host.disk_details.length > 0)) && (
Resource Information
{/* System Overview */}
{/* System Uptime */}
{host.system_uptime && (
)}
{/* CPU Model */}
{host.cpu_model && (
)}
{/* CPU Cores */}
{host.cpu_cores && (
)}
{/* RAM Installed */}
{host.ram_installed && (
)}
{/* Swap Size */}
{host.swap_size !== undefined &&
host.swap_size !== null && (
)}
{/* Load Average */}
{host.load_average &&
Array.isArray(host.load_average) &&
host.load_average.length > 0 &&
host.load_average.some((load) => load != null) && (
{host.load_average
.filter((load) => load != null)
.map((load, index) => (
{typeof load === "number"
? load.toFixed(2)
: String(load)}
{index <
host.load_average.filter(
(load) => load != null,
).length -
1 && ", "}
))}
)}
{/* Disk Information */}
{host.disk_details &&
Array.isArray(host.disk_details) &&
host.disk_details.length > 0 && (
Disk Usage
{host.disk_details.map((disk, index) => (
{disk.name || `Disk ${index + 1}`}
{disk.size && (
Size: {disk.size}
)}
{disk.mountpoint && (
Mount: {disk.mountpoint}
)}
{disk.usage &&
typeof disk.usage === "number" && (
)}
))}
)}
)}
{/* No Data State */}
{!host.kernel_version &&
!host.selinux_status &&
!host.architecture &&
!host.system_uptime &&
!host.cpu_model &&
!host.cpu_cores &&
!host.ram_installed &&
host.swap_size === undefined &&
(!host.load_average ||
!Array.isArray(host.load_average) ||
host.load_average.length === 0 ||
!host.load_average.some((load) => load != null)) &&
(!host.disk_details ||
!Array.isArray(host.disk_details) ||
host.disk_details.length === 0) && (
No system information available
System information will appear once the agent collects
data from this host
)}
)}
{activeTab === "network" &&
!(
host.ip ||
host.gateway_ip ||
host.dns_servers ||
host.network_interfaces
) && (
No network information available
)}
{/* Update History */}
{activeTab === "history" && (
{host.update_history?.length > 0 ? (
<>
Status
Date
Total Packages
Outdated Packages
Security
Payload (KB)
Exec Time (s)
{host.update_history.map((update) => (
{update.status === "success"
? "Success"
: "Failed"}
{formatDate(update.timestamp)}
{update.total_packages || "-"}
{update.packages_count}
{update.security_count > 0 ? (
{update.security_count}
) : (
-
)}
{update.payload_size_kb
? `${update.payload_size_kb.toFixed(2)}`
: "-"}
{update.execution_time
? `${update.execution_time.toFixed(2)}`
: "-"}
))}
{/* Pagination Controls */}
{host.pagination &&
host.pagination.total > historyLimit && (
Showing {historyPage * historyLimit + 1} to{" "}
{Math.min(
(historyPage + 1) * historyLimit,
host.pagination.total,
)}{" "}
of {host.pagination.total} entries
setHistoryPage(0)}
disabled={historyPage === 0}
className="px-3 py-1 text-xs font-medium text-secondary-600 dark:text-secondary-300 hover:text-secondary-800 dark:hover:text-secondary-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
First
setHistoryPage(historyPage - 1)}
disabled={historyPage === 0}
className="px-3 py-1 text-xs font-medium text-secondary-600 dark:text-secondary-300 hover:text-secondary-800 dark:hover:text-secondary-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
Page {historyPage + 1} of{" "}
{Math.ceil(host.pagination.total / historyLimit)}
setHistoryPage(historyPage + 1)}
disabled={!host.pagination.hasMore}
className="px-3 py-1 text-xs font-medium text-secondary-600 dark:text-secondary-300 hover:text-secondary-800 dark:hover:text-secondary-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
setHistoryPage(
Math.ceil(
host.pagination.total / historyLimit,
) - 1,
)
}
disabled={!host.pagination.hasMore}
className="px-3 py-1 text-xs font-medium text-secondary-600 dark:text-secondary-300 hover:text-secondary-800 dark:hover:text-secondary-100 disabled:opacity-50 disabled:cursor-not-allowed"
>
Last
)}
>
) : (
No update history available
)}
)}
{/* Notes */}
{activeTab === "notes" && (
Host Notes
{/* Success/Error Message */}
{notesMessage.text && (
{notesMessage.type === "success" ? (
) : (
)}
)}
)}
{/* Agent Queue */}
{activeTab === "queue" &&
}
{/* Credentials Modal */}
{showCredentialsModal && (
setShowCredentialsModal(false)}
/>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
setShowDeleteModal(false)}
onConfirm={handleDeleteHost}
isLoading={deleteHostMutation.isPending}
/>
)}
);
};
// Credentials Modal Component
const CredentialsModal = ({ host, isOpen, onClose }) => {
const [showApiKey, setShowApiKey] = useState(false);
const [activeTab, setActiveTab] = useState("quick-install");
const [forceInstall, setForceInstall] = useState(false);
const [architecture, setArchitecture] = useState("amd64");
const apiIdInputId = useId();
const apiKeyInputId = useId();
const architectureSelectId = useId();
const { data: serverUrlData } = useQuery({
queryKey: ["serverUrl"],
queryFn: () => settingsAPI.getServerUrl().then((res) => res.data),
});
const serverUrl = serverUrlData?.server_url || "http://localhost:3001";
// Fetch settings for dynamic curl flags (local to modal)
const { data: settings } = useQuery({
queryKey: ["settings"],
queryFn: () => settingsAPI.get().then((res) => res.data),
});
// Helper function to get curl flags based on settings
const getCurlFlags = () => {
return settings?.ignore_ssl_self_signed ? "-sk" : "-s";
};
// Helper function to build installation URL with optional force flag and architecture
const getInstallUrl = () => {
const baseUrl = `${serverUrl}/api/v1/hosts/install`;
const params = new URLSearchParams();
if (forceInstall) params.append("force", "true");
params.append("arch", architecture);
return `${baseUrl}?${params.toString()}`;
};
const copyToClipboard = async (text) => {
try {
// Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return;
}
// Fallback for older browsers or non-secure contexts
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand("copy");
if (!successful) {
throw new Error("Copy command failed");
}
} catch {
// If all else fails, show the text in a prompt
prompt("Copy this command:", text);
} finally {
document.body.removeChild(textArea);
}
} catch (err) {
console.error("Failed to copy to clipboard:", err);
// Show the text in a prompt as last resort
prompt("Copy this command:", text);
}
};
if (!isOpen || !host) return null;
return (
Host Setup - {host.friendly_name}
{/* Tabs */}
setActiveTab("quick-install")}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === "quick-install"
? "border-primary-500 text-primary-600 dark:text-primary-400"
: "border-transparent text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300 hover:border-secondary-300 dark:hover:border-secondary-500"
}`}
>
Quick Install
setActiveTab("credentials")}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === "credentials"
? "border-primary-500 text-primary-600 dark:text-primary-400"
: "border-transparent text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300 hover:border-secondary-300 dark:hover:border-secondary-500"
}`}
>
API Credentials
{/* Tab Content */}
{activeTab === "quick-install" && (
One-Line Installation
Copy and run this command on the target host to securely install
and configure the PatchMon agent:
{/* Force Install Toggle */}
{/* Architecture Selection */}
Target Architecture
setArchitecture(e.target.value)}
className="px-3 py-2 border border-primary-300 dark:border-primary-600 rounded-md bg-white dark:bg-secondary-800 text-sm text-secondary-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
>
AMD64 (x86_64) - Default
386 (i386) - 32-bit
ARM64 (aarch64) - ARM
Select the architecture of the target host
copyToClipboard(
`curl ${getCurlFlags()} ${getInstallUrl()} -H "X-API-ID: ${host.api_id}" -H "X-API-KEY: ${host.api_key}" | bash`,
)
}
className="btn-primary flex items-center gap-1"
>
Copy
Manual Installation
If you prefer to install manually, follow these steps:
)}
{activeTab === "credentials" && (
Security Notice
Keep these credentials secure. They provide full access to
this host's monitoring data.
)}
Close
);
};
// Delete Confirmation Modal Component
const DeleteConfirmationModal = ({
host,
isOpen,
onClose,
onConfirm,
isLoading,
}) => {
if (!isOpen || !host) return null;
return (
Delete Host
This action cannot be undone
Are you sure you want to delete the host{" "}
"{host.friendly_name}" ?
Warning: This will permanently remove the host
and all its associated data, including package information and
update history.
Cancel
{isLoading ? "Deleting..." : "Delete Host"}
);
};
// 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 (
);
}
if (error) {
return (
Failed to load queue data
refetch()}
className="mt-2 px-4 py-2 text-sm bg-primary-600 text-white rounded-md hover:bg-primary-700"
>
Retry
);
}
const { waiting, active, delayed, failed, jobHistory } = queueData.data;
const getStatusIcon = (status) => {
switch (status) {
case "completed":
return ;
case "failed":
return ;
case "active":
return ;
default:
return ;
}
};
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 (
Live Agent Queue Status
refetch()}
className="btn-outline flex items-center gap-2"
title="Refresh queue data"
>
{/* Queue Summary */}
{/* Job History */}
{jobHistory.length === 0 ? (
) : (
Job ID
Job Name
Status
Attempt
Date/Time
Error/Output
{jobHistory.map((job) => (
{job.job_id}
{formatJobType(job.job_name)}
{getStatusIcon(job.status)}
{job.status.charAt(0).toUpperCase() +
job.status.slice(1)}
{job.attempt_number}
{new Date(job.created_at).toLocaleString()}
{job.error_message ? (
{job.error_message}
) : job.output ? (
{JSON.stringify(job.output)}
) : (
-
)}
))}
)}
);
};
export default HostDetail;