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"}

); } 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)}
{/* Package Statistics Cards */}
{/* Main Content - Full Width */}
{/* Host Info, Hardware, Network, System Info in Tabs */}
{/* 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

)} {/* Network Information */} {activeTab === "network" && (host.ip || host.gateway_ip || host.dns_servers || host.network_interfaces) && (
{host.ip && (

IP Address

{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 && (

System Uptime

{host.system_uptime}

)} {/* CPU Model */} {host.cpu_model && (

CPU Model

{host.cpu_model}

)} {/* CPU Cores */} {host.cpu_cores && (

CPU Cores

{host.cpu_cores}

)} {/* RAM Installed */} {host.ram_installed && (

RAM Installed

{host.ram_installed} GB

)} {/* Swap Size */} {host.swap_size !== undefined && host.swap_size !== null && (

Swap Size

{host.swap_size} GB

)} {/* Load Average */} {host.load_average && Array.isArray(host.load_average) && host.load_average.length > 0 && host.load_average.some((load) => load != null) && (

Load Average

{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" && (
Usage {disk.usage}%
)}
))}
)}
)} {/* 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 ? ( <>
{host.update_history.map((update) => ( ))}
Status Date Total Packages Outdated Packages Security Payload (KB) Exec Time (s)
{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
Page {historyPage + 1} of{" "} {Math.ceil(host.pagination.total / historyLimit)}
)} ) : (

No update history available

)}
)} {/* Notes */} {activeTab === "notes" && (

Host Notes

{/* Success/Error Message */} {notesMessage.text && (
{notesMessage.type === "success" ? ( ) : ( )}

{notesMessage.text}

)}