From cdb24520d806d231a7f26d47da54b5465138fcf1 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Tue, 7 Oct 2025 21:46:37 +0100 Subject: [PATCH] Added Total Packages in the Agent history Added Script execution time in the Agent history tab Added Pagination for the agent History --- agents/patchmon-agent.sh | 14 +- .../migration.sql | 3 + .../migration.sql | 4 + backend/prisma/schema.prisma | 19 +- backend/src/routes/dashboardRoutes.js | 63 +- backend/src/routes/hostRoutes.js | 10 +- frontend/src/pages/HostDetail.jsx | 1302 +++++++++-------- frontend/src/utils/api.js | 6 +- 8 files changed, 760 insertions(+), 661 deletions(-) create mode 100644 backend/prisma/migrations/20251007000000_add_total_packages_to_history/migration.sql create mode 100644 backend/prisma/migrations/20251007000001_add_performance_metrics_to_history/migration.sql diff --git a/agents/patchmon-agent.sh b/agents/patchmon-agent.sh index 038cd10..53b3c2a 100755 --- a/agents/patchmon-agent.sh +++ b/agents/patchmon-agent.sh @@ -1,12 +1,12 @@ #!/bin/bash -# PatchMon Agent Script v1.2.7 +# PatchMon Agent Script v1.2.8 # This script sends package update information to the PatchMon server using API credentials # Configuration PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}" API_VERSION="v1" -AGENT_VERSION="1.2.7" +AGENT_VERSION="1.2.8" CONFIG_FILE="/etc/patchmon/agent.conf" CREDENTIALS_FILE="/etc/patchmon/credentials" LOG_FILE="/var/log/patchmon-agent.log" @@ -896,6 +896,9 @@ get_system_info() { send_update() { load_credentials + # Track execution start time + local start_time=$(date +%s.%N) + # Verify datetime before proceeding if ! verify_datetime; then warning "Datetime verification failed, but continuing with update..." @@ -924,6 +927,10 @@ send_update() { # Get machine ID local machine_id=$(get_machine_id) + # Calculate execution time (in seconds with decimals) + local end_time=$(date +%s.%N) + local execution_time=$(echo "$end_time - $start_time" | bc) + # Create the base payload and merge with system info local base_payload=$(cat < hp.needs_update && hp.is_security_update, ).length, }, + pagination: { + total: totalHistoryCount, + limit, + offset, + hasMore: offset + limit < totalHistoryCount, + }, }; res.json(hostWithStats); diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 5a076a5..b69e224 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -325,9 +325,13 @@ router.post( return res.status(400).json({ errors: errors.array() }); } - const { packages, repositories } = req.body; + const { packages, repositories, executionTime } = req.body; const host = req.hostRecord; + // Calculate payload size in KB + const payloadSizeBytes = JSON.stringify(req.body).length; + const payloadSizeKb = payloadSizeBytes / 1024; + // Update host last update timestamp and system info if provided const updateData = { last_update: new Date(), @@ -383,6 +387,7 @@ router.post( (pkg) => pkg.isSecurityUpdate, ).length; const updatesCount = packages.filter((pkg) => pkg.needsUpdate).length; + const totalPackages = packages.length; // Process everything in a single transaction to avoid race conditions await prisma.$transaction(async (tx) => { @@ -525,6 +530,9 @@ router.post( host_id: host.id, packages_count: updatesCount, security_count: securityCount, + total_packages: totalPackages, + payload_size_kb: payloadSizeKb, + execution_time: executionTime ? parseFloat(executionTime) : null, status: "success", }, }); diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 7d2e0e1..dd5aca5 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -43,9 +43,10 @@ const HostDetail = () => { const queryClient = useQueryClient(); const [showCredentialsModal, setShowCredentialsModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showAllUpdates, setShowAllUpdates] = useState(false); const [activeTab, setActiveTab] = useState("host"); const [_forceInstall, _setForceInstall] = useState(false); + const [historyPage, setHistoryPage] = useState(0); + const [historyLimit] = useState(10); const { data: host, @@ -54,8 +55,14 @@ const HostDetail = () => { refetch, isFetching, } = useQuery({ - queryKey: ["host", hostId], - queryFn: () => dashboardAPI.getHostDetail(hostId).then((res) => res.data), + 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 }); @@ -285,554 +292,613 @@ const HostDetail = () => { - {/* Main Content Grid */} -
- {/* Left Column - System Details with Tabs */} -
- {/* Host Info, Hardware, Network, System Info in Tabs */} -
-
- - - - - + {/* Package Statistics Cards */} +
+ -
- {/* Host Information */} - {activeTab === "host" && ( -
-
+ + + +
+ + {/* 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 && (

- Friendly Name + System Hostname +

+

+ {host.hostname}

- - 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 && ( + {host.machine_id && ( +
+

+ Machine ID +

+

+ {host.machine_id} +

+
+ )} + +
+

+ Host Group +

+ {host.host_groups ? ( + + {host.host_groups.name} + + ) : ( + + Ungrouped + + )} +
+ +
+

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

- System Hostname +

+ IP Address

- {host.hostname} + {host.ip}

)} - {host.machine_id && ( + {host.gateway_ip && (
-

- Machine ID +

+ Gateway IP

-

- {host.machine_id} +

+ {host.gateway_ip}

)} -
-

- Host Group -

- {host.host_groups ? ( - - {host.host_groups.name} - - ) : ( - - Ungrouped - + {host.dns_servers && + Array.isArray(host.dns_servers) && + host.dns_servers.length > 0 && ( +
+

+ DNS Servers +

+
+ {host.dns_servers.map((dns) => ( +

+ {dns} +

+ ))} +
+
)} -
-
-

- Operating System -

-
- -

- {host.os_type} {host.os_version} -

-
-
- -
-

- Agent Version -

-

- {host.agent_version || "Unknown"} -

-
- -
-

- Agent Auto-update -

- -
+ {host.network_interfaces && + Array.isArray(host.network_interfaces) && + host.network_interfaces.length > 0 && ( +
+

+ Network Interfaces +

+
+ {host.network_interfaces.map((iface) => ( +

+ {iface.name} +

+ ))} +
+
+ )}
)} - {/* Network Information */} - {activeTab === "network" && - (host.ip || - host.gateway_ip || - host.dns_servers || - host.network_interfaces) && ( -
-
- {host.ip && ( + {/* System Information */} + {activeTab === "system" && ( +
+ {/* Basic System Information */} + {(host.kernel_version || + host.selinux_status || + host.architecture) && ( +
+

+ + System Information +

+
+ {host.architecture && (

- IP Address + Architecture

-

- {host.ip} +

+ {host.architecture}

)} - {host.gateway_ip && ( + {host.kernel_version && (

- Gateway IP + Kernel Version

- {host.gateway_ip} + {host.kernel_version}

)} - {host.dns_servers && - Array.isArray(host.dns_servers) && - host.dns_servers.length > 0 && ( -
-

- DNS Servers -

-
- {host.dns_servers.map((dns) => ( -

- {dns} -

- ))} -
-
- )} + {/* Empty div to push SELinux status to the right */} +
- {host.network_interfaces && - Array.isArray(host.network_interfaces) && - host.network_interfaces.length > 0 && ( -
-

- Network Interfaces -

-
- {host.network_interfaces.map((iface) => ( -

- {iface.name} -

- ))} -
-
- )} + {host.selinux_status && ( +
+

+ SELinux Status +

+ + {host.selinux_status} + +
+ )}
)} - {/* System Information */} - {activeTab === "system" && ( -
- {/* Basic System Information */} - {(host.kernel_version || - host.selinux_status || - host.architecture) && ( -
-

- - System Information -

-
- {host.architecture && ( -
+ {/* 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 && ( +
+
+

- Architecture -

-

- {host.architecture} + System Uptime

- )} +

+ {host.system_uptime} +

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

- Kernel Version -

-

- {host.kernel_version} + CPU Model

- )} +

+ {host.cpu_model} +

+
+ )} - {/* Empty div to push SELinux status to the right */} -
- - {host.selinux_status && ( -
+ {/* CPU Cores */} + {host.cpu_cores && ( +
+
+

- 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 Cores

- )} +

+ {host.cpu_cores} +

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

- CPU Model -

-
-

- {host.cpu_model} + {/* RAM Installed */} + {host.ram_installed && ( +

+
+ +

+ RAM Installed

- )} +

+ {host.ram_installed} GB +

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

- CPU Cores -

-
-

- {host.cpu_cores} -

-
- )} - - {/* RAM Installed */} - {host.ram_installed && ( + {/* Swap Size */} + {host.swap_size !== undefined && + host.swap_size !== null && (

- RAM Installed + Swap Size

- {host.ram_installed} GB + {host.swap_size} 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

- )} - - {/* 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}% -
-
-
-
-
- )} -
- ))} -
+

+ {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 && ", "} + + ))} +

)}
- )} - {/* 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 -

+ {/* 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}% +
+
+
+
+
+ )} +
+ ))} +
+
+ )}
)} - {/* Update History */} - {activeTab === "history" && ( -
- {host.update_history?.length > 0 ? ( - <> + {/* 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 ? ( + <> +
@@ -843,18 +909,24 @@ const HostDetail = () => { Date + + + - {(showAllUpdates - ? host.update_history - : host.update_history.slice(0, 5) - ).map((update) => ( + {host.update_history.map((update) => ( { + @@ -897,164 +972,143 @@ const HostDetail = () => { )} + + ))}
- Packages + Total Packages + + Outdated Packages Security + Payload (KB) + + Exec Time (s) +
{formatDate(update.timestamp)} + {update.total_packages || "-"} + {update.packages_count} + {update.payload_size_kb + ? `${update.payload_size_kb.toFixed(2)}` + : "-"} + + {update.execution_time + ? `${update.execution_time.toFixed(2)}` + : "-"} +
+
- {host.update_history.length > 5 && ( -
- + {/* 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 -

+ + ) : ( +
+ +

+ No update history available +

-
-