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 && ( )} diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index eacf849..8126105 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -52,6 +52,7 @@ const HostDetail = () => { const [historyPage, setHistoryPage] = useState(0); const [historyLimit] = useState(10); const [notes, setNotes] = useState(""); + const [notesMessage, setNotesMessage] = useState({ text: "", type: "" }); const { data: host, @@ -212,6 +213,17 @@ const HostDetail = () => { onSuccess: () => { queryClient.invalidateQueries(["host", hostId]); queryClient.invalidateQueries(["hosts"]); + setNotesMessage({ text: "Notes saved successfully!", type: "success" }); + // Clear message after 3 seconds + setTimeout(() => setNotesMessage({ text: "", type: "" }), 3000); + }, + onError: (error) => { + setNotesMessage({ + text: error.response?.data?.error || "Failed to save notes", + type: "error", + }); + // Clear message after 5 seconds for errors + setTimeout(() => setNotesMessage({ text: "", type: "" }), 5000); }, }); @@ -1233,6 +1245,37 @@ const HostDetail = () => { Host Notes + + {/* Success/Error Message */} + {notesMessage.text && ( +
+
+ {notesMessage.type === "success" ? ( + + ) : ( + + )} +
+

+ {notesMessage.text} +

+
+
+
+ )} +