fixing host route of version checking for other architectures

This commit is contained in:
Muhammad Ibrahim
2025-11-17 21:49:05 +00:00
parent fa1f0fd7d7
commit f57d87e1c0
3 changed files with 138 additions and 45 deletions

View File

@@ -242,33 +242,48 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
orderBy: { last_update: "desc" },
});
// OPTIMIZATION: Get all package counts in 2 batch queries instead of N*2 queries
// OPTIMIZATION: Get all package counts in 3 batch queries instead of N*3 queries
const hostIds = hosts.map((h) => h.id);
const [updateCounts, totalCounts] = await Promise.all([
// Get update counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
needs_update: true,
},
_count: { id: true },
}),
// Get total counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
},
_count: { id: true },
}),
]);
const [updateCounts, securityUpdateCounts, totalCounts] = await Promise.all(
[
// Get update counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
needs_update: true,
},
_count: { id: true },
}),
// Get security update counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
needs_update: true,
is_security_update: true,
},
_count: { id: true },
}),
// Get total counts for all hosts at once
prisma.host_packages.groupBy({
by: ["host_id"],
where: {
host_id: { in: hostIds },
},
_count: { id: true },
}),
],
);
// Create lookup maps for O(1) access
const updateCountMap = new Map(
updateCounts.map((item) => [item.host_id, item._count.id]),
);
const securityUpdateCountMap = new Map(
securityUpdateCounts.map((item) => [item.host_id, item._count.id]),
);
const totalCountMap = new Map(
totalCounts.map((item) => [item.host_id, item._count.id]),
);
@@ -276,6 +291,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
// Process hosts with counts from maps (no more DB queries!)
const hostsWithUpdateInfo = hosts.map((host) => {
const updatesCount = updateCountMap.get(host.id) || 0;
const securityUpdatesCount = securityUpdateCountMap.get(host.id) || 0;
const totalPackagesCount = totalCountMap.get(host.id) || 0;
// Calculate effective status based on reporting interval
@@ -292,6 +308,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
return {
...host,
updatesCount,
securityUpdatesCount,
totalPackagesCount,
isStale,
effectiveStatus,

View File

@@ -181,6 +181,9 @@ router.get("/agent/version", async (req, res) => {
if (fs.existsSync(binaryPath)) {
// Binary exists in server's agents folder - use its version
let serverVersion = null;
// Try method 1: Execute binary (works for same architecture)
try {
const { stdout } = await execAsync(`${binaryPath} --help`, {
timeout: 10000,
@@ -192,30 +195,63 @@ router.get("/agent/version", async (req, res) => {
);
if (versionMatch) {
const serverVersion = versionMatch[1];
const agentVersion = req.query.currentVersion || serverVersion;
// Proper semantic version comparison: only update if server version is NEWER
const hasUpdate = compareVersions(serverVersion, agentVersion) > 0;
return res.json({
currentVersion: agentVersion,
latestVersion: serverVersion,
hasUpdate: hasUpdate,
downloadUrl: `/api/v1/hosts/agent/download?arch=${architecture}`,
releaseNotes: `PatchMon Agent v${serverVersion}`,
minServerVersion: null,
architecture: architecture,
agentType: "go",
});
serverVersion = versionMatch[1];
}
} catch (execError) {
// Execution failed, but binary exists - try to get version another way
// Execution failed (likely cross-architecture) - try alternative method
console.warn(
`Failed to execute binary ${binaryName} to get version: ${execError.message}`,
`Failed to execute binary ${binaryName} to get version (may be cross-architecture): ${execError.message}`,
);
// Fall through to error response
// Try method 2: Extract version using strings command (works for cross-architecture)
try {
const { stdout: stringsOutput } = await execAsync(
`strings "${binaryPath}" | grep -E "PatchMon Agent v[0-9]+\\.[0-9]+\\.[0-9]+" | head -1`,
{
timeout: 10000,
},
);
const versionMatch = stringsOutput.match(
/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
);
if (versionMatch) {
serverVersion = versionMatch[1];
console.log(
`✅ Extracted version ${serverVersion} from binary using strings command`,
);
}
} catch (stringsError) {
console.warn(
`Failed to extract version using strings command: ${stringsError.message}`,
);
}
}
// If we successfully got the version, return it
if (serverVersion) {
const agentVersion = req.query.currentVersion || serverVersion;
// Proper semantic version comparison: only update if server version is NEWER
const hasUpdate = compareVersions(serverVersion, agentVersion) > 0;
return res.json({
currentVersion: agentVersion,
latestVersion: serverVersion,
hasUpdate: hasUpdate,
downloadUrl: `/api/v1/hosts/agent/download?arch=${architecture}`,
releaseNotes: `PatchMon Agent v${serverVersion}`,
minServerVersion: null,
architecture: architecture,
agentType: "go",
});
}
// If we couldn't get version, fall through to error response
console.warn(
`Could not determine version for binary ${binaryName} using any method`,
);
}
// Binary doesn't exist or couldn't get version - return error

View File

@@ -335,9 +335,15 @@ const Hosts = () => {
{ id: "status", label: "Status", visible: true, order: 10 },
{ id: "needs_reboot", label: "Reboot", visible: true, order: 11 },
{ id: "updates", label: "Updates", visible: true, order: 12 },
{ id: "notes", label: "Notes", visible: false, order: 13 },
{ id: "last_update", label: "Last Update", visible: true, order: 14 },
{ id: "actions", label: "Actions", visible: true, order: 15 },
{
id: "security_updates",
label: "Security Updates",
visible: true,
order: 13,
},
{ id: "notes", label: "Notes", visible: false, order: 14 },
{ id: "last_update", label: "Last Update", visible: true, order: 15 },
{ id: "actions", label: "Actions", visible: true, order: 16 },
];
const saved = localStorage.getItem("hosts-column-config");
@@ -781,6 +787,10 @@ const Hosts = () => {
aValue = a.updatesCount || 0;
bValue = b.updatesCount || 0;
break;
case "security_updates":
aValue = a.securityUpdatesCount || 0;
bValue = b.securityUpdatesCount || 0;
break;
case "needs_reboot":
// Sort by boolean: false (0) comes before true (1)
aValue = a.needs_reboot ? 1 : 0;
@@ -947,9 +957,15 @@ const Hosts = () => {
{ id: "status", label: "Status", visible: true, order: 10 },
{ id: "needs_reboot", label: "Reboot", visible: true, order: 11 },
{ id: "updates", label: "Updates", visible: true, order: 12 },
{ id: "notes", label: "Notes", visible: false, order: 13 },
{ id: "last_update", label: "Last Update", visible: true, order: 14 },
{ id: "actions", label: "Actions", visible: true, order: 15 },
{
id: "security_updates",
label: "Security Updates",
visible: true,
order: 13,
},
{ id: "notes", label: "Notes", visible: false, order: 14 },
{ id: "last_update", label: "Last Update", visible: true, order: 15 },
{ id: "actions", label: "Actions", visible: true, order: 16 },
];
updateColumnConfig(defaultConfig);
};
@@ -1135,6 +1151,19 @@ const Hosts = () => {
{host.updatesCount || 0}
</button>
);
case "security_updates":
return (
<button
type="button"
onClick={() =>
navigate(`/packages?host=${host.id}&filter=security-updates`)
}
className="text-sm text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 font-medium hover:underline"
title="View security updates for this host"
>
{host.securityUpdatesCount || 0}
</button>
);
case "last_update":
return (
<div className="text-sm text-secondary-500 dark:text-secondary-300">
@@ -1731,6 +1760,17 @@ const Hosts = () => {
{column.label}
{getSortIcon("updates")}
</button>
) : column.id === "security_updates" ? (
<button
type="button"
onClick={() =>
handleSort("security_updates")
}
className="flex items-center gap-2 hover:text-secondary-700"
>
{column.label}
{getSortIcon("security_updates")}
</button>
) : column.id === "needs_reboot" ? (
<button
type="button"