Fixed issues with the agent not sending apt data properly

Added Indexing to the database for faster and efficient searching
Fixed some filtering from the hosts page relating to packages that need updating
Added buy me a coffee link (sorry and thank you <3)
This commit is contained in:
Muhammad Ibrahim
2025-10-07 20:52:46 +01:00
parent 840779844a
commit a305fe23d3
8 changed files with 197 additions and 42 deletions

View File

@@ -605,8 +605,24 @@ get_apt_packages() {
local -n packages_ref=$1 local -n packages_ref=$1
local -n first_ref=$2 local -n first_ref=$2
# Update package lists (use apt-get for older distros; quieter output) # Update package lists with retry logic for lock conflicts
apt-get update -qq local retry_count=0
local max_retries=3
local retry_delay=5
while [[ $retry_count -lt $max_retries ]]; do
if apt-get update -qq 2>/dev/null; then
break
else
retry_count=$((retry_count + 1))
if [[ $retry_count -lt $max_retries ]]; then
warning "APT lock detected, retrying in ${retry_delay} seconds... (attempt $retry_count/$max_retries)"
sleep $retry_delay
else
warning "APT lock persists after $max_retries attempts, continuing without update..."
fi
fi
done
# Determine upgradable packages using apt-get simulation (compatible with Ubuntu 18.04) # Determine upgradable packages using apt-get simulation (compatible with Ubuntu 18.04)
# Example line format: # Example line format:
@@ -626,6 +642,11 @@ get_apt_packages() {
is_security_update=true is_security_update=true
fi fi
# Escape JSON special characters in package data
package_name=$(echo "$package_name" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g')
current_version=$(echo "$current_version" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g')
available_version=$(echo "$available_version" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g')
if [[ "$first_ref" == true ]]; then if [[ "$first_ref" == true ]]; then
first_ref=false first_ref=false
else else
@@ -642,7 +663,11 @@ get_apt_packages() {
while IFS=' ' read -r package_name version; do while IFS=' ' read -r package_name version; do
if [[ -n "$package_name" && -n "$version" ]]; then if [[ -n "$package_name" && -n "$version" ]]; then
# Check if this package is not in the upgrade list # Check if this package is not in the upgrade list
if ! echo "$upgradable" | grep -q "^$package_name/"; then if ! echo "$upgradable_sim" | grep -q "^Inst $package_name "; then
# Escape JSON special characters in package data
package_name=$(echo "$package_name" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g')
version=$(echo "$version" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g')
if [[ "$first_ref" == true ]]; then if [[ "$first_ref" == true ]]; then
first_ref=false first_ref=false
else else
@@ -883,6 +908,15 @@ send_update() {
local network_json=$(get_network_info) local network_json=$(get_network_info)
local system_json=$(get_system_info) local system_json=$(get_system_info)
# Validate JSON before sending
if ! echo "$packages_json" | jq empty 2>/dev/null; then
error "Invalid packages JSON generated: $packages_json"
fi
if ! echo "$repositories_json" | jq empty 2>/dev/null; then
error "Invalid repositories JSON generated: $repositories_json"
fi
info "Sending update to PatchMon server..." info "Sending update to PatchMon server..."
# Merge all JSON objects into one # Merge all JSON objects into one
@@ -909,15 +943,27 @@ EOF
# Merge the base payload with the system information # Merge the base payload with the system information
local payload=$(echo "$base_payload $merged_json" | jq -s '.[0] * .[1]') local payload=$(echo "$base_payload $merged_json" | jq -s '.[0] * .[1]')
# Write payload to temporary file to avoid "Argument list too long" error
local temp_payload_file=$(mktemp)
echo "$payload" > "$temp_payload_file"
# Debug: Show payload size
local payload_size=$(wc -c < "$temp_payload_file")
echo -e "${BLUE} 📊 Payload size: $payload_size bytes${NC}"
local response=$(curl $CURL_FLAGS -X POST \ local response=$(curl $CURL_FLAGS -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "X-API-ID: $API_ID" \ -H "X-API-ID: $API_ID" \
-H "X-API-KEY: $API_KEY" \ -H "X-API-KEY: $API_KEY" \
-d "$payload" \ -d @"$temp_payload_file" \
"$PATCHMON_SERVER/api/$API_VERSION/hosts/update") "$PATCHMON_SERVER/api/$API_VERSION/hosts/update" 2>&1)
if [[ $? -eq 0 ]]; then local curl_exit_code=$?
# Clean up temporary file
rm -f "$temp_payload_file"
if [[ $curl_exit_code -eq 0 ]]; then
if echo "$response" | grep -q "success"; then if echo "$response" | grep -q "success"; then
local packages_count=$(echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2) local packages_count=$(echo "$response" | grep -o '"packagesProcessed":[0-9]*' | cut -d':' -f2)
success "Update sent successfully (${packages_count} packages processed)" success "Update sent successfully (${packages_count} packages processed)"
@@ -953,7 +999,7 @@ EOF
error "Update failed: $response" error "Update failed: $response"
fi fi
else else
error "Failed to send update" error "Failed to send update (curl exit code: $curl_exit_code): $response"
fi fi
} }

View File

@@ -0,0 +1,30 @@
-- Add indexes to host_packages table for performance optimization
-- These indexes will dramatically speed up queries filtering by host_id, package_id, needs_update, and is_security_update
-- Index for queries filtering by host_id (very common - used when viewing packages for a specific host)
CREATE INDEX IF NOT EXISTS "host_packages_host_id_idx" ON "host_packages"("host_id");
-- Index for queries filtering by package_id (used when finding hosts for a specific package)
CREATE INDEX IF NOT EXISTS "host_packages_package_id_idx" ON "host_packages"("package_id");
-- Index for queries filtering by needs_update (used when finding outdated packages)
CREATE INDEX IF NOT EXISTS "host_packages_needs_update_idx" ON "host_packages"("needs_update");
-- Index for queries filtering by is_security_update (used when finding security updates)
CREATE INDEX IF NOT EXISTS "host_packages_is_security_update_idx" ON "host_packages"("is_security_update");
-- Composite index for the most common query pattern: host_id + needs_update
-- This is optimal for "show me outdated packages for this host"
CREATE INDEX IF NOT EXISTS "host_packages_host_id_needs_update_idx" ON "host_packages"("host_id", "needs_update");
-- Composite index for host_id + needs_update + is_security_update
-- This is optimal for "show me security updates for this host"
CREATE INDEX IF NOT EXISTS "host_packages_host_id_needs_update_security_idx" ON "host_packages"("host_id", "needs_update", "is_security_update");
-- Index for queries filtering by package_id + needs_update
-- This is optimal for "show me hosts where this package needs updates"
CREATE INDEX IF NOT EXISTS "host_packages_package_id_needs_update_idx" ON "host_packages"("package_id", "needs_update");
-- Index on last_checked for cleanup/maintenance queries
CREATE INDEX IF NOT EXISTS "host_packages_last_checked_idx" ON "host_packages"("last_checked");

View File

@@ -44,6 +44,14 @@ model host_packages {
packages packages @relation(fields: [package_id], references: [id], onDelete: Cascade) packages packages @relation(fields: [package_id], references: [id], onDelete: Cascade)
@@unique([host_id, package_id]) @@unique([host_id, package_id])
@@index([host_id])
@@index([package_id])
@@index([needs_update])
@@index([is_security_update])
@@index([host_id, needs_update])
@@index([host_id, needs_update, is_security_update])
@@index([package_id, needs_update])
@@index([last_checked])
} }
model host_repositories { model host_repositories {
@@ -108,6 +116,9 @@ model packages {
created_at DateTime @default(now()) created_at DateTime @default(now())
updated_at DateTime updated_at DateTime
host_packages host_packages[] host_packages host_packages[]
@@index([name])
@@index([category])
} }
model repositories { model repositories {

View File

@@ -14,6 +14,7 @@ router.get("/", async (req, res) => {
category = "", category = "",
needsUpdate = "", needsUpdate = "",
isSecurityUpdate = "", isSecurityUpdate = "",
host = "",
} = req.query; } = req.query;
const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10); const skip = (parseInt(page, 10) - 1) * parseInt(limit, 10);
@@ -33,8 +34,27 @@ router.get("/", async (req, res) => {
: {}, : {},
// Category filter // Category filter
category ? { category: { equals: category } } : {}, category ? { category: { equals: category } } : {},
// Update status filters // Host filter - only return packages installed on the specified host
needsUpdate // Combined with update status filters if both are present
host
? {
host_packages: {
some: {
host_id: host,
// If needsUpdate or isSecurityUpdate filters are present, apply them here
...(needsUpdate
? { needs_update: needsUpdate === "true" }
: {}),
...(isSecurityUpdate
? { is_security_update: isSecurityUpdate === "true" }
: {}),
},
},
}
: {},
// Update status filters (only applied if no host filter)
// If host filter is present, these are already applied above
!host && needsUpdate
? { ? {
host_packages: { host_packages: {
some: { some: {
@@ -43,7 +63,7 @@ router.get("/", async (req, res) => {
}, },
} }
: {}, : {},
isSecurityUpdate !host && isSecurityUpdate
? { ? {
host_packages: { host_packages: {
some: { some: {
@@ -84,24 +104,32 @@ router.get("/", async (req, res) => {
// Get additional stats for each package // Get additional stats for each package
const packagesWithStats = await Promise.all( const packagesWithStats = await Promise.all(
packages.map(async (pkg) => { packages.map(async (pkg) => {
// Build base where clause for this package
const baseWhere = { package_id: pkg.id };
// If host filter is specified, add host filter to all queries
const hostWhere = host ? { ...baseWhere, host_id: host } : baseWhere;
const [updatesCount, securityCount, packageHosts] = await Promise.all([ const [updatesCount, securityCount, packageHosts] = await Promise.all([
prisma.host_packages.count({ prisma.host_packages.count({
where: { where: {
package_id: pkg.id, ...hostWhere,
needs_update: true, needs_update: true,
}, },
}), }),
prisma.host_packages.count({ prisma.host_packages.count({
where: { where: {
package_id: pkg.id, ...hostWhere,
needs_update: true, needs_update: true,
is_security_update: true, is_security_update: true,
}, },
}), }),
prisma.host_packages.findMany({ prisma.host_packages.findMany({
where: { where: {
package_id: pkg.id, ...hostWhere,
needs_update: true, // If host filter is specified, include all packages for that host
// Otherwise, only include packages that need updates
...(host ? {} : { needs_update: true }),
}, },
select: { select: {
hosts: { hosts: {
@@ -112,6 +140,10 @@ router.get("/", async (req, res) => {
os_type: true, os_type: true,
}, },
}, },
current_version: true,
available_version: true,
needs_update: true,
is_security_update: true,
}, },
take: 10, // Limit to first 10 for performance take: 10, // Limit to first 10 for performance
}), }),

File diff suppressed because one or more lines are too long

View File

@@ -1012,13 +1012,15 @@ const HostDetail = () => {
{host.stats.total_packages} {host.stats.total_packages}
</p> </p>
<p className="text-sm text-secondary-500 dark:text-secondary-300"> <p className="text-sm text-secondary-500 dark:text-secondary-300">
Total Packages Total Installed
</p> </p>
</button> </button>
<button <button
type="button" type="button"
onClick={() => navigate(`/packages?host=${hostId}`)} 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" 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" title="View outdated packages for this host"
> >

View File

@@ -870,9 +870,11 @@ const Hosts = () => {
return ( return (
<button <button
type="button" type="button"
onClick={() => navigate(`/packages?host=${host.id}`)} onClick={() =>
navigate(`/packages?host=${host.id}&filter=outdated`)
}
className="text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 font-medium hover:underline" className="text-sm text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 font-medium hover:underline"
title="View packages for this host" title="View outdated packages for this host"
> >
{host.updatesCount || 0} {host.updatesCount || 0}
</button> </button>

View File

@@ -115,8 +115,20 @@ const Packages = () => {
refetch, refetch,
isFetching, isFetching,
} = useQuery({ } = useQuery({
queryKey: ["packages"], queryKey: ["packages", hostFilter, updateStatusFilter],
queryFn: () => packagesAPI.getAll({ limit: 1000 }).then((res) => res.data), queryFn: () => {
const params = { limit: 10000 }; // High limit to effectively get all packages
if (hostFilter && hostFilter !== "all") {
params.host = hostFilter;
}
// Pass update status filter to backend to pre-filter packages
if (updateStatusFilter === "needs-updates") {
params.needsUpdate = "true";
} else if (updateStatusFilter === "security-updates") {
params.isSecurityUpdate = "true";
}
return packagesAPI.getAll(params).then((res) => res.data);
},
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
refetchOnWindowFocus: false, // Don't refetch when window regains focus refetchOnWindowFocus: false, // Don't refetch when window regains focus
}); });
@@ -160,15 +172,13 @@ const Packages = () => {
const matchesUpdateStatus = const matchesUpdateStatus =
updateStatusFilter === "all-packages" || updateStatusFilter === "all-packages" ||
updateStatusFilter === "needs-updates" || (updateStatusFilter === "needs-updates" &&
(updateStatusFilter === "security-updates" && pkg.isSecurityUpdate) || (pkg.stats?.updatesNeeded || 0) > 0) ||
(updateStatusFilter === "regular-updates" && !pkg.isSecurityUpdate); (updateStatusFilter === "security-updates" &&
(pkg.stats?.securityUpdates || 0) > 0) ||
// For "all-packages", we don't filter by update status (updateStatusFilter === "regular-updates" &&
// For other filters, we only show packages that need updates (pkg.stats?.updatesNeeded || 0) > 0 &&
const matchesUpdateNeeded = (pkg.stats?.securityUpdates || 0) === 0);
updateStatusFilter === "all-packages" ||
(pkg.stats?.updatesNeeded || 0) > 0;
const packageHosts = pkg.packageHosts || []; const packageHosts = pkg.packageHosts || [];
const matchesHost = const matchesHost =
@@ -176,11 +186,7 @@ const Packages = () => {
packageHosts.some((host) => host.hostId === hostFilter); packageHosts.some((host) => host.hostId === hostFilter);
return ( return (
matchesSearch && matchesSearch && matchesCategory && matchesUpdateStatus && matchesHost
matchesCategory &&
matchesUpdateStatus &&
matchesUpdateNeeded &&
matchesHost
); );
}); });
@@ -435,8 +441,16 @@ const Packages = () => {
}); });
const uniquePackageHostsCount = uniquePackageHosts.size; const uniquePackageHostsCount = uniquePackageHosts.size;
// Calculate total packages available // Calculate total packages installed
const totalPackagesCount = packages?.length || 0; // When filtering by host, count each package once (since it can only be installed once per host)
// When not filtering, sum up all installations across all hosts
const totalPackagesCount =
hostFilter && hostFilter !== "all"
? packages?.length || 0
: packages?.reduce(
(sum, pkg) => sum + (pkg.stats?.totalInstalls || 0),
0,
) || 0;
// Calculate outdated packages // Calculate outdated packages
const outdatedPackagesCount = const outdatedPackagesCount =
@@ -517,7 +531,7 @@ const Packages = () => {
<Package className="h-5 w-5 text-primary-600 mr-2" /> <Package className="h-5 w-5 text-primary-600 mr-2" />
<div> <div>
<p className="text-sm text-secondary-500 dark:text-white"> <p className="text-sm text-secondary-500 dark:text-white">
Total Packages Total Installed
</p> </p>
<p className="text-xl font-semibold text-secondary-900 dark:text-white"> <p className="text-xl font-semibold text-secondary-900 dark:text-white">
{totalPackagesCount} {totalPackagesCount}