mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-24 08:33:38 +00:00
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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "update_history" ADD COLUMN "total_packages" INTEGER;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user