diff --git a/agents/patchmon-agent-linux-386 b/agents/patchmon-agent-linux-386
index 07e0831..f0d6ea4 100755
Binary files a/agents/patchmon-agent-linux-386 and b/agents/patchmon-agent-linux-386 differ
diff --git a/agents/patchmon-agent-linux-amd64 b/agents/patchmon-agent-linux-amd64
index cc23668..8d1a994 100755
Binary files a/agents/patchmon-agent-linux-amd64 and b/agents/patchmon-agent-linux-amd64 differ
diff --git a/agents/patchmon-agent-linux-arm b/agents/patchmon-agent-linux-arm
index 68037e1..70d723c 100755
Binary files a/agents/patchmon-agent-linux-arm and b/agents/patchmon-agent-linux-arm differ
diff --git a/agents/patchmon-agent-linux-arm64 b/agents/patchmon-agent-linux-arm64
index 8447f0a..0a1803a 100755
Binary files a/agents/patchmon-agent-linux-arm64 and b/agents/patchmon-agent-linux-arm64 differ
diff --git a/agents/patchmon-docker-agent.sh b/agents/patchmon-docker-agent.sh
index 2e934f6..2fadddc 100755
--- a/agents/patchmon-docker-agent.sh
+++ b/agents/patchmon-docker-agent.sh
@@ -1,12 +1,12 @@
#!/bin/bash
-# PatchMon Docker Agent Script v1.2.9
+# PatchMon Docker Agent Script v1.3.0
# This script collects Docker container and image information and sends it to PatchMon
# Configuration
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
API_VERSION="v1"
-AGENT_VERSION="1.2.9"
+AGENT_VERSION="1.3.0"
CONFIG_FILE="/etc/patchmon/agent.conf"
CREDENTIALS_FILE="/etc/patchmon/credentials"
LOG_FILE="/var/log/patchmon-docker-agent.log"
diff --git a/agents/patchmon_install.sh b/agents/patchmon_install.sh
index 5bd6b3a..82a0b4b 100644
--- a/agents/patchmon_install.sh
+++ b/agents/patchmon_install.sh
@@ -399,7 +399,7 @@ fi
curl $CURL_FLAGS \
-H "X-API-ID: $API_ID" \
-H "X-API-KEY: $API_KEY" \
- "$PATCHMON_URL/api/v1/hosts/agent/download?arch=$ARCHITECTURE" \
+ "$PATCHMON_URL/api/v1/hosts/agent/download?arch=$ARCHITECTURE&force=binary" \
-o /usr/local/bin/patchmon-agent
chmod +x /usr/local/bin/patchmon-agent
diff --git a/backend/package.json b/backend/package.json
index 9e39827..a0396be 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "patchmon-backend",
- "version": "1.2.9",
+ "version": "1.3.0",
"description": "Backend API for Linux Patch Monitoring System",
"license": "AGPL-3.0",
"main": "src/server.js",
diff --git a/backend/src/config/database.js b/backend/src/config/prisma.js
similarity index 74%
rename from backend/src/config/database.js
rename to backend/src/config/prisma.js
index 169533d..6908c2a 100644
--- a/backend/src/config/database.js
+++ b/backend/src/config/prisma.js
@@ -1,6 +1,6 @@
/**
- * Database configuration for multiple instances
- * Optimizes connection pooling to prevent "too many connections" errors
+ * Centralized Prisma Client Singleton
+ * Prevents multiple Prisma clients from creating connection leaks
*/
const { PrismaClient } = require("@prisma/client");
@@ -26,22 +26,43 @@ function getOptimizedDatabaseUrl() {
return url.toString();
}
-// Create optimized Prisma client
-function createPrismaClient() {
- const optimizedUrl = getOptimizedDatabaseUrl();
+// Singleton Prisma client instance
+let prismaInstance = null;
- return new PrismaClient({
- datasources: {
- db: {
- url: optimizedUrl,
+function getPrismaClient() {
+ if (!prismaInstance) {
+ const optimizedUrl = getOptimizedDatabaseUrl();
+
+ prismaInstance = new PrismaClient({
+ datasources: {
+ db: {
+ url: optimizedUrl,
+ },
},
- },
- log:
- process.env.PRISMA_LOG_QUERIES === "true"
- ? ["query", "info", "warn", "error"]
- : ["warn", "error"],
- errorFormat: "pretty",
- });
+ log:
+ process.env.PRISMA_LOG_QUERIES === "true"
+ ? ["query", "info", "warn", "error"]
+ : ["warn", "error"],
+ errorFormat: "pretty",
+ });
+
+ // Handle graceful shutdown
+ process.on("beforeExit", async () => {
+ await prismaInstance.$disconnect();
+ });
+
+ process.on("SIGINT", async () => {
+ await prismaInstance.$disconnect();
+ process.exit(0);
+ });
+
+ process.on("SIGTERM", async () => {
+ await prismaInstance.$disconnect();
+ process.exit(0);
+ });
+ }
+
+ return prismaInstance;
}
// Connection health check
@@ -50,7 +71,7 @@ async function checkDatabaseConnection(prisma) {
await prisma.$queryRaw`SELECT 1`;
return true;
} catch (error) {
- console.error("Database connection failed:", error.message);
+ console.error("Database connection check failed:", error.message);
return false;
}
}
@@ -121,9 +142,8 @@ async function disconnectPrisma(prisma, maxRetries = 3) {
}
module.exports = {
- createPrismaClient,
+ getPrismaClient,
checkDatabaseConnection,
waitForDatabase,
disconnectPrisma,
- getOptimizedDatabaseUrl,
};
diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js
index 3d5dd38..adc57d8 100644
--- a/backend/src/middleware/auth.js
+++ b/backend/src/middleware/auth.js
@@ -1,12 +1,12 @@
const jwt = require("jsonwebtoken");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const {
validate_session,
update_session_activity,
is_tfa_bypassed,
} = require("../utils/session_manager");
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Middleware to verify JWT token with session validation
const authenticateToken = async (req, res, next) => {
diff --git a/backend/src/middleware/permissions.js b/backend/src/middleware/permissions.js
index bebc197..22a1dbd 100644
--- a/backend/src/middleware/permissions.js
+++ b/backend/src/middleware/permissions.js
@@ -1,5 +1,5 @@
-const { PrismaClient } = require("@prisma/client");
-const prisma = new PrismaClient();
+const { getPrismaClient } = require("../config/prisma");
+const prisma = getPrismaClient();
// Permission middleware factory
const requirePermission = (permission) => {
diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js
index fd1aa99..69930ea 100644
--- a/backend/src/routes/authRoutes.js
+++ b/backend/src/routes/authRoutes.js
@@ -1,7 +1,7 @@
const express = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { body, validationResult } = require("express-validator");
const { authenticateToken, _requireAdmin } = require("../middleware/auth");
const {
@@ -20,7 +20,7 @@ const {
} = require("../utils/session_manager");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
/**
* Parse user agent string to extract browser and OS info
diff --git a/backend/src/routes/autoEnrollmentRoutes.js b/backend/src/routes/autoEnrollmentRoutes.js
index 4f7f056..ba64e20 100644
--- a/backend/src/routes/autoEnrollmentRoutes.js
+++ b/backend/src/routes/autoEnrollmentRoutes.js
@@ -1,5 +1,5 @@
const express = require("express");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const crypto = require("node:crypto");
const bcrypt = require("bcryptjs");
const { body, validationResult } = require("express-validator");
@@ -8,7 +8,7 @@ const { requireManageSettings } = require("../middleware/permissions");
const { v4: uuidv4 } = require("uuid");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Generate auto-enrollment token credentials
const generate_auto_enrollment_token = () => {
diff --git a/backend/src/routes/dashboardPreferencesRoutes.js b/backend/src/routes/dashboardPreferencesRoutes.js
index 04d868c..81b0787 100644
--- a/backend/src/routes/dashboardPreferencesRoutes.js
+++ b/backend/src/routes/dashboardPreferencesRoutes.js
@@ -1,11 +1,11 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const { v4: uuidv4 } = require("uuid");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Helper function to get user permissions based on role
async function getUserPermissions(userRole) {
diff --git a/backend/src/routes/dashboardRoutes.js b/backend/src/routes/dashboardRoutes.js
index 54d2322..386eb91 100644
--- a/backend/src/routes/dashboardRoutes.js
+++ b/backend/src/routes/dashboardRoutes.js
@@ -1,5 +1,5 @@
const express = require("express");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const moment = require("moment");
const { authenticateToken } = require("../middleware/auth");
const {
@@ -11,7 +11,7 @@ const {
const { queueManager } = require("../services/automation");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Get dashboard statistics
router.get(
@@ -61,9 +61,15 @@ router.get(
},
}),
- // Total outdated packages across all hosts
- prisma.host_packages.count({
- where: { needs_update: true },
+ // Total unique packages that need updates
+ prisma.packages.count({
+ where: {
+ host_packages: {
+ some: {
+ needs_update: true,
+ },
+ },
+ },
}),
// Errored hosts (not updated within threshold based on update interval)
@@ -76,11 +82,15 @@ router.get(
},
}),
- // Security updates count
- prisma.host_packages.count({
+ // Security updates count (unique packages)
+ prisma.packages.count({
where: {
- needs_update: true,
- is_security_update: true,
+ host_packages: {
+ some: {
+ needs_update: true,
+ is_security_update: true,
+ },
+ },
},
}),
diff --git a/backend/src/routes/dockerRoutes.js b/backend/src/routes/dockerRoutes.js
index c03fa72..271e33a 100644
--- a/backend/src/routes/dockerRoutes.js
+++ b/backend/src/routes/dockerRoutes.js
@@ -1,9 +1,9 @@
const express = require("express");
const { authenticateToken } = require("../middleware/auth");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { v4: uuidv4 } = require("uuid");
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
const router = express.Router();
// Helper function to convert BigInt fields to strings for JSON serialization
diff --git a/backend/src/routes/gethomepageRoutes.js b/backend/src/routes/gethomepageRoutes.js
index cc44163..c015d9e 100644
--- a/backend/src/routes/gethomepageRoutes.js
+++ b/backend/src/routes/gethomepageRoutes.js
@@ -1,9 +1,9 @@
const express = require("express");
-const { createPrismaClient } = require("../config/database");
+const { getPrismaClient } = require("../config/prisma");
const bcrypt = require("bcryptjs");
const router = express.Router();
-const prisma = createPrismaClient();
+const prisma = getPrismaClient();
// Middleware to authenticate API key
const authenticateApiKey = async (req, res, next) => {
@@ -114,9 +114,15 @@ router.get("/stats", authenticateApiKey, async (_req, res) => {
where: { status: "active" },
});
- // Get total outdated packages count
- const totalOutdatedPackages = await prisma.host_packages.count({
- where: { needs_update: true },
+ // Get total unique packages that need updates (consistent with dashboard)
+ const totalOutdatedPackages = await prisma.packages.count({
+ where: {
+ host_packages: {
+ some: {
+ needs_update: true,
+ },
+ },
+ },
});
// Get total repositories count
@@ -136,11 +142,15 @@ router.get("/stats", authenticateApiKey, async (_req, res) => {
},
});
- // Get security updates count
- const securityUpdates = await prisma.host_packages.count({
+ // Get security updates count (unique packages - consistent with dashboard)
+ const securityUpdates = await prisma.packages.count({
where: {
- needs_update: true,
- is_security_update: true,
+ host_packages: {
+ some: {
+ needs_update: true,
+ is_security_update: true,
+ },
+ },
},
});
diff --git a/backend/src/routes/hostGroupRoutes.js b/backend/src/routes/hostGroupRoutes.js
index 5b44bbb..3c23db9 100644
--- a/backend/src/routes/hostGroupRoutes.js
+++ b/backend/src/routes/hostGroupRoutes.js
@@ -1,12 +1,12 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { randomUUID } = require("node:crypto");
const { authenticateToken } = require("../middleware/auth");
const { requireManageHosts } = require("../middleware/permissions");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Get all host groups
router.get("/", authenticateToken, async (_req, res) => {
diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js
index b67e850..c268071 100644
--- a/backend/src/routes/hostRoutes.js
+++ b/backend/src/routes/hostRoutes.js
@@ -1,5 +1,5 @@
const express = require("express");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { body, validationResult } = require("express-validator");
const { v4: uuidv4 } = require("uuid");
const crypto = require("node:crypto");
@@ -12,7 +12,7 @@ const {
} = require("../middleware/permissions");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Secure endpoint to download the agent script/binary (requires API authentication)
router.get("/agent/download", async (req, res) => {
@@ -39,7 +39,10 @@ router.get("/agent/download", async (req, res) => {
// Check if this is a legacy agent (bash script) requesting update
// Legacy agents will have agent_version < 1.2.9 (excluding 1.2.9 itself)
+ // But allow forcing binary download for fresh installations
+ const forceBinary = req.query.force === "binary";
const isLegacyAgent =
+ !forceBinary &&
host.agent_version &&
((host.agent_version.startsWith("1.2.") &&
host.agent_version !== "1.2.9") ||
diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js
index d944a1f..c65aac4 100644
--- a/backend/src/routes/packageRoutes.js
+++ b/backend/src/routes/packageRoutes.js
@@ -1,8 +1,8 @@
const express = require("express");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Get all packages with their update status
router.get("/", async (req, res) => {
diff --git a/backend/src/routes/permissionsRoutes.js b/backend/src/routes/permissionsRoutes.js
index 159ac08..6d77cdb 100644
--- a/backend/src/routes/permissionsRoutes.js
+++ b/backend/src/routes/permissionsRoutes.js
@@ -1,5 +1,5 @@
const express = require("express");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const {
requireManageSettings,
@@ -7,7 +7,7 @@ const {
} = require("../middleware/permissions");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Get all role permissions (allow users who can manage users to view roles)
router.get(
diff --git a/backend/src/routes/repositoryRoutes.js b/backend/src/routes/repositoryRoutes.js
index a1c0b10..37c35b8 100644
--- a/backend/src/routes/repositoryRoutes.js
+++ b/backend/src/routes/repositoryRoutes.js
@@ -1,6 +1,6 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const {
requireViewHosts,
@@ -8,7 +8,7 @@ const {
} = require("../middleware/permissions");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Get all repositories with host count
router.get("/", authenticateToken, requireViewHosts, async (_req, res) => {
diff --git a/backend/src/routes/searchRoutes.js b/backend/src/routes/searchRoutes.js
index 456dde4..c1a09e6 100644
--- a/backend/src/routes/searchRoutes.js
+++ b/backend/src/routes/searchRoutes.js
@@ -1,9 +1,9 @@
const express = require("express");
const router = express.Router();
-const { createPrismaClient } = require("../config/database");
+const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
-const prisma = createPrismaClient();
+const prisma = getPrismaClient();
/**
* Global search endpoint
diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js
index e1cfb72..d2a782d 100644
--- a/backend/src/routes/settingsRoutes.js
+++ b/backend/src/routes/settingsRoutes.js
@@ -1,12 +1,12 @@
const express = require("express");
const { body, validationResult } = require("express-validator");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { authenticateToken } = require("../middleware/auth");
const { requireManageSettings } = require("../middleware/permissions");
const { getSettings, updateSettings } = require("../services/settingsService");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// WebSocket broadcaster for agent policy updates (no longer used - queue-based delivery preferred)
// const { broadcastSettingsUpdate } = require("../services/agentWs");
diff --git a/backend/src/routes/tfaRoutes.js b/backend/src/routes/tfaRoutes.js
index 35eb041..e27dbd4 100644
--- a/backend/src/routes/tfaRoutes.js
+++ b/backend/src/routes/tfaRoutes.js
@@ -1,12 +1,12 @@
const express = require("express");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const speakeasy = require("speakeasy");
const QRCode = require("qrcode");
const { authenticateToken } = require("../middleware/auth");
const { body, validationResult } = require("express-validator");
const router = express.Router();
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Generate TFA secret and QR code
router.get("/setup", authenticateToken, async (req, res) => {
diff --git a/backend/src/routes/versionRoutes.js b/backend/src/routes/versionRoutes.js
index 0fcaded..72a39ac 100644
--- a/backend/src/routes/versionRoutes.js
+++ b/backend/src/routes/versionRoutes.js
@@ -1,9 +1,9 @@
const express = require("express");
const { authenticateToken } = require("../middleware/auth");
const { requireManageSettings } = require("../middleware/permissions");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Default GitHub repository URL
const DEFAULT_GITHUB_REPO = "https://github.com/PatchMon/PatchMon.git";
@@ -14,13 +14,13 @@ const router = express.Router();
function getCurrentVersion() {
try {
const packageJson = require("../../package.json");
- return packageJson?.version || "1.2.9";
+ return packageJson?.version || "1.3.0";
} catch (packageError) {
console.warn(
"Could not read version from package.json, using fallback:",
packageError.message,
);
- return "1.2.9";
+ return "1.3.0";
}
}
diff --git a/backend/src/routes/wsRoutes.js b/backend/src/routes/wsRoutes.js
index ae42209..4cfd78a 100644
--- a/backend/src/routes/wsRoutes.js
+++ b/backend/src/routes/wsRoutes.js
@@ -53,8 +53,6 @@ router.get("/status/:apiId/stream", async (req, res) => {
// Validate session (same as regular auth middleware)
const validation = await validate_session(decoded.sessionId, token);
if (!validation.valid) {
- console.error("[SSE] Session validation failed:", validation.reason);
- console.error("[SSE] Invalid session for api_id:", apiId);
return res.status(401).json({ error: "Invalid or expired session" });
}
@@ -62,9 +60,7 @@ router.get("/status/:apiId/stream", async (req, res) => {
await update_session_activity(decoded.sessionId);
req.user = validation.user;
- } catch (err) {
- console.error("[SSE] JWT verification failed:", err.message);
- console.error("[SSE] Invalid token for api_id:", apiId);
+ } catch (_err) {
return res.status(401).json({ error: "Invalid or expired token" });
}
diff --git a/backend/src/server.js b/backend/src/server.js
index 5f631ce..ee2d06f 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -41,10 +41,10 @@ const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const cookieParser = require("cookie-parser");
const {
- createPrismaClient,
+ getPrismaClient,
waitForDatabase,
disconnectPrisma,
-} = require("./config/database");
+} = require("./config/prisma");
const winston = require("winston");
// Import routes
@@ -75,7 +75,7 @@ const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const { ExpressAdapter } = require("@bull-board/express");
// Initialize Prisma client with optimized connection pooling for multiple instances
-const prisma = createPrismaClient();
+const prisma = getPrismaClient();
// Function to check and create default role permissions on startup
async function checkAndCreateRolePermissions() {
diff --git a/backend/src/services/automation/githubUpdateCheck.js b/backend/src/services/automation/githubUpdateCheck.js
index 9725918..8148d0c 100644
--- a/backend/src/services/automation/githubUpdateCheck.js
+++ b/backend/src/services/automation/githubUpdateCheck.js
@@ -52,7 +52,7 @@ class GitHubUpdateCheck {
}
// Read version from package.json
- let currentVersion = "1.2.7"; // fallback
+ let currentVersion = "1.3.0"; // fallback
try {
const packageJson = require("../../../package.json");
if (packageJson?.version) {
diff --git a/backend/src/services/automation/index.js b/backend/src/services/automation/index.js
index 244ef5f..b8c670c 100644
--- a/backend/src/services/automation/index.js
+++ b/backend/src/services/automation/index.js
@@ -99,16 +99,27 @@ class QueueManager {
* Initialize all workers
*/
async initializeWorkers() {
+ // Optimized worker options to reduce Redis connections
+ const workerOptions = {
+ connection: redisConnection,
+ concurrency: 1, // Keep concurrency low to reduce connections
+ // Connection optimization
+ maxStalledCount: 1,
+ stalledInterval: 30000,
+ // Reduce connection churn
+ settings: {
+ stalledInterval: 30000,
+ maxStalledCount: 1,
+ },
+ };
+
// GitHub Update Check Worker
this.workers[QUEUE_NAMES.GITHUB_UPDATE_CHECK] = new Worker(
QUEUE_NAMES.GITHUB_UPDATE_CHECK,
this.automations[QUEUE_NAMES.GITHUB_UPDATE_CHECK].process.bind(
this.automations[QUEUE_NAMES.GITHUB_UPDATE_CHECK],
),
- {
- connection: redisConnection,
- concurrency: 1,
- },
+ workerOptions,
);
// Session Cleanup Worker
@@ -117,10 +128,7 @@ class QueueManager {
this.automations[QUEUE_NAMES.SESSION_CLEANUP].process.bind(
this.automations[QUEUE_NAMES.SESSION_CLEANUP],
),
- {
- connection: redisConnection,
- concurrency: 1,
- },
+ workerOptions,
);
// Orphaned Repo Cleanup Worker
@@ -129,10 +137,7 @@ class QueueManager {
this.automations[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].process.bind(
this.automations[QUEUE_NAMES.ORPHANED_REPO_CLEANUP],
),
- {
- connection: redisConnection,
- concurrency: 1,
- },
+ workerOptions,
);
// Orphaned Package Cleanup Worker
@@ -141,167 +146,33 @@ class QueueManager {
this.automations[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].process.bind(
this.automations[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP],
),
- {
- connection: redisConnection,
- concurrency: 1,
- },
+ workerOptions,
);
// Agent Commands Worker
this.workers[QUEUE_NAMES.AGENT_COMMANDS] = new Worker(
QUEUE_NAMES.AGENT_COMMANDS,
async (job) => {
- const { api_id, type, update_interval } = job.data || {};
- console.log("[agent-commands] processing job", job.id, api_id, type);
+ const { api_id, type } = job.data;
+ console.log(`Processing agent command: ${type} for ${api_id}`);
- // Log job attempt to history - use job.id as the unique identifier
- const attemptNumber = job.attemptsMade || 1;
- const historyId = job.id; // Single row per job, updated with each attempt
-
- try {
- if (!api_id || !type) {
- throw new Error("invalid job data");
- }
-
- // Find host by api_id
- const host = await prisma.hosts.findUnique({
- where: { api_id },
- select: { id: true },
- });
-
- // Ensure agent is connected; if not, retry later
- if (!agentWs.isConnected(api_id)) {
- const error = new Error("agent not connected");
- // Log failed attempt
- await prisma.job_history.upsert({
- where: { id: historyId },
- create: {
- id: historyId,
- job_id: job.id,
- queue_name: QUEUE_NAMES.AGENT_COMMANDS,
- job_name: type,
- host_id: host?.id,
- api_id,
- status: "failed",
- attempt_number: attemptNumber,
- error_message: error.message,
- created_at: new Date(),
- updated_at: new Date(),
- },
- update: {
- status: "failed",
- attempt_number: attemptNumber,
- error_message: error.message,
- updated_at: new Date(),
- },
- });
- console.log(
- "[agent-commands] agent not connected, will retry",
- api_id,
- );
- throw error;
- }
-
- // Process the command
- let result;
- if (type === "settings_update") {
- agentWs.pushSettingsUpdate(api_id, update_interval);
- console.log(
- "[agent-commands] delivered settings_update",
- api_id,
- update_interval,
- );
- result = { delivered: true, update_interval };
- } else if (type === "report_now") {
- agentWs.pushReportNow(api_id);
- console.log("[agent-commands] delivered report_now", api_id);
- result = { delivered: true };
- } else {
- throw new Error("unsupported agent command");
- }
-
- // Log successful completion
- await prisma.job_history.upsert({
- where: { id: historyId },
- create: {
- id: historyId,
- job_id: job.id,
- queue_name: QUEUE_NAMES.AGENT_COMMANDS,
- job_name: type,
- host_id: host?.id,
- api_id,
- status: "completed",
- attempt_number: attemptNumber,
- output: result,
- created_at: new Date(),
- updated_at: new Date(),
- completed_at: new Date(),
- },
- update: {
- status: "completed",
- attempt_number: attemptNumber,
- output: result,
- error_message: null,
- updated_at: new Date(),
- completed_at: new Date(),
- },
- });
-
- return result;
- } catch (error) {
- // Log error to history (if not already logged above)
- if (error.message !== "agent not connected") {
- const host = await prisma.hosts
- .findUnique({
- where: { api_id },
- select: { id: true },
- })
- .catch(() => null);
-
- await prisma.job_history
- .upsert({
- where: { id: historyId },
- create: {
- id: historyId,
- job_id: job.id,
- queue_name: QUEUE_NAMES.AGENT_COMMANDS,
- job_name: type || "unknown",
- host_id: host?.id,
- api_id,
- status: "failed",
- attempt_number: attemptNumber,
- error_message: error.message,
- created_at: new Date(),
- updated_at: new Date(),
- },
- update: {
- status: "failed",
- attempt_number: attemptNumber,
- error_message: error.message,
- updated_at: new Date(),
- },
- })
- .catch((err) =>
- console.error("[agent-commands] failed to log error:", err),
- );
- }
- throw error;
+ // Send command via WebSocket based on type
+ if (type === "report_now") {
+ agentWs.pushReportNow(api_id);
+ } else if (type === "settings_update") {
+ // For settings update, we need additional data
+ const { update_interval } = job.data;
+ agentWs.pushSettingsUpdate(api_id, update_interval);
+ } else {
+ console.error(`Unknown agent command type: ${type}`);
}
},
- {
- connection: redisConnection,
- concurrency: 10,
- },
+ workerOptions,
);
- // Add error handling for all workers
- Object.values(this.workers).forEach((worker) => {
- worker.on("error", (error) => {
- console.error("Worker error:", error);
- });
- });
-
- console.log("✅ All workers initialized");
+ console.log(
+ "✅ All workers initialized with optimized connection settings",
+ );
}
/**
diff --git a/backend/src/services/automation/shared/prisma.js b/backend/src/services/automation/shared/prisma.js
index c8518ca..5e894d7 100644
--- a/backend/src/services/automation/shared/prisma.js
+++ b/backend/src/services/automation/shared/prisma.js
@@ -1,5 +1,5 @@
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../../../config/prisma");
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
module.exports = { prisma };
diff --git a/backend/src/services/automation/shared/redis.js b/backend/src/services/automation/shared/redis.js
index d5afccf..2bb1809 100644
--- a/backend/src/services/automation/shared/redis.js
+++ b/backend/src/services/automation/shared/redis.js
@@ -1,17 +1,56 @@
const IORedis = require("ioredis");
-// Redis connection configuration
+// Redis connection configuration with connection pooling
const redisConnection = {
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
username: process.env.REDIS_USER || undefined,
db: parseInt(process.env.REDIS_DB, 10) || 0,
+ // Connection pooling settings
+ lazyConnect: true,
+ keepAlive: 30000,
+ connectTimeout: 30000, // Increased from 10s to 30s
+ commandTimeout: 30000, // Increased from 5s to 30s
+ enableReadyCheck: false,
+ // Reduce connection churn
+ family: 4, // Force IPv4
+ // Retry settings
+ retryDelayOnClusterDown: 300,
retryDelayOnFailover: 100,
maxRetriesPerRequest: null, // BullMQ requires this to be null
+ // Connection pool settings
+ maxLoadingTimeout: 30000,
};
-// Create Redis connection
-const redis = new IORedis(redisConnection);
+// Create Redis connection with singleton pattern
+let redisInstance = null;
-module.exports = { redis, redisConnection };
+function getRedisConnection() {
+ if (!redisInstance) {
+ redisInstance = new IORedis(redisConnection);
+
+ // Handle graceful shutdown
+ process.on("beforeExit", async () => {
+ await redisInstance.quit();
+ });
+
+ process.on("SIGINT", async () => {
+ await redisInstance.quit();
+ process.exit(0);
+ });
+
+ process.on("SIGTERM", async () => {
+ await redisInstance.quit();
+ process.exit(0);
+ });
+ }
+
+ return redisInstance;
+}
+
+module.exports = {
+ redis: getRedisConnection(),
+ redisConnection,
+ getRedisConnection,
+};
diff --git a/backend/src/services/automation/shared/utils.js b/backend/src/services/automation/shared/utils.js
index 9eb52b8..87a7f16 100644
--- a/backend/src/services/automation/shared/utils.js
+++ b/backend/src/services/automation/shared/utils.js
@@ -33,7 +33,7 @@ async function checkPublicRepo(owner, repo) {
try {
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
- let currentVersion = "1.2.7"; // fallback
+ let currentVersion = "1.3.0"; // fallback
try {
const packageJson = require("../../../package.json");
if (packageJson?.version) {
diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js
index 3a137ef..8296d43 100644
--- a/backend/src/services/settingsService.js
+++ b/backend/src/services/settingsService.js
@@ -1,7 +1,7 @@
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
const { v4: uuidv4 } = require("uuid");
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
// Cached settings instance
let cachedSettings = null;
diff --git a/backend/src/utils/session_manager.js b/backend/src/utils/session_manager.js
index 5155071..3550442 100644
--- a/backend/src/utils/session_manager.js
+++ b/backend/src/utils/session_manager.js
@@ -1,8 +1,8 @@
const jwt = require("jsonwebtoken");
const crypto = require("node:crypto");
-const { PrismaClient } = require("@prisma/client");
+const { getPrismaClient } = require("../config/prisma");
-const prisma = new PrismaClient();
+const prisma = getPrismaClient();
/**
* Session Manager - Handles secure session management with inactivity timeout
diff --git a/frontend/package.json b/frontend/package.json
index 3685593..0da7ae1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "patchmon-frontend",
"private": true,
- "version": "1.2.9",
+ "version": "1.3.0",
"license": "AGPL-3.0",
"type": "module",
"scripts": {
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index f82e130..8f68a16 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -117,7 +117,7 @@ const Layout = ({ children }) => {
name: "Automation",
href: "/automation",
icon: RefreshCw,
- beta: true,
+ new: true,
});
if (canViewReports()) {
@@ -440,6 +440,11 @@ const Layout = ({ children }) => {
Beta
)}
+ {subItem.new && (
+
+ New
+
+ )}
)}
@@ -716,6 +721,11 @@ const Layout = ({ children }) => {
Beta
)}
+ {subItem.new && (
+
+ New
+
+ )}
{subItem.showUpgradeIcon && (
+ {notesMessage.text} +
+