diff --git a/agents/patchmon-agent-linux-386 b/agents/patchmon-agent-linux-386 index c673215..e93885a 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 f129b20..7cb36c9 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 43fb562..68e5461 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 02c896e..2abd7a1 100755 Binary files a/agents/patchmon-agent-linux-arm64 and b/agents/patchmon-agent-linux-arm64 differ diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 7170b07..1c539bd 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -1430,6 +1430,69 @@ router.patch( }, ); +// Force agent update for specific host +router.post( + "/:hostId/force-agent-update", + authenticateToken, + requireManageHosts, + async (req, res) => { + try { + const { hostId } = req.params; + + // Get host to verify it exists + const host = await prisma.hosts.findUnique({ + where: { id: hostId }, + }); + + if (!host) { + return res.status(404).json({ error: "Host not found" }); + } + + // Get queue manager + const { QUEUE_NAMES } = require("../services/automation"); + const queueManager = req.app.locals.queueManager; + + if (!queueManager) { + return res.status(500).json({ + error: "Queue manager not available", + }); + } + + // Get the agent-commands queue + const queue = queueManager.queues[QUEUE_NAMES.AGENT_COMMANDS]; + + // Add job to queue + await queue.add( + "update_agent", + { + api_id: host.api_id, + type: "update_agent", + }, + { + attempts: 3, + backoff: { + type: "exponential", + delay: 2000, + }, + }, + ); + + res.json({ + success: true, + message: "Agent update queued successfully", + host: { + id: host.id, + friendlyName: host.friendly_name, + apiId: host.api_id, + }, + }); + } catch (error) { + console.error("Force agent update error:", error); + res.status(500).json({ error: "Failed to force agent update" }); + } + }, +); + // Serve the installation script (requires API authentication) router.get("/install", async (req, res) => { try { diff --git a/backend/src/services/agentWs.js b/backend/src/services/agentWs.js index 1d7fc61..b5e3b1f 100644 --- a/backend/src/services/agentWs.js +++ b/backend/src/services/agentWs.js @@ -176,6 +176,15 @@ function pushSettingsUpdate(apiId, newInterval) { ); } +function pushUpdateAgent(apiId) { + const ws = apiIdToSocket.get(apiId); + safeSend(ws, JSON.stringify({ type: "update_agent" })); +} + +function getConnectionByApiId(apiId) { + return apiIdToSocket.get(apiId); +} + function pushUpdateNotification(apiId, updateInfo) { const ws = apiIdToSocket.get(apiId); if (ws && ws.readyState === WebSocket.OPEN) { @@ -330,10 +339,12 @@ module.exports = { broadcastSettingsUpdate, pushReportNow, pushSettingsUpdate, + pushUpdateAgent, pushUpdateNotification, pushUpdateNotificationToAll, // Expose read-only view of connected agents getConnectedApiIds: () => Array.from(apiIdToSocket.keys()), + getConnectionByApiId, isConnected: (apiId) => { const ws = apiIdToSocket.get(apiId); return !!ws && ws.readyState === WebSocket.OPEN; diff --git a/backend/src/services/automation/index.js b/backend/src/services/automation/index.js index ff40751..53eacdc 100644 --- a/backend/src/services/automation/index.js +++ b/backend/src/services/automation/index.js @@ -190,6 +190,19 @@ class QueueManager { // For settings update, we need additional data const { update_interval } = job.data; agentWs.pushSettingsUpdate(api_id, update_interval); + } else if (type === "update_agent") { + // Force agent to update by sending WebSocket command + const ws = agentWs.getConnectionByApiId(api_id); + if (ws && ws.readyState === 1) { + // WebSocket.OPEN + agentWs.pushUpdateAgent(api_id); + console.log(`✅ Update command sent to agent ${api_id}`); + } else { + console.error(`❌ Agent ${api_id} is not connected`); + throw new Error( + `Agent ${api_id} is not connected. Cannot send update command.`, + ); + } } else { console.error(`Unknown agent command type: ${type}`); } diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 8126105..750da57 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -187,6 +187,16 @@ const HostDetail = () => { }, }); + // Force agent update mutation + const forceAgentUpdateMutation = useMutation({ + mutationFn: () => + adminHostsAPI.forceAgentUpdate(hostId).then((res) => res.data), + onSuccess: () => { + queryClient.invalidateQueries(["host", hostId]); + queryClient.invalidateQueries(["hosts"]); + }, + }); + const updateFriendlyNameMutation = useMutation({ mutationFn: (friendlyName) => adminHostsAPI @@ -703,6 +713,29 @@ const HostDetail = () => { /> + +
+

+ Force Update +

+ +
)} diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index b1ba10d..75983bb 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -95,6 +95,7 @@ export const adminHostsAPI = { api.put("/hosts/bulk/groups", { hostIds, groupIds }), toggleAutoUpdate: (hostId, autoUpdate) => api.patch(`/hosts/${hostId}/auto-update`, { auto_update: autoUpdate }), + forceAgentUpdate: (hostId) => api.post(`/hosts/${hostId}/force-agent-update`), updateFriendlyName: (hostId, friendlyName) => api.patch(`/hosts/${hostId}/friendly-name`, { friendly_name: friendlyName, diff --git a/setup.sh b/setup.sh index c45de2b..b278cb3 100755 --- a/setup.sh +++ b/setup.sh @@ -1797,7 +1797,12 @@ create_agent_version() { cp "$APP_DIR/agents/patchmon-agent.sh" "$APP_DIR/backend/" print_status "Agent version management removed - using file-based approach" -# Ensure we close the conditional and the function properly + fi + + # Make agent binaries executable + if [ -d "$APP_DIR/agents" ]; then + chmod +x "$APP_DIR/agents/patchmon-agent-linux-"* 2>/dev/null || true + print_status "Agent binaries made executable" fi return 0 @@ -2703,6 +2708,13 @@ update_env_file() { : ${TFA_MAX_REMEMBER_SESSIONS:=5} : ${TFA_SUSPICIOUS_ACTIVITY_THRESHOLD:=3} + # Prisma Connection Pool + : ${DB_CONNECTION_LIMIT:=30} + : ${DB_POOL_TIMEOUT:=20} + : ${DB_CONNECT_TIMEOUT:=10} + : ${DB_IDLE_TIMEOUT:=300} + : ${DB_MAX_LIFETIME:=1800} + # Track which variables were added local added_vars=() @@ -2764,6 +2776,21 @@ update_env_file() { if ! grep -q "^TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=" "$env_file"; then added_vars+=("TFA_SUSPICIOUS_ACTIVITY_THRESHOLD") fi + if ! grep -q "^DB_CONNECTION_LIMIT=" "$env_file"; then + added_vars+=("DB_CONNECTION_LIMIT") + fi + if ! grep -q "^DB_POOL_TIMEOUT=" "$env_file"; then + added_vars+=("DB_POOL_TIMEOUT") + fi + if ! grep -q "^DB_CONNECT_TIMEOUT=" "$env_file"; then + added_vars+=("DB_CONNECT_TIMEOUT") + fi + if ! grep -q "^DB_IDLE_TIMEOUT=" "$env_file"; then + added_vars+=("DB_IDLE_TIMEOUT") + fi + if ! grep -q "^DB_MAX_LIFETIME=" "$env_file"; then + added_vars+=("DB_MAX_LIFETIME") + fi # If there are missing variables, add them if [ ${#added_vars[@]} -gt 0 ]; then @@ -2849,6 +2876,25 @@ EOF echo "TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=$TFA_SUSPICIOUS_ACTIVITY_THRESHOLD" >> "$env_file" fi + # Add Prisma connection pool config if missing + if printf '%s\n' "${added_vars[@]}" | grep -q "DB_CONNECTION_LIMIT"; then + echo "" >> "$env_file" + echo "# Database Connection Pool Configuration (Prisma)" >> "$env_file" + echo "DB_CONNECTION_LIMIT=$DB_CONNECTION_LIMIT" >> "$env_file" + fi + if printf '%s\n' "${added_vars[@]}" | grep -q "DB_POOL_TIMEOUT"; then + echo "DB_POOL_TIMEOUT=$DB_POOL_TIMEOUT" >> "$env_file" + fi + if printf '%s\n' "${added_vars[@]}" | grep -q "DB_CONNECT_TIMEOUT"; then + echo "DB_CONNECT_TIMEOUT=$DB_CONNECT_TIMEOUT" >> "$env_file" + fi + if printf '%s\n' "${added_vars[@]}" | grep -q "DB_IDLE_TIMEOUT"; then + echo "DB_IDLE_TIMEOUT=$DB_IDLE_TIMEOUT" >> "$env_file" + fi + if printf '%s\n' "${added_vars[@]}" | grep -q "DB_MAX_LIFETIME"; then + echo "DB_MAX_LIFETIME=$DB_MAX_LIFETIME" >> "$env_file" + fi + print_status ".env file updated with ${#added_vars[@]} new variable(s)" print_info "Added variables: ${added_vars[*]}" else @@ -3054,6 +3100,12 @@ update_installation() { print_info "Building frontend..." npm run build + # Make agent binaries executable + if [ -d "$instance_dir/agents" ]; then + chmod +x "$instance_dir/agents/patchmon-agent-linux-"* 2>/dev/null || true + print_status "Agent binaries made executable" + fi + # Run database migrations with self-healing print_info "Running database migrations..." cd "$instance_dir/backend"