mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-19 14:08:34 +00:00
Compare commits
9 Commits
v1.3.4
...
renovate/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf062bc2b9 | ||
|
|
c6a2163e79 | ||
|
|
b55ee6a6e0 | ||
|
|
e983d39bd6 | ||
|
|
189de7a593 | ||
|
|
f57d87e1c0 | ||
|
|
470b204a8c | ||
|
|
fa1f0fd7d7 | ||
|
|
334357a12e |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -197,6 +197,40 @@ while IFS= read -r line; do
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if agent is already installed and working BEFORE enrollment
|
||||
info " Checking if agent is already configured..."
|
||||
config_check=$(timeout 10 pct exec "$vmid" -- bash -c "
|
||||
if [[ -f /etc/patchmon/config.yml ]] && [[ -f /etc/patchmon/credentials.yml ]]; then
|
||||
if [[ -f /usr/local/bin/patchmon-agent ]]; then
|
||||
# Try to ping using existing configuration
|
||||
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
|
||||
echo 'ping_success'
|
||||
else
|
||||
echo 'ping_failed'
|
||||
fi
|
||||
else
|
||||
echo 'binary_missing'
|
||||
fi
|
||||
else
|
||||
echo 'not_configured'
|
||||
fi
|
||||
" 2>/dev/null </dev/null || echo "error")
|
||||
|
||||
if [[ "$config_check" == "ping_success" ]]; then
|
||||
info " ✓ Host already enrolled and agent ping successful - skipping enrollment"
|
||||
((skipped_count++)) || true
|
||||
echo ""
|
||||
continue
|
||||
elif [[ "$config_check" == "ping_failed" ]]; then
|
||||
warn " ⚠ Agent configuration exists but ping failed - will re-enroll and reinstall"
|
||||
elif [[ "$config_check" == "binary_missing" ]]; then
|
||||
warn " ⚠ Config exists but agent binary missing - will re-enroll and reinstall"
|
||||
elif [[ "$config_check" == "not_configured" ]]; then
|
||||
info " ℹ Agent not yet configured - proceeding with enrollment"
|
||||
else
|
||||
warn " ⚠ Could not check agent status - proceeding with enrollment"
|
||||
fi
|
||||
|
||||
# Call PatchMon auto-enrollment API
|
||||
info " Enrolling $friendly_name in PatchMon..."
|
||||
|
||||
@@ -230,40 +264,6 @@ while IFS= read -r line; do
|
||||
|
||||
info " ✓ Host enrolled successfully: $api_id"
|
||||
|
||||
# Check if agent is already installed and working
|
||||
info " Checking if agent is already configured..."
|
||||
config_check=$(timeout 10 pct exec "$vmid" -- bash -c "
|
||||
if [[ -f /etc/patchmon/config.yml ]] && [[ -f /etc/patchmon/credentials.yml ]]; then
|
||||
if [[ -f /usr/local/bin/patchmon-agent ]]; then
|
||||
# Try to ping using existing configuration
|
||||
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
|
||||
echo 'ping_success'
|
||||
else
|
||||
echo 'ping_failed'
|
||||
fi
|
||||
else
|
||||
echo 'binary_missing'
|
||||
fi
|
||||
else
|
||||
echo 'not_configured'
|
||||
fi
|
||||
" 2>/dev/null </dev/null || echo "error")
|
||||
|
||||
if [[ "$config_check" == "ping_success" ]]; then
|
||||
info " ✓ Host already enrolled and agent ping successful - skipping"
|
||||
((skipped_count++)) || true
|
||||
echo ""
|
||||
continue
|
||||
elif [[ "$config_check" == "ping_failed" ]]; then
|
||||
warn " ⚠ Agent configuration exists but ping failed - will reinstall"
|
||||
elif [[ "$config_check" == "binary_missing" ]]; then
|
||||
warn " ⚠ Config exists but agent binary missing - will reinstall"
|
||||
elif [[ "$config_check" == "not_configured" ]]; then
|
||||
info " ℹ Agent not yet configured - proceeding with installation"
|
||||
else
|
||||
warn " ⚠ Could not check agent status - proceeding with installation"
|
||||
fi
|
||||
|
||||
# Ensure curl is installed in the container
|
||||
info " Checking for curl in container..."
|
||||
curl_check=$(timeout 10 pct exec "$vmid" -- bash -c "command -v curl >/dev/null 2>&1 && echo 'installed' || echo 'missing'" 2>/dev/null </dev/null || echo "error")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"description": "Backend API for Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "src/server.js",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@bull-board/express": "^6.13.1",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"axios": "^1.7.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcryptjs": "^3.0.0",
|
||||
"bullmq": "^5.61.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
@@ -37,7 +37,7 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prisma": "^6.1.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
||||
@@ -588,8 +588,11 @@ router.get("/script", async (req, res) => {
|
||||
// Check for --force parameter
|
||||
const force_install = req.query.force === "true" || req.query.force === "1";
|
||||
|
||||
// Use bash for proxmox-lxc, sh for others
|
||||
const shebang = script_type === "proxmox-lxc" ? "#!/bin/bash" : "#!/bin/sh";
|
||||
|
||||
// Inject the token credentials, server URL, curl flags, and force flag into the script
|
||||
const env_vars = `#!/bin/sh
|
||||
const env_vars = `${shebang}
|
||||
# PatchMon Auto-Enrollment Configuration (Auto-generated)
|
||||
export PATCHMON_URL="${server_url}"
|
||||
export AUTO_ENROLLMENT_KEY="${token.token_key}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,7 +12,6 @@ const {
|
||||
} = require("../middleware/permissions");
|
||||
const { queueManager, QUEUE_NAMES } = require("../services/automation");
|
||||
const { pushIntegrationToggle, isConnected } = require("../services/agentWs");
|
||||
const agentVersionService = require("../services/agentVersionService");
|
||||
const { compareVersions } = require("../services/automation/shared/utils");
|
||||
|
||||
const router = express.Router();
|
||||
@@ -170,111 +169,101 @@ router.get("/agent/version", async (req, res) => {
|
||||
});
|
||||
} else {
|
||||
// Go agent version check
|
||||
// Detect server architecture and map to Go architecture names
|
||||
const os = require("node:os");
|
||||
// Always check the server's local binary for the requested architecture
|
||||
// The server's agents folder is the source of truth, not GitHub
|
||||
const { exec } = require("node:child_process");
|
||||
const { promisify } = require("node:util");
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const serverArch = os.arch();
|
||||
// Map Node.js architecture to Go architecture names
|
||||
const archMap = {
|
||||
x64: "amd64",
|
||||
ia32: "386",
|
||||
arm64: "arm64",
|
||||
arm: "arm",
|
||||
};
|
||||
const serverGoArch = archMap[serverArch] || serverArch;
|
||||
const binaryName = `patchmon-agent-linux-${architecture}`;
|
||||
const binaryPath = path.join(__dirname, "../../../agents", binaryName);
|
||||
|
||||
// If requested architecture matches server architecture, execute the binary
|
||||
if (architecture === serverGoArch) {
|
||||
const binaryName = `patchmon-agent-linux-${architecture}`;
|
||||
const binaryPath = path.join(__dirname, "../../../agents", binaryName);
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
// Binary exists in server's agents folder - use its version
|
||||
let serverVersion = null;
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
// Binary doesn't exist, fall back to GitHub
|
||||
console.log(`Binary ${binaryName} not found, falling back to GitHub`);
|
||||
} else {
|
||||
// Execute the binary to get its version
|
||||
// Try method 1: Execute binary (works for same architecture)
|
||||
try {
|
||||
const { stdout } = await execAsync(`${binaryPath} --help`, {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Parse version from help output (e.g., "PatchMon Agent v1.3.1")
|
||||
const versionMatch = stdout.match(
|
||||
/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
|
||||
);
|
||||
|
||||
if (versionMatch) {
|
||||
serverVersion = versionMatch[1];
|
||||
}
|
||||
} catch (execError) {
|
||||
// Execution failed (likely cross-architecture) - try alternative method
|
||||
console.warn(
|
||||
`Failed to execute binary ${binaryName} to get version (may be cross-architecture): ${execError.message}`,
|
||||
);
|
||||
|
||||
// Try method 2: Extract version using strings command (works for cross-architecture)
|
||||
try {
|
||||
const { stdout } = await execAsync(`${binaryPath} --help`, {
|
||||
timeout: 10000,
|
||||
});
|
||||
const { stdout: stringsOutput } = await execAsync(
|
||||
`strings "${binaryPath}" | grep -E "PatchMon Agent v[0-9]+\\.[0-9]+\\.[0-9]+" | head -1`,
|
||||
{
|
||||
timeout: 10000,
|
||||
},
|
||||
);
|
||||
|
||||
// Parse version from help output (e.g., "PatchMon Agent v1.3.1")
|
||||
const versionMatch = stdout.match(
|
||||
const versionMatch = stringsOutput.match(
|
||||
/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
|
||||
);
|
||||
|
||||
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];
|
||||
console.log(
|
||||
`✅ Extracted version ${serverVersion} from binary using strings command`,
|
||||
);
|
||||
}
|
||||
} catch (execError) {
|
||||
// Execution failed, fall back to GitHub
|
||||
console.log(
|
||||
`Failed to execute binary ${binaryName}: ${execError.message}, falling back to GitHub`,
|
||||
} catch (stringsError) {
|
||||
console.warn(
|
||||
`Failed to extract version using strings command: ${stringsError.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to GitHub if architecture doesn't match or binary execution failed
|
||||
try {
|
||||
const versionInfo = await agentVersionService.getVersionInfo();
|
||||
const latestVersion = versionInfo.latestVersion;
|
||||
const agentVersion =
|
||||
req.query.currentVersion || latestVersion || "unknown";
|
||||
// If we successfully got the version, return it
|
||||
if (serverVersion) {
|
||||
const agentVersion = req.query.currentVersion || serverVersion;
|
||||
|
||||
if (!latestVersion) {
|
||||
return res.status(503).json({
|
||||
error: "Unable to determine latest version from GitHub releases",
|
||||
// Proper semantic version comparison: only update if server version is NEWER
|
||||
const hasUpdate = compareVersions(serverVersion, agentVersion) > 0;
|
||||
|
||||
return res.json({
|
||||
currentVersion: agentVersion,
|
||||
latestVersion: null,
|
||||
hasUpdate: false,
|
||||
latestVersion: serverVersion,
|
||||
hasUpdate: hasUpdate,
|
||||
downloadUrl: `/api/v1/hosts/agent/download?arch=${architecture}`,
|
||||
releaseNotes: `PatchMon Agent v${serverVersion}`,
|
||||
minServerVersion: null,
|
||||
architecture: architecture,
|
||||
agentType: "go",
|
||||
});
|
||||
}
|
||||
|
||||
// Proper semantic version comparison: only update if latest version is NEWER
|
||||
const hasUpdate =
|
||||
latestVersion !== null &&
|
||||
compareVersions(latestVersion, agentVersion) > 0;
|
||||
|
||||
res.json({
|
||||
currentVersion: agentVersion,
|
||||
latestVersion: latestVersion,
|
||||
hasUpdate: hasUpdate,
|
||||
downloadUrl: `/api/v1/hosts/agent/download?arch=${architecture}`,
|
||||
releaseNotes: `PatchMon Agent v${latestVersion}`,
|
||||
minServerVersion: null,
|
||||
architecture: architecture,
|
||||
agentType: "go",
|
||||
});
|
||||
} catch (serviceError) {
|
||||
console.error(
|
||||
"Failed to get version from agentVersionService:",
|
||||
serviceError.message,
|
||||
// If we couldn't get version, fall through to error response
|
||||
console.warn(
|
||||
`Could not determine version for binary ${binaryName} using any method`,
|
||||
);
|
||||
return res.status(500).json({
|
||||
error: "Failed to get agent version from service",
|
||||
details: serviceError.message,
|
||||
});
|
||||
}
|
||||
|
||||
// Binary doesn't exist or couldn't get version - return error
|
||||
// Don't fall back to GitHub - the server's agents folder is the source of truth
|
||||
const agentVersion = req.query.currentVersion || "unknown";
|
||||
return res.status(404).json({
|
||||
error: `Agent binary not found for architecture: ${architecture}. Please ensure the binary is in the server's agents folder.`,
|
||||
currentVersion: agentVersion,
|
||||
latestVersion: null,
|
||||
hasUpdate: false,
|
||||
architecture: architecture,
|
||||
agentType: "go",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Version check error:", error);
|
||||
|
||||
@@ -10,8 +10,6 @@ ENV NODE_ENV=development \
|
||||
|
||||
RUN apk add --no-cache openssl tini curl libc6-compat
|
||||
|
||||
USER node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node package*.json ./
|
||||
@@ -20,7 +18,10 @@ COPY --chown=node:node agents ./agents_backup
|
||||
COPY --chown=node:node agents ./agents
|
||||
COPY --chmod=755 docker/backend.docker-entrypoint.sh ./entrypoint.sh
|
||||
|
||||
RUN npm install --workspace=backend --ignore-scripts && cd backend && npx prisma generate
|
||||
USER node
|
||||
|
||||
RUN npm install --workspace=backend --ignore-scripts && cd backend && npx prisma generate && \
|
||||
chmod -R u+w /app/node_modules/@prisma/engines 2>/dev/null || true
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
@@ -66,8 +67,6 @@ ENV NODE_ENV=production \
|
||||
|
||||
RUN apk add --no-cache openssl tini curl libc6-compat
|
||||
|
||||
USER node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder --chown=node:node /app/backend ./backend
|
||||
@@ -76,6 +75,14 @@ COPY --chown=node:node agents ./agents_backup
|
||||
COPY --chown=node:node agents ./agents
|
||||
COPY --chmod=755 docker/backend.docker-entrypoint.sh ./entrypoint.sh
|
||||
|
||||
# Ensure Prisma engines directory is writable for rootless Docker (Prisma 6.1.0+ requirement)
|
||||
# This must be done as root before switching to node user
|
||||
# Order: chown first (sets ownership), then chmod (sets permissions)
|
||||
RUN chown -R node:node /app/node_modules/@prisma/engines && \
|
||||
chmod -R u+w /app/node_modules/@prisma/engines
|
||||
|
||||
USER node
|
||||
|
||||
WORKDIR /app/backend
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
@@ -6,5 +6,5 @@ VITE_API_URL=http://localhost:3001/api/v1
|
||||
|
||||
# Application Metadata
|
||||
VITE_APP_NAME=PatchMon
|
||||
VITE_APP_VERSION=1.3.4
|
||||
VITE_APP_VERSION=1.3.5
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "patchmon-frontend",
|
||||
"private": true,
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -248,7 +248,6 @@ const Hosts = () => {
|
||||
const showFiltersParam = searchParams.get("showFilters");
|
||||
const osFilterParam = searchParams.get("osFilter");
|
||||
const groupParam = searchParams.get("group");
|
||||
const rebootParam = searchParams.get("reboot");
|
||||
|
||||
if (filter === "needsUpdates") {
|
||||
setShowFilters(true);
|
||||
@@ -335,9 +334,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 +786,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 +956,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 +1150,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">
|
||||
@@ -1190,7 +1218,7 @@ const Hosts = () => {
|
||||
navigate("/hosts", { replace: true });
|
||||
};
|
||||
|
||||
const handleUpToDateClick = () => {
|
||||
const _handleUpToDateClick = () => {
|
||||
// Filter to show only up-to-date hosts
|
||||
setStatusFilter("active");
|
||||
setShowFilters(true);
|
||||
@@ -1731,6 +1759,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"
|
||||
|
||||
171
package-lock.json
generated
171
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "patchmon",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"license": "AGPL-3.0",
|
||||
"workspaces": [
|
||||
"backend",
|
||||
@@ -23,14 +23,14 @@
|
||||
},
|
||||
"backend": {
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.13.1",
|
||||
"@bull-board/express": "^6.13.1",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"axios": "^1.7.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcryptjs": "^3.0.0",
|
||||
"bullmq": "^5.61.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
@@ -49,7 +49,7 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prisma": "^6.1.0"
|
||||
},
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"frontend": {
|
||||
"name": "patchmon-frontend",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -134,6 +134,7 @@
|
||||
"version": "7.28.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -547,6 +548,7 @@
|
||||
"node_modules/@bull-board/ui": {
|
||||
"version": "6.13.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bull-board/api": "6.13.1"
|
||||
}
|
||||
@@ -580,6 +582,7 @@
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -988,9 +991,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz",
|
||||
"integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==",
|
||||
"deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcryptjs": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
@@ -1020,6 +1029,7 @@
|
||||
"version": "18.3.24",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1183,8 +1193,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"license": "MIT"
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
@@ -1267,6 +1282,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -1456,6 +1472,7 @@
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -2030,6 +2047,7 @@
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -2795,6 +2813,76 @@
|
||||
"lefthook-windows-x64": "1.13.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lefthook-darwin-arm64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.13.5.tgz",
|
||||
"integrity": "sha512-BYt5CnAOXasVCS6i+A4ljUo9xru/B5uMFD6EWHhs3R26jGF7mBSDxM3ErzXTUaJRTP0kQI/XBmgqBryBqoqZOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-darwin-x64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-1.13.5.tgz",
|
||||
"integrity": "sha512-ZDtLBzvI5e26C/RZ4irOHpELTd22x9lDTgF2+eCYcnrBWOkB7800V8tuAvBybsLGvg6JwKjFxn+NTRNZnCC2hw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-freebsd-arm64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.13.5.tgz",
|
||||
"integrity": "sha512-uQ/kQZSSedw74aGCpsfOPN4yVt3klg8grOP6gHQOCRUMv5oK/Lj3pe1PylpTuuhxWORWRzkauPMot26J0OZZdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-freebsd-x64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.13.5.tgz",
|
||||
"integrity": "sha512-6czek8XagVrI7ExURawkfrfX40Qjc/wktc8bLq/iXfRlmdvKDMrx2FrA82mDfEVCAEz+tTvkteK1TfR3icYF3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-linux-arm64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-1.13.5.tgz",
|
||||
"integrity": "sha512-MjWtiuW1br+rpTtgG1KGV53mSGtL5MWQwgafYzrFleJ89fKb86F4TD/4mVNzk5thmZ+HVPZw9bRZGUHFBnNJWg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-linux-x64": {
|
||||
"version": "1.13.5",
|
||||
"cpu": [
|
||||
@@ -2807,6 +2895,62 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-openbsd-arm64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.13.5.tgz",
|
||||
"integrity": "sha512-lYXrWf0/hBrwtG8ceaHq886bcqRKh3Lfv+jZJs+ykMLB6L/kaqk8tA4V2NHWydQ5h56o45ugs/580nMz36ZdRg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-openbsd-x64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.13.5.tgz",
|
||||
"integrity": "sha512-Ba1JrsRbfan4WKd8Q7gUhTxCUuppXzirDObd3JxpLRSLxA47yxhjMv7KByDunRDTvzTgsXoykZI6mPupkc1JiQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-windows-arm64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-1.13.5.tgz",
|
||||
"integrity": "sha512-Y/CpmEIb0hlFe+kTT/efWgX6+/gUTp5NItTF+gmUrY1/G/bTLIxdIRS7WpodVM0MEN24sOrQVTSi9DN9FvGoGg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/lefthook-windows-x64": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-1.13.5.tgz",
|
||||
"integrity": "sha512-WJBqGNBlFJnunRwy12QyaDHdGULtostPqpYSZSS4boFJDY0lP5qtz9lAGmJ49aA5GQ19jrnDjGLwVPFiwIqksQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"dev": true,
|
||||
@@ -3419,6 +3563,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3548,6 +3693,7 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.16.2",
|
||||
"@prisma/engines": "6.16.2"
|
||||
@@ -3737,6 +3883,7 @@
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -3755,6 +3902,7 @@
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -4472,6 +4620,7 @@
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4624,6 +4773,7 @@
|
||||
"version": "7.1.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4713,6 +4863,7 @@
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.5",
|
||||
"description": "Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
Reference in New Issue
Block a user