Added Total Packages in the Agent history

Added Script execution time in the Agent history tab
Added Pagination for the agent History
This commit is contained in:
Muhammad Ibrahim
2025-10-07 21:46:37 +01:00
parent 831adf3038
commit cdb24520d8
8 changed files with 760 additions and 661 deletions

View File

@@ -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 <<EOF
{
@@ -935,7 +942,8 @@ send_update() {
"ip": "$IP_ADDRESS",
"architecture": "$ARCHITECTURE",
"agentVersion": "$AGENT_VERSION",
"machineId": "$machine_id"
"machineId": "$machine_id",
"executionTime": $execution_time
}
EOF
)

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "update_history" ADD COLUMN "total_packages" INTEGER;

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "update_history" ADD COLUMN "payload_size_kb" DOUBLE PRECISION;
ALTER TABLE "update_history" ADD COLUMN "execution_time" DOUBLE PRECISION;

View File

@@ -185,6 +185,9 @@ model update_history {
host_id String
packages_count Int
security_count Int
total_packages Int?
payload_size_kb Float?
execution_time Float?
timestamp DateTime @default(now())
status String @default("success")
error_message String?

View File

@@ -347,7 +347,11 @@ router.get(
try {
const { hostId } = req.params;
const host = await prisma.hosts.findUnique({
const limit = parseInt(req.query.limit) || 10;
const offset = parseInt(req.query.offset) || 0;
const [host, totalHistoryCount] = await Promise.all([
prisma.hosts.findUnique({
where: { id: hostId },
include: {
host_groups: {
@@ -369,10 +373,15 @@ router.get(
orderBy: {
timestamp: "desc",
},
take: 10,
take: limit,
skip: offset,
},
},
});
}),
prisma.update_history.count({
where: { host_id: hostId },
}),
]);
if (!host) {
return res.status(404).json({ error: "Host not found" });
@@ -388,6 +397,12 @@ router.get(
(hp) => hp.needs_update && hp.is_security_update,
).length,
},
pagination: {
total: totalHistoryCount,
limit,
offset,
hasMore: offset + limit < totalHistoryCount,
},
};
res.json(hostWithStats);

View File

@@ -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",
},
});

View File

@@ -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,10 +292,68 @@ const HostDetail = () => {
</div>
</div>
{/* Main Content Grid */}
<div className="flex-1 grid grid-cols-12 gap-4 overflow-hidden">
{/* Left Column - System Details with Tabs */}
<div className="col-span-12 lg:col-span-7 flex flex-col gap-4 overflow-hidden">
{/* Package Statistics Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<button
type="button"
onClick={() => 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"
>
<div className="flex items-center">
<Package className="h-5 w-5 text-primary-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">
Total Installed
</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
{host.stats.total_packages}
</p>
</div>
</div>
</button>
<button
type="button"
onClick={() => 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"
>
<div className="flex items-center">
<Clock className="h-5 w-5 text-warning-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">
Outdated Packages
</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
{host.stats.outdated_packages}
</p>
</div>
</div>
</button>
<button
type="button"
onClick={() => 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"
>
<div className="flex items-center">
<Shield className="h-5 w-5 text-danger-600 mr-2" />
<div>
<p className="text-sm text-secondary-500 dark:text-white">
Security Updates
</p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
{host.stats.security_updates}
</p>
</div>
</div>
</button>
</div>
{/* Main Content - Full Width */}
<div className="flex-1 overflow-hidden">
{/* Host Info, Hardware, Network, System Info in Tabs */}
<div className="card">
<div className="flex border-b border-secondary-200 dark:border-secondary-600">
@@ -830,9 +895,10 @@ const HostDetail = () => {
{/* Update History */}
{activeTab === "history" && (
<div className="overflow-x-auto">
<div className="space-y-4">
{host.update_history?.length > 0 ? (
<>
<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>
@@ -843,18 +909,24 @@ const HostDetail = () => {
Date
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Packages
Total Packages
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Outdated Packages
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Security
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Payload (KB)
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
Exec Time (s)
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
{(showAllUpdates
? host.update_history
: host.update_history.slice(0, 5)
).map((update) => (
{host.update_history.map((update) => (
<tr
key={update.id}
className="hover:bg-secondary-50 dark:hover:bg-secondary-700"
@@ -880,6 +952,9 @@ const HostDetail = () => {
<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white">
{formatDate(update.timestamp)}
</td>
<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white">
{update.total_packages || "-"}
</td>
<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white">
{update.packages_count}
</td>
@@ -897,30 +972,80 @@ const HostDetail = () => {
</span>
)}
</td>
<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white">
{update.payload_size_kb
? `${update.payload_size_kb.toFixed(2)}`
: "-"}
</td>
<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white">
{update.execution_time
? `${update.execution_time.toFixed(2)}`
: "-"}
</td>
</tr>
))}
</tbody>
</table>
</div>
{host.update_history.length > 5 && (
<div className="px-4 py-2 border-t border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700">
{/* Pagination Controls */}
{host.pagination &&
host.pagination.total > historyLimit && (
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700">
<div className="flex items-center gap-2 text-sm text-secondary-600 dark:text-secondary-300">
<span>
Showing {historyPage * historyLimit + 1} to{" "}
{Math.min(
(historyPage + 1) * historyLimit,
host.pagination.total,
)}{" "}
of {host.pagination.total} entries
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShowAllUpdates(!showAllUpdates)}
className="flex items-center gap-1.5 text-xs text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium"
onClick={() => 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"
>
{showAllUpdates ? (
<>
<ChevronUp className="h-3 w-3" />
Show Less
</>
) : (
<>
<ChevronDown className="h-3 w-3" />
Show All ({host.update_history.length} total)
</>
)}
First
</button>
<button
type="button"
onClick={() => 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
</button>
<span className="px-3 py-1 text-xs font-medium text-secondary-900 dark:text-white">
Page {historyPage + 1} of{" "}
{Math.ceil(host.pagination.total / historyLimit)}
</span>
<button
type="button"
onClick={() => 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
</button>
<button
type="button"
onClick={() =>
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
</button>
</div>
</div>
)}
</>
@@ -988,77 +1113,6 @@ const HostDetail = () => {
</div>
</div>
{/* Right Column - Package Statistics */}
<div className="col-span-12 lg:col-span-5 flex flex-col gap-4">
{/* Package Statistics */}
<div className="card">
<div className="px-4 py-2.5 border-b border-secondary-200 dark:border-secondary-600">
<h3 className="text-sm font-medium text-secondary-500 dark:text-secondary-400">
Package Statistics
</h3>
</div>
<div className="p-4">
<div className="grid grid-cols-3 gap-4">
<button
type="button"
onClick={() => navigate(`/packages?host=${hostId}`)}
className="text-center p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors group"
title="View all packages for this host"
>
<div className="flex items-center justify-center w-12 h-12 bg-primary-100 dark:bg-primary-800 rounded-lg mx-auto mb-2 group-hover:bg-primary-200 dark:group-hover:bg-primary-700 transition-colors">
<Package className="h-6 w-6 text-primary-600 dark:text-primary-400" />
</div>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{host.stats.total_packages}
</p>
<p className="text-sm text-secondary-500 dark:text-secondary-300">
Total Installed
</p>
</button>
<button
type="button"
onClick={() =>
navigate(`/packages?host=${hostId}&filter=outdated`)
}
className="text-center p-4 bg-warning-50 dark:bg-warning-900/20 rounded-lg hover:bg-warning-100 dark:hover:bg-warning-900/30 transition-colors group"
title="View outdated packages for this host"
>
<div className="flex items-center justify-center w-12 h-12 bg-warning-100 dark:bg-warning-800 rounded-lg mx-auto mb-2 group-hover:bg-warning-200 dark:group-hover:bg-warning-700 transition-colors">
<Clock className="h-6 w-6 text-warning-600 dark:text-warning-400" />
</div>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{host.stats.outdated_packages}
</p>
<p className="text-sm text-secondary-500 dark:text-secondary-300">
Outdated Packages
</p>
</button>
<button
type="button"
onClick={() =>
navigate(`/packages?host=${hostId}&filter=security`)
}
className="text-center p-4 bg-danger-50 dark:bg-danger-900/20 rounded-lg hover:bg-danger-100 dark:hover:bg-danger-900/30 transition-colors group"
title="View security packages for this host"
>
<div className="flex items-center justify-center w-12 h-12 bg-danger-100 dark:bg-danger-800 rounded-lg mx-auto mb-2 group-hover:bg-danger-200 dark:group-hover:bg-danger-700 transition-colors">
<Shield className="h-6 w-6 text-danger-600 dark:text-danger-400" />
</div>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{host.stats.security_updates}
</p>
<p className="text-sm text-secondary-500 dark:text-secondary-300">
Security Updates
</p>
</button>
</div>
</div>
</div>
</div>
</div>
{/* Credentials Modal */}
{showCredentialsModal && (
<CredentialsModal

View File

@@ -51,7 +51,11 @@ export const dashboardAPI = {
getStats: () => api.get("/dashboard/stats"),
getHosts: () => api.get("/dashboard/hosts"),
getPackages: () => api.get("/dashboard/packages"),
getHostDetail: (hostId) => api.get(`/dashboard/hosts/${hostId}`),
getHostDetail: (hostId, params = {}) => {
const queryString = new URLSearchParams(params).toString();
const url = `/dashboard/hosts/${hostId}${queryString ? `?${queryString}` : ""}`;
return api.get(url);
},
getRecentUsers: () => api.get("/dashboard/recent-users"),
getRecentCollection: () => api.get("/dashboard/recent-collection"),
};