mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-03 21:43:33 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			00abbc8c62
			...
			v1.3.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0189a307ef | ||
| 
						 | 
					50e546ee7e | ||
| 
						 | 
					2174abf395 | 
										
											Binary file not shown.
										
									
								
							@@ -341,20 +341,50 @@ const parseOrigins = (val) =>
 | 
			
		||||
		.map((s) => s.trim())
 | 
			
		||||
		.filter(Boolean);
 | 
			
		||||
const allowedOrigins = parseOrigins(
 | 
			
		||||
	process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || "http://fabio:3000",
 | 
			
		||||
	process.env.CORS_ORIGINS ||
 | 
			
		||||
		process.env.CORS_ORIGIN ||
 | 
			
		||||
		"http://localhost:3000",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// Add Bull Board origin to allowed origins if not already present
 | 
			
		||||
const bullBoardOrigin = process.env.CORS_ORIGIN || "http://localhost:3000";
 | 
			
		||||
if (!allowedOrigins.includes(bullBoardOrigin)) {
 | 
			
		||||
	allowedOrigins.push(bullBoardOrigin);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.use(
 | 
			
		||||
	cors({
 | 
			
		||||
		origin: (origin, callback) => {
 | 
			
		||||
			// Allow non-browser/SSR tools with no origin
 | 
			
		||||
			if (!origin) return callback(null, true);
 | 
			
		||||
			if (allowedOrigins.includes(origin)) return callback(null, true);
 | 
			
		||||
 | 
			
		||||
			// Allow Bull Board requests from the same origin as CORS_ORIGIN
 | 
			
		||||
			if (origin === bullBoardOrigin) return callback(null, true);
 | 
			
		||||
 | 
			
		||||
			// Allow same-origin requests (e.g., Bull Board accessing its own API)
 | 
			
		||||
			// This allows http://hostname:3001 to make requests to http://hostname:3001
 | 
			
		||||
			if (origin?.includes(":3001")) return callback(null, true);
 | 
			
		||||
 | 
			
		||||
			// Allow Bull Board requests from the frontend origin (same host, different port)
 | 
			
		||||
			// This handles cases where frontend is on port 3000 and backend on 3001
 | 
			
		||||
			const frontendOrigin = origin?.replace(/:3001$/, ":3000");
 | 
			
		||||
			if (frontendOrigin && allowedOrigins.includes(frontendOrigin)) {
 | 
			
		||||
				return callback(null, true);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return callback(new Error("Not allowed by CORS"));
 | 
			
		||||
		},
 | 
			
		||||
		credentials: true,
 | 
			
		||||
		// Additional CORS options for better cookie handling
 | 
			
		||||
		optionsSuccessStatus: 200,
 | 
			
		||||
		methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
 | 
			
		||||
		allowedHeaders: [
 | 
			
		||||
			"Content-Type",
 | 
			
		||||
			"Authorization",
 | 
			
		||||
			"Cookie",
 | 
			
		||||
			"X-Requested-With",
 | 
			
		||||
		],
 | 
			
		||||
	}),
 | 
			
		||||
);
 | 
			
		||||
app.use(limiter);
 | 
			
		||||
@@ -446,7 +476,7 @@ app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
 | 
			
		||||
 | 
			
		||||
// Bull Board - will be populated after queue manager initializes
 | 
			
		||||
let bullBoardRouter = null;
 | 
			
		||||
const bullBoardSessions = new Map(); // Store authenticated sessions
 | 
			
		||||
const _bullBoardSessions = new Map(); // Store authenticated sessions
 | 
			
		||||
 | 
			
		||||
// Mount Bull Board at /bullboard for cleaner URL
 | 
			
		||||
app.use(`/bullboard`, (_req, res, next) => {
 | 
			
		||||
@@ -456,16 +486,176 @@ app.use(`/bullboard`, (_req, res, next) => {
 | 
			
		||||
		res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-none");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add headers to help with WebSocket connections
 | 
			
		||||
	res.setHeader("X-Frame-Options", "SAMEORIGIN");
 | 
			
		||||
	res.setHeader(
 | 
			
		||||
		"Content-Security-Policy",
 | 
			
		||||
		"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' ws: wss:;",
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Authentication middleware for Bull Board
 | 
			
		||||
// Simplified Bull Board authentication - just validate token once and set a simple auth cookie
 | 
			
		||||
app.use(`/bullboard`, async (req, res, next) => {
 | 
			
		||||
	// Skip authentication for static assets only
 | 
			
		||||
	// Skip authentication for static assets
 | 
			
		||||
	if (req.path.includes("/static/") || req.path.includes("/favicon")) {
 | 
			
		||||
		return next();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check for existing Bull Board auth cookie
 | 
			
		||||
	if (req.cookies["bull-board-auth"]) {
 | 
			
		||||
		// Already authenticated, allow access
 | 
			
		||||
		return next();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// No auth cookie - check for token in query
 | 
			
		||||
	const token = req.query.token;
 | 
			
		||||
	if (!token) {
 | 
			
		||||
		return res.status(401).json({
 | 
			
		||||
			error:
 | 
			
		||||
				"Authentication required. Please access Bull Board from the Automation page.",
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Validate token and set auth cookie
 | 
			
		||||
	req.headers.authorization = `Bearer ${token}`;
 | 
			
		||||
	return authenticateToken(req, res, (err) => {
 | 
			
		||||
		if (err) {
 | 
			
		||||
			return res.status(401).json({ error: "Invalid authentication token" });
 | 
			
		||||
		}
 | 
			
		||||
		return requireAdmin(req, res, (adminErr) => {
 | 
			
		||||
			if (adminErr) {
 | 
			
		||||
				return res.status(403).json({ error: "Admin access required" });
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Set a simple auth cookie that will persist for the session
 | 
			
		||||
			res.cookie("bull-board-auth", token, {
 | 
			
		||||
				httpOnly: false,
 | 
			
		||||
				secure: false,
 | 
			
		||||
				maxAge: 3600000, // 1 hour
 | 
			
		||||
				path: "/bullboard",
 | 
			
		||||
				sameSite: "lax",
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			console.log("Bull Board - Authentication successful, cookie set");
 | 
			
		||||
			return next();
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Remove all the old complex middleware below and replace with the new Bull Board router setup
 | 
			
		||||
app.use(`/bullboard`, (req, res, next) => {
 | 
			
		||||
	if (bullBoardRouter) {
 | 
			
		||||
		return bullBoardRouter(req, res, next);
 | 
			
		||||
	}
 | 
			
		||||
	return res.status(503).json({ error: "Bull Board not initialized yet" });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
// OLD MIDDLEWARE - REMOVED FOR SIMPLIFICATION - DO NOT USE
 | 
			
		||||
if (false) {
 | 
			
		||||
		const sessionId = req.cookies["bull-board-session"];
 | 
			
		||||
		console.log("Bull Board API call - Session ID:", sessionId ? "present" : "missing");
 | 
			
		||||
		console.log("Bull Board API call - Cookies:", req.cookies);
 | 
			
		||||
		console.log("Bull Board API call - Bull Board token cookie:", req.cookies["bull-board-token"] ? "present" : "missing");
 | 
			
		||||
		console.log("Bull Board API call - Query token:", req.query.token ? "present" : "missing");
 | 
			
		||||
		console.log("Bull Board API call - Auth header:", req.headers.authorization ? "present" : "missing");
 | 
			
		||||
		console.log("Bull Board API call - Origin:", req.headers.origin || "missing");
 | 
			
		||||
		console.log("Bull Board API call - Referer:", req.headers.referer || "missing");
 | 
			
		||||
		
 | 
			
		||||
		// Check if we have any authentication method available
 | 
			
		||||
		const hasSession = !!sessionId;
 | 
			
		||||
		const hasTokenCookie = !!req.cookies["bull-board-token"];
 | 
			
		||||
		const hasQueryToken = !!req.query.token;
 | 
			
		||||
		const hasAuthHeader = !!req.headers.authorization;
 | 
			
		||||
		const hasReferer = !!req.headers.referer;
 | 
			
		||||
		
 | 
			
		||||
		console.log("Bull Board API call - Auth methods available:", {
 | 
			
		||||
			session: hasSession,
 | 
			
		||||
			tokenCookie: hasTokenCookie,
 | 
			
		||||
			queryToken: hasQueryToken,
 | 
			
		||||
			authHeader: hasAuthHeader,
 | 
			
		||||
			referer: hasReferer
 | 
			
		||||
		});
 | 
			
		||||
		
 | 
			
		||||
		// Check for valid session first
 | 
			
		||||
		if (sessionId) {
 | 
			
		||||
			const session = bullBoardSessions.get(sessionId);
 | 
			
		||||
			console.log("Bull Board API call - Session found:", !!session);
 | 
			
		||||
			if (session && Date.now() - session.timestamp < 3600000) {
 | 
			
		||||
				// Valid session, extend it
 | 
			
		||||
				session.timestamp = Date.now();
 | 
			
		||||
				console.log("Bull Board API call - Using existing session, proceeding");
 | 
			
		||||
				return next();
 | 
			
		||||
			} else if (session) {
 | 
			
		||||
				// Expired session, remove it
 | 
			
		||||
				console.log("Bull Board API call - Session expired, removing");
 | 
			
		||||
				bullBoardSessions.delete(sessionId);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		// No valid session, check for token as fallback
 | 
			
		||||
		let token = req.query.token;
 | 
			
		||||
		if (!token && req.headers.authorization) {
 | 
			
		||||
			token = req.headers.authorization.replace("Bearer ", "");
 | 
			
		||||
		}
 | 
			
		||||
		if (!token && req.cookies["bull-board-token"]) {
 | 
			
		||||
			token = req.cookies["bull-board-token"];
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		// For API calls, also check if the token is in the referer URL
 | 
			
		||||
		// This handles cases where the main page hasn't set the cookie yet
 | 
			
		||||
		if (!token && req.headers.referer) {
 | 
			
		||||
			try {
 | 
			
		||||
				const refererUrl = new URL(req.headers.referer);
 | 
			
		||||
				const refererToken = refererUrl.searchParams.get('token');
 | 
			
		||||
				if (refererToken) {
 | 
			
		||||
					token = refererToken;
 | 
			
		||||
					console.log("Bull Board API call - Token found in referer URL:", refererToken.substring(0, 20) + "...");
 | 
			
		||||
				} else {
 | 
			
		||||
					console.log("Bull Board API call - No token found in referer URL");
 | 
			
		||||
					// If no token in referer and no session, return 401 with redirect info
 | 
			
		||||
					if (!sessionId) {
 | 
			
		||||
						console.log("Bull Board API call - No authentication available, returning 401");
 | 
			
		||||
						return res.status(401).json({ 
 | 
			
		||||
							error: "Authentication required", 
 | 
			
		||||
							message: "Please refresh the page to re-authenticate"
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.log("Bull Board API call - Error parsing referer URL:", error.message);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		if (token) {
 | 
			
		||||
			console.log("Bull Board API call - Token found, authenticating");
 | 
			
		||||
			// Add token to headers for authentication
 | 
			
		||||
			req.headers.authorization = `Bearer ${token}`;
 | 
			
		||||
			
 | 
			
		||||
			// Authenticate the user
 | 
			
		||||
			return authenticateToken(req, res, (err) => {
 | 
			
		||||
				if (err) {
 | 
			
		||||
					console.log("Bull Board API call - Token authentication failed");
 | 
			
		||||
					return res.status(401).json({ error: "Authentication failed" });
 | 
			
		||||
				}
 | 
			
		||||
				return requireAdmin(req, res, (adminErr) => {
 | 
			
		||||
					if (adminErr) {
 | 
			
		||||
						console.log("Bull Board API call - Admin access required");
 | 
			
		||||
						return res.status(403).json({ error: "Admin access required" });
 | 
			
		||||
					}
 | 
			
		||||
					console.log("Bull Board API call - Token authentication successful");
 | 
			
		||||
					return next();
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		// No valid session or token for API calls, deny access
 | 
			
		||||
		console.log("Bull Board API call - No valid session or token, denying access");
 | 
			
		||||
		return res.status(401).json({ error: "Valid Bull Board session or token required" });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check for bull-board-session cookie first
 | 
			
		||||
	const sessionId = req.cookies["bull-board-session"];
 | 
			
		||||
	if (sessionId) {
 | 
			
		||||
@@ -486,6 +676,9 @@ app.use(`/bullboard`, async (req, res, next) => {
 | 
			
		||||
	if (!token && req.headers.authorization) {
 | 
			
		||||
		token = req.headers.authorization.replace("Bearer ", "");
 | 
			
		||||
	}
 | 
			
		||||
	if (!token && req.cookies["bull-board-token"]) {
 | 
			
		||||
		token = req.cookies["bull-board-token"];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If no token, deny access
 | 
			
		||||
	if (!token) {
 | 
			
		||||
@@ -514,13 +707,23 @@ app.use(`/bullboard`, async (req, res, next) => {
 | 
			
		||||
				userId: req.user.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Set session cookie
 | 
			
		||||
			res.cookie("bull-board-session", newSessionId, {
 | 
			
		||||
			// Set session cookie with proper configuration for domain access
 | 
			
		||||
			const isHttps = process.env.NODE_ENV === "production" || process.env.SERVER_PROTOCOL === "https";
 | 
			
		||||
			const cookieOptions = {
 | 
			
		||||
				httpOnly: true,
 | 
			
		||||
				secure: process.env.NODE_ENV === "production",
 | 
			
		||||
				sameSite: "lax",
 | 
			
		||||
				secure: isHttps,
 | 
			
		||||
				maxAge: 3600000, // 1 hour
 | 
			
		||||
			});
 | 
			
		||||
				path: "/", // Set path to root so it's available for all Bull Board requests
 | 
			
		||||
			};
 | 
			
		||||
			
 | 
			
		||||
			// Configure sameSite based on protocol and environment
 | 
			
		||||
			if (isHttps) {
 | 
			
		||||
				cookieOptions.sameSite = "none"; // Required for HTTPS cross-origin
 | 
			
		||||
			} else {
 | 
			
		||||
				cookieOptions.sameSite = "lax"; // Better for HTTP same-origin
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			res.cookie("bull-board-session", newSessionId, cookieOptions);
 | 
			
		||||
 | 
			
		||||
			// Clean up old sessions periodically
 | 
			
		||||
			if (bullBoardSessions.size > 100) {
 | 
			
		||||
@@ -536,13 +739,111 @@ app.use(`/bullboard`, async (req, res, next) => {
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// Second middleware block - COMMENTED OUT - using simplified version above instead
 | 
			
		||||
/*
 | 
			
		||||
app.use(`/bullboard`, (req, res, next) => {
 | 
			
		||||
	if (bullBoardRouter) {
 | 
			
		||||
		// If this is the main Bull Board page (not an API call), inject the token and create session
 | 
			
		||||
		if (!req.path.includes("/api/") && !req.path.includes("/static/") && req.path === "/bullboard") {
 | 
			
		||||
			const token = req.query.token;
 | 
			
		||||
			console.log("Bull Board main page - Token:", token ? "present" : "missing");
 | 
			
		||||
			console.log("Bull Board main page - Query params:", req.query);
 | 
			
		||||
			console.log("Bull Board main page - Origin:", req.headers.origin || "missing");
 | 
			
		||||
			console.log("Bull Board main page - Referer:", req.headers.referer || "missing");
 | 
			
		||||
			console.log("Bull Board main page - Cookies:", req.cookies);
 | 
			
		||||
			
 | 
			
		||||
			if (token) {
 | 
			
		||||
				// Authenticate the user and create a session immediately on page load
 | 
			
		||||
				req.headers.authorization = `Bearer ${token}`;
 | 
			
		||||
				
 | 
			
		||||
				return authenticateToken(req, res, (err) => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						console.log("Bull Board main page - Token authentication failed");
 | 
			
		||||
						return res.status(401).json({ error: "Authentication failed" });
 | 
			
		||||
					}
 | 
			
		||||
					return requireAdmin(req, res, (adminErr) => {
 | 
			
		||||
						if (adminErr) {
 | 
			
		||||
							console.log("Bull Board main page - Admin access required");
 | 
			
		||||
							return res.status(403).json({ error: "Admin access required" });
 | 
			
		||||
						}
 | 
			
		||||
						
 | 
			
		||||
						console.log("Bull Board main page - Token authentication successful, creating session");
 | 
			
		||||
						
 | 
			
		||||
						// Create a Bull Board session immediately
 | 
			
		||||
						const newSessionId = require("node:crypto")
 | 
			
		||||
							.randomBytes(32)
 | 
			
		||||
							.toString("hex");
 | 
			
		||||
						bullBoardSessions.set(newSessionId, {
 | 
			
		||||
							timestamp: Date.now(),
 | 
			
		||||
							userId: req.user.id,
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						// Set session cookie with proper configuration for domain access
 | 
			
		||||
						const sessionCookieOptions = {
 | 
			
		||||
							httpOnly: true,
 | 
			
		||||
							secure: false, // Always false for HTTP
 | 
			
		||||
							maxAge: 3600000, // 1 hour
 | 
			
		||||
							path: "/", // Set path to root so it's available for all Bull Board requests
 | 
			
		||||
							sameSite: "lax", // Always lax for HTTP
 | 
			
		||||
						};
 | 
			
		||||
						
 | 
			
		||||
						res.cookie("bull-board-session", newSessionId, sessionCookieOptions);
 | 
			
		||||
						console.log("Bull Board main page - Session created:", newSessionId);
 | 
			
		||||
						console.log("Bull Board main page - Cookie options:", sessionCookieOptions);
 | 
			
		||||
						
 | 
			
		||||
						// Also set a token cookie for API calls as a fallback
 | 
			
		||||
						const tokenCookieOptions = {
 | 
			
		||||
							httpOnly: false, // Allow JavaScript to access it
 | 
			
		||||
							secure: false, // Always false for HTTP
 | 
			
		||||
							maxAge: 3600000, // 1 hour
 | 
			
		||||
							path: "/", // Set path to root for broader compatibility
 | 
			
		||||
							sameSite: "lax", // Always lax for HTTP
 | 
			
		||||
						};
 | 
			
		||||
						
 | 
			
		||||
						res.cookie("bull-board-token", token, tokenCookieOptions);
 | 
			
		||||
						console.log("Bull Board main page - Token cookie also set for API fallback");
 | 
			
		||||
						
 | 
			
		||||
						// Clean up old sessions periodically
 | 
			
		||||
						if (bullBoardSessions.size > 100) {
 | 
			
		||||
							const now = Date.now();
 | 
			
		||||
							for (const [sid, session] of bullBoardSessions.entries()) {
 | 
			
		||||
								if (now - session.timestamp > 3600000) {
 | 
			
		||||
									bullBoardSessions.delete(sid);
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
						
 | 
			
		||||
						// Now proceed to serve the Bull Board page
 | 
			
		||||
						return bullBoardRouter(req, res, next);
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				console.log("Bull Board main page - No token provided, checking for existing session");
 | 
			
		||||
				// Check if we have an existing session
 | 
			
		||||
				const sessionId = req.cookies["bull-board-session"];
 | 
			
		||||
				if (sessionId) {
 | 
			
		||||
					const session = bullBoardSessions.get(sessionId);
 | 
			
		||||
					if (session && Date.now() - session.timestamp < 3600000) {
 | 
			
		||||
						console.log("Bull Board main page - Using existing session");
 | 
			
		||||
						// Extend session
 | 
			
		||||
						session.timestamp = Date.now();
 | 
			
		||||
						return bullBoardRouter(req, res, next);
 | 
			
		||||
					} else if (session) {
 | 
			
		||||
						console.log("Bull Board main page - Session expired, removing");
 | 
			
		||||
						bullBoardSessions.delete(sessionId);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				console.log("Bull Board main page - No valid session, denying access");
 | 
			
		||||
				return res.status(401).json({ error: "Access token required" });
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return bullBoardRouter(req, res, next);
 | 
			
		||||
	}
 | 
			
		||||
	return res.status(503).json({ error: "Bull Board not initialized yet" });
 | 
			
		||||
});
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// Error handler specifically for Bull Board routes
 | 
			
		||||
app.use("/bullboard", (err, req, res, _next) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
const axios = require("axios");
 | 
			
		||||
const fs = require("node:fs").promises;
 | 
			
		||||
const path = require("node:path");
 | 
			
		||||
const { exec } = require("node:child_process");
 | 
			
		||||
const { exec, spawn } = require("node:child_process");
 | 
			
		||||
const { promisify } = require("node:util");
 | 
			
		||||
const execAsync = promisify(exec);
 | 
			
		||||
const _execAsync = promisify(exec);
 | 
			
		||||
 | 
			
		||||
// Simple semver comparison function
 | 
			
		||||
function compareVersions(version1, version2) {
 | 
			
		||||
@@ -135,16 +135,34 @@ class AgentVersionService {
 | 
			
		||||
 | 
			
		||||
			// Execute the agent binary with help flag to get version info
 | 
			
		||||
			try {
 | 
			
		||||
				const { stdout, stderr } = await execAsync(`${agentPath} --help`, {
 | 
			
		||||
				const child = spawn(agentPath, ["--help"], {
 | 
			
		||||
					timeout: 10000,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (stderr) {
 | 
			
		||||
					console.log("⚠️ Agent help stderr:", stderr);
 | 
			
		||||
				let stdout = "";
 | 
			
		||||
				let stderr = "";
 | 
			
		||||
 | 
			
		||||
				child.stdout.on("data", (data) => {
 | 
			
		||||
					stdout += data.toString();
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				child.stderr.on("data", (data) => {
 | 
			
		||||
					stderr += data.toString();
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				const result = await new Promise((resolve, reject) => {
 | 
			
		||||
					child.on("close", (code) => {
 | 
			
		||||
						resolve({ stdout, stderr, code });
 | 
			
		||||
					});
 | 
			
		||||
					child.on("error", reject);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (result.stderr) {
 | 
			
		||||
					console.log("⚠️ Agent help stderr:", result.stderr);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Parse version from help output (e.g., "PatchMon Agent v1.3.0")
 | 
			
		||||
				const versionMatch = stdout.match(
 | 
			
		||||
				const versionMatch = result.stdout.match(
 | 
			
		||||
					/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
 | 
			
		||||
				);
 | 
			
		||||
				if (versionMatch) {
 | 
			
		||||
@@ -153,7 +171,7 @@ class AgentVersionService {
 | 
			
		||||
				} else {
 | 
			
		||||
					console.log(
 | 
			
		||||
						"⚠️ Could not parse version from agent help output:",
 | 
			
		||||
						stdout,
 | 
			
		||||
						result.stdout,
 | 
			
		||||
					);
 | 
			
		||||
					this.currentVersion = null;
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,37 @@ function init(server, prismaClient) {
 | 
			
		||||
	server.on("upgrade", async (request, socket, head) => {
 | 
			
		||||
		try {
 | 
			
		||||
			const { pathname } = url.parse(request.url);
 | 
			
		||||
			if (!pathname || !pathname.startsWith("/api/")) {
 | 
			
		||||
			if (!pathname) {
 | 
			
		||||
				socket.destroy();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Handle Bull Board WebSocket connections
 | 
			
		||||
			if (pathname.startsWith("/bullboard")) {
 | 
			
		||||
				// For Bull Board, we need to check if the user is authenticated
 | 
			
		||||
				// Check for session cookie or authorization header
 | 
			
		||||
				const sessionCookie = request.headers.cookie?.match(
 | 
			
		||||
					/bull-board-session=([^;]+)/,
 | 
			
		||||
				)?.[1];
 | 
			
		||||
				const authHeader = request.headers.authorization;
 | 
			
		||||
 | 
			
		||||
				if (!sessionCookie && !authHeader) {
 | 
			
		||||
					socket.destroy();
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Accept the WebSocket connection for Bull Board
 | 
			
		||||
				wss.handleUpgrade(request, socket, head, (ws) => {
 | 
			
		||||
					ws.on("message", (message) => {
 | 
			
		||||
						// Echo back for Bull Board WebSocket
 | 
			
		||||
						ws.send(message);
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Handle agent WebSocket connections
 | 
			
		||||
			if (!pathname.startsWith("/api/")) {
 | 
			
		||||
				socket.destroy();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ ENV NODE_ENV=development \
 | 
			
		||||
    PM_LOG_TO_CONSOLE=true \
 | 
			
		||||
    PORT=3001
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache openssl tini curl
 | 
			
		||||
RUN apk add --no-cache openssl tini curl libc6-compat
 | 
			
		||||
 | 
			
		||||
USER node
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +64,7 @@ ENV NODE_ENV=production \
 | 
			
		||||
    JWT_REFRESH_EXPIRES_IN=7d \
 | 
			
		||||
    SESSION_INACTIVITY_TIMEOUT_MINUTES=30
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache openssl tini curl
 | 
			
		||||
RUN apk add --no-cache openssl tini curl libc6-compat
 | 
			
		||||
 | 
			
		||||
USER node
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ log() {
 | 
			
		||||
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Function to extract version from agent script
 | 
			
		||||
# Function to extract version from agent script (legacy)
 | 
			
		||||
get_agent_version() {
 | 
			
		||||
    local file="$1"
 | 
			
		||||
    if [ -f "$file" ]; then
 | 
			
		||||
@@ -18,6 +18,32 @@ get_agent_version() {
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Function to get version from binary using --help flag
 | 
			
		||||
get_binary_version() {
 | 
			
		||||
    local binary="$1"
 | 
			
		||||
    if [ -f "$binary" ]; then
 | 
			
		||||
        # Make sure binary is executable
 | 
			
		||||
        chmod +x "$binary" 2>/dev/null || true
 | 
			
		||||
        
 | 
			
		||||
        # Try to execute the binary and extract version from help output
 | 
			
		||||
        # The Go binary shows version in the --help output as "PatchMon Agent v1.3.0"
 | 
			
		||||
        local version=$("$binary" --help 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 | tr -d 'v')
 | 
			
		||||
        if [ -n "$version" ]; then
 | 
			
		||||
            echo "$version"
 | 
			
		||||
        else
 | 
			
		||||
            # Fallback: try --version flag
 | 
			
		||||
            version=$("$binary" --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
 | 
			
		||||
            if [ -n "$version" ]; then
 | 
			
		||||
                echo "$version"
 | 
			
		||||
            else
 | 
			
		||||
                echo "0.0.0"
 | 
			
		||||
            fi
 | 
			
		||||
        fi
 | 
			
		||||
    else
 | 
			
		||||
        echo "0.0.0"
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Function to compare versions (returns 0 if $1 > $2)
 | 
			
		||||
version_greater() {
 | 
			
		||||
    # Use sort -V for version comparison
 | 
			
		||||
@@ -28,6 +54,8 @@ version_greater() {
 | 
			
		||||
update_agents() {
 | 
			
		||||
    local backup_agent="/app/agents_backup/patchmon-agent.sh"
 | 
			
		||||
    local current_agent="/app/agents/patchmon-agent.sh"
 | 
			
		||||
    local backup_binary="/app/agents_backup/patchmon-agent-linux-amd64"
 | 
			
		||||
    local current_binary="/app/agents/patchmon-agent-linux-amd64"
 | 
			
		||||
    
 | 
			
		||||
    # Check if agents directory exists
 | 
			
		||||
    if [ ! -d "/app/agents" ]; then
 | 
			
		||||
@@ -41,54 +69,72 @@ update_agents() {
 | 
			
		||||
        return 0
 | 
			
		||||
    fi
 | 
			
		||||
    
 | 
			
		||||
    # Get versions
 | 
			
		||||
    local backup_version=$(get_agent_version "$backup_agent")
 | 
			
		||||
    local current_version=$(get_agent_version "$current_agent")
 | 
			
		||||
    # Get versions from both script and binary
 | 
			
		||||
    local backup_script_version=$(get_agent_version "$backup_agent")
 | 
			
		||||
    local current_script_version=$(get_agent_version "$current_agent")
 | 
			
		||||
    local backup_binary_version=$(get_binary_version "$backup_binary")
 | 
			
		||||
    local current_binary_version=$(get_binary_version "$current_binary")
 | 
			
		||||
    
 | 
			
		||||
    log "Agent version check:"
 | 
			
		||||
    log "  Image version: ${backup_version}"
 | 
			
		||||
    log "  Volume version: ${current_version}"
 | 
			
		||||
    log "  Image script version: ${backup_script_version}"
 | 
			
		||||
    log "  Volume script version: ${current_script_version}"
 | 
			
		||||
    log "  Image binary version: ${backup_binary_version}"
 | 
			
		||||
    log "  Volume binary version: ${current_binary_version}"
 | 
			
		||||
    
 | 
			
		||||
    # Determine if update is needed
 | 
			
		||||
    local needs_update=0
 | 
			
		||||
    
 | 
			
		||||
    # Case 1: No agents in volume (first time setup)
 | 
			
		||||
    if [ -z "$(find /app/agents -maxdepth 1 -type f -name '*.sh' 2>/dev/null | head -n 1)" ]; then
 | 
			
		||||
    # Case 1: No agents in volume at all (first time setup)
 | 
			
		||||
    if [ -z "$(find /app/agents -maxdepth 1 -type f 2>/dev/null | head -n 1)" ]; then
 | 
			
		||||
        log "Agents directory is empty - performing initial copy"
 | 
			
		||||
        needs_update=1
 | 
			
		||||
    # Case 2: Backup version is newer
 | 
			
		||||
    elif version_greater "$backup_version" "$current_version"; then
 | 
			
		||||
        log "Newer agent version available (${backup_version} > ${current_version})"
 | 
			
		||||
    # Case 2: Binary exists but backup binary is newer
 | 
			
		||||
    elif [ "$current_binary_version" != "0.0.0" ] && version_greater "$backup_binary_version" "$current_binary_version"; then
 | 
			
		||||
        log "Newer agent binary available (${backup_binary_version} > ${current_binary_version})"
 | 
			
		||||
        needs_update=1
 | 
			
		||||
    # Case 3: No binary in volume, but shell scripts exist (legacy setup) - copy binaries
 | 
			
		||||
    elif [ "$current_binary_version" = "0.0.0" ] && [ "$backup_binary_version" != "0.0.0" ]; then
 | 
			
		||||
        log "No binary found in volume but backup has binaries - performing update"
 | 
			
		||||
        needs_update=1
 | 
			
		||||
    else
 | 
			
		||||
        log "Agents are up to date"
 | 
			
		||||
        log "Agents are up to date (binary: ${current_binary_version})"
 | 
			
		||||
        needs_update=0
 | 
			
		||||
    fi
 | 
			
		||||
    
 | 
			
		||||
    # Perform update if needed
 | 
			
		||||
    if [ $needs_update -eq 1 ]; then
 | 
			
		||||
        log "Updating agents to version ${backup_version}..."
 | 
			
		||||
        log "Updating agents to version ${backup_binary_version}..."
 | 
			
		||||
        
 | 
			
		||||
        # Create backup of existing agents if they exist
 | 
			
		||||
        if [ -f "$current_agent" ]; then
 | 
			
		||||
        if [ -f "$current_agent" ] || [ -f "$current_binary" ]; then
 | 
			
		||||
            local backup_timestamp=$(date +%Y%m%d_%H%M%S)
 | 
			
		||||
            local backup_name="/app/agents/patchmon-agent.sh.backup.${backup_timestamp}"
 | 
			
		||||
            cp "$current_agent" "$backup_name" 2>/dev/null || true
 | 
			
		||||
            log "Previous agent backed up to: $(basename $backup_name)"
 | 
			
		||||
            mkdir -p "/app/agents/backups"
 | 
			
		||||
            
 | 
			
		||||
            # Backup shell script if it exists
 | 
			
		||||
            if [ -f "$current_agent" ]; then
 | 
			
		||||
                cp "$current_agent" "/app/agents/backups/patchmon-agent.sh.${backup_timestamp}" 2>/dev/null || true
 | 
			
		||||
                log "Previous script backed up"
 | 
			
		||||
            fi
 | 
			
		||||
            
 | 
			
		||||
            # Backup binary if it exists
 | 
			
		||||
            if [ -f "$current_binary" ]; then
 | 
			
		||||
                cp "$current_binary" "/app/agents/backups/patchmon-agent-linux-amd64.${backup_timestamp}" 2>/dev/null || true
 | 
			
		||||
                log "Previous binary backed up"
 | 
			
		||||
            fi
 | 
			
		||||
        fi
 | 
			
		||||
        
 | 
			
		||||
        # Copy new agents
 | 
			
		||||
        # Copy new agents (both scripts and binaries)
 | 
			
		||||
        cp -r /app/agents_backup/* /app/agents/
 | 
			
		||||
        
 | 
			
		||||
        # Make agent binaries executable
 | 
			
		||||
        chmod +x /app/agents/patchmon-agent-linux-* 2>/dev/null || true
 | 
			
		||||
        
 | 
			
		||||
        # Verify update
 | 
			
		||||
        local new_version=$(get_agent_version "$current_agent")
 | 
			
		||||
        if [ "$new_version" = "$backup_version" ]; then
 | 
			
		||||
            log "✅ Agents successfully updated to version ${new_version}"
 | 
			
		||||
        local new_binary_version=$(get_binary_version "$current_binary")
 | 
			
		||||
        if [ "$new_binary_version" = "$backup_binary_version" ]; then
 | 
			
		||||
            log "✅ Agents successfully updated to version ${new_binary_version}"
 | 
			
		||||
        else
 | 
			
		||||
            log "⚠️ Warning: Agent update may have failed (expected: ${backup_version}, got: ${new_version})"
 | 
			
		||||
            log "⚠️ Warning: Agent update may have failed (expected: ${backup_binary_version}, got: ${new_binary_version})"
 | 
			
		||||
        fi
 | 
			
		||||
    fi
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,17 +35,20 @@ server {
 | 
			
		||||
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
        proxy_set_header X-Forwarded-Host $host;
 | 
			
		||||
        proxy_set_header Cookie $http_cookie;  # Forward cookies to backend
 | 
			
		||||
        proxy_cache_bypass $http_upgrade;
 | 
			
		||||
        proxy_read_timeout 300s;
 | 
			
		||||
        proxy_connect_timeout 75s;
 | 
			
		||||
        
 | 
			
		||||
        # Enable cookie passthrough in both directions
 | 
			
		||||
        proxy_pass_header Set-Cookie;
 | 
			
		||||
        proxy_cookie_path / /;
 | 
			
		||||
 | 
			
		||||
        # Preserve original client IP through proxy chain
 | 
			
		||||
        proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
 | 
			
		||||
 | 
			
		||||
        # CORS headers for Bull Board
 | 
			
		||||
        add_header Access-Control-Allow-Origin * always;
 | 
			
		||||
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
 | 
			
		||||
        add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
 | 
			
		||||
        # CORS headers for Bull Board - let backend handle CORS properly
 | 
			
		||||
        # Note: Backend handles CORS with proper origin validation and credentials
 | 
			
		||||
 | 
			
		||||
        # Handle preflight requests
 | 
			
		||||
        if ($request_method = 'OPTIONS') {
 | 
			
		||||
@@ -78,10 +81,8 @@ server {
 | 
			
		||||
        # Preserve original client IP through proxy chain
 | 
			
		||||
        proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
 | 
			
		||||
 | 
			
		||||
        # CORS headers for API calls - even though backend is doing it
 | 
			
		||||
        add_header Access-Control-Allow-Origin * always;
 | 
			
		||||
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
 | 
			
		||||
        add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
 | 
			
		||||
        # CORS headers for API calls - let backend handle CORS properly
 | 
			
		||||
        # Note: Backend handles CORS with proper origin validation and credentials
 | 
			
		||||
 | 
			
		||||
        # Handle preflight requests
 | 
			
		||||
        if ($request_method = 'OPTIONS') {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,33 @@
 | 
			
		||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { AlertCircle, CheckCircle, Clock, RefreshCw } from "lucide-react";
 | 
			
		||||
import {
 | 
			
		||||
	AlertCircle,
 | 
			
		||||
	CheckCircle,
 | 
			
		||||
	Clock,
 | 
			
		||||
	Download,
 | 
			
		||||
	ExternalLink,
 | 
			
		||||
	RefreshCw,
 | 
			
		||||
	X,
 | 
			
		||||
} from "lucide-react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import api from "../../utils/api";
 | 
			
		||||
 | 
			
		||||
const AgentManagementTab = () => {
 | 
			
		||||
	const _queryClient = useQueryClient();
 | 
			
		||||
	const [toast, setToast] = useState(null);
 | 
			
		||||
 | 
			
		||||
	// Auto-hide toast after 5 seconds
 | 
			
		||||
	useEffect(() => {
 | 
			
		||||
		if (toast) {
 | 
			
		||||
			const timer = setTimeout(() => {
 | 
			
		||||
				setToast(null);
 | 
			
		||||
			}, 5000);
 | 
			
		||||
			return () => clearTimeout(timer);
 | 
			
		||||
		}
 | 
			
		||||
	}, [toast]);
 | 
			
		||||
 | 
			
		||||
	const showToast = (message, type = "success") => {
 | 
			
		||||
		setToast({ message, type });
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// Agent version queries
 | 
			
		||||
	const {
 | 
			
		||||
@@ -57,9 +81,11 @@ const AgentManagementTab = () => {
 | 
			
		||||
		},
 | 
			
		||||
		onSuccess: () => {
 | 
			
		||||
			refetchVersion();
 | 
			
		||||
			showToast("Successfully checked for updates", "success");
 | 
			
		||||
		},
 | 
			
		||||
		onError: (error) => {
 | 
			
		||||
			console.error("Check updates error:", error);
 | 
			
		||||
			showToast(`Failed to check for updates: ${error.message}`, "error");
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -79,11 +105,11 @@ const AgentManagementTab = () => {
 | 
			
		||||
			// Show success message
 | 
			
		||||
			const message =
 | 
			
		||||
				data.data?.message || "Agent binaries downloaded successfully";
 | 
			
		||||
			alert(`✅ ${message}`);
 | 
			
		||||
			showToast(message, "success");
 | 
			
		||||
		},
 | 
			
		||||
		onError: (error) => {
 | 
			
		||||
			console.error("Download update error:", error);
 | 
			
		||||
			alert(`❌ Download failed: ${error.message}`);
 | 
			
		||||
			showToast(`Download failed: ${error.message}`, "error");
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -173,111 +199,255 @@ const AgentManagementTab = () => {
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<div className="space-y-6">
 | 
			
		||||
			{/* Header */}
 | 
			
		||||
			<div className="flex items-center justify-between mb-6">
 | 
			
		||||
				<div>
 | 
			
		||||
					<h2 className="text-2xl font-bold text-secondary-900 dark:text-white">
 | 
			
		||||
						Agent Version Management
 | 
			
		||||
					</h2>
 | 
			
		||||
					<p className="text-secondary-600 dark:text-secondary-300">
 | 
			
		||||
						Monitor agent versions and download updates
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div className="flex space-x-3">
 | 
			
		||||
					<button
 | 
			
		||||
						type="button"
 | 
			
		||||
						onClick={() => checkUpdatesMutation.mutate()}
 | 
			
		||||
						disabled={checkUpdatesMutation.isPending}
 | 
			
		||||
						className="flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
 | 
			
		||||
			{/* Toast Notification */}
 | 
			
		||||
			{toast && (
 | 
			
		||||
				<div
 | 
			
		||||
					className={`fixed top-4 right-4 z-50 max-w-md rounded-lg shadow-lg border-2 p-4 flex items-start space-x-3 animate-in slide-in-from-top-5 ${
 | 
			
		||||
						toast.type === "success"
 | 
			
		||||
							? "bg-green-50 dark:bg-green-900/90 border-green-500 dark:border-green-600"
 | 
			
		||||
							: "bg-red-50 dark:bg-red-900/90 border-red-500 dark:border-red-600"
 | 
			
		||||
					}`}
 | 
			
		||||
				>
 | 
			
		||||
					<div
 | 
			
		||||
						className={`flex-shrink-0 rounded-full p-1 ${
 | 
			
		||||
							toast.type === "success"
 | 
			
		||||
								? "bg-green-100 dark:bg-green-800"
 | 
			
		||||
								: "bg-red-100 dark:bg-red-800"
 | 
			
		||||
						}`}
 | 
			
		||||
					>
 | 
			
		||||
						<RefreshCw
 | 
			
		||||
							className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
 | 
			
		||||
						/>
 | 
			
		||||
						Check Updates
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{/* Download Updates Button */}
 | 
			
		||||
			<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
 | 
			
		||||
				<div className="flex items-center justify-between">
 | 
			
		||||
					<div>
 | 
			
		||||
						<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
							{versionInfo?.currentVersion
 | 
			
		||||
								? "Download Agent Updates"
 | 
			
		||||
								: "Download Agent Binaries"}
 | 
			
		||||
						</h3>
 | 
			
		||||
						<p className="text-secondary-600 dark:text-secondary-300">
 | 
			
		||||
							{versionInfo?.currentVersion
 | 
			
		||||
								? "Download the latest agent binaries from GitHub"
 | 
			
		||||
								: "No agent binaries found. Download from GitHub to get started."}
 | 
			
		||||
						{toast.type === "success" ? (
 | 
			
		||||
							<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
 | 
			
		||||
						) : (
 | 
			
		||||
							<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
 | 
			
		||||
						)}
 | 
			
		||||
					</div>
 | 
			
		||||
					<div className="flex-1">
 | 
			
		||||
						<p
 | 
			
		||||
							className={`text-sm font-medium ${
 | 
			
		||||
								toast.type === "success"
 | 
			
		||||
									? "text-green-800 dark:text-green-100"
 | 
			
		||||
									: "text-red-800 dark:text-red-100"
 | 
			
		||||
							}`}
 | 
			
		||||
						>
 | 
			
		||||
							{toast.message}
 | 
			
		||||
						</p>
 | 
			
		||||
					</div>
 | 
			
		||||
					<button
 | 
			
		||||
						type="button"
 | 
			
		||||
						onClick={() => downloadUpdateMutation.mutate()}
 | 
			
		||||
						disabled={downloadUpdateMutation.isPending}
 | 
			
		||||
						className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
 | 
			
		||||
						onClick={() => setToast(null)}
 | 
			
		||||
						className={`flex-shrink-0 rounded-lg p-1 transition-colors ${
 | 
			
		||||
							toast.type === "success"
 | 
			
		||||
								? "hover:bg-green-100 dark:hover:bg-green-800 text-green-600 dark:text-green-400"
 | 
			
		||||
								: "hover:bg-red-100 dark:hover:bg-red-800 text-red-600 dark:text-red-400"
 | 
			
		||||
						}`}
 | 
			
		||||
					>
 | 
			
		||||
						<X className="h-4 w-4" />
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			)}
 | 
			
		||||
 | 
			
		||||
			{/* Header */}
 | 
			
		||||
			<div className="mb-6">
 | 
			
		||||
				<h2 className="text-2xl font-bold text-secondary-900 dark:text-white mb-2">
 | 
			
		||||
					Agent Version Management
 | 
			
		||||
				</h2>
 | 
			
		||||
				<p className="text-secondary-600 dark:text-secondary-400">
 | 
			
		||||
					Monitor and manage agent versions across your infrastructure
 | 
			
		||||
				</p>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{/* Status Banner */}
 | 
			
		||||
			<div
 | 
			
		||||
				className={`rounded-xl shadow-sm p-6 border-2 ${
 | 
			
		||||
					versionStatus.status === "up-to-date"
 | 
			
		||||
						? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
 | 
			
		||||
						: versionStatus.status === "update-available"
 | 
			
		||||
							? "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800"
 | 
			
		||||
							: versionStatus.status === "no-agent"
 | 
			
		||||
								? "bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800"
 | 
			
		||||
								: "bg-white dark:bg-secondary-800 border-secondary-200 dark:border-secondary-600"
 | 
			
		||||
				}`}
 | 
			
		||||
			>
 | 
			
		||||
				<div className="flex items-start justify-between">
 | 
			
		||||
					<div className="flex items-start space-x-4">
 | 
			
		||||
						<div
 | 
			
		||||
							className={`p-3 rounded-lg ${
 | 
			
		||||
								versionStatus.status === "up-to-date"
 | 
			
		||||
									? "bg-green-100 dark:bg-green-800"
 | 
			
		||||
									: versionStatus.status === "update-available"
 | 
			
		||||
										? "bg-yellow-100 dark:bg-yellow-800"
 | 
			
		||||
										: versionStatus.status === "no-agent"
 | 
			
		||||
											? "bg-orange-100 dark:bg-orange-800"
 | 
			
		||||
											: "bg-secondary-100 dark:bg-secondary-700"
 | 
			
		||||
							}`}
 | 
			
		||||
						>
 | 
			
		||||
							{StatusIcon && (
 | 
			
		||||
								<StatusIcon className={`h-6 w-6 ${versionStatus.color}`} />
 | 
			
		||||
							)}
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-1">
 | 
			
		||||
								{versionStatus.message}
 | 
			
		||||
							</h3>
 | 
			
		||||
							<p className="text-sm text-secondary-600 dark:text-secondary-400">
 | 
			
		||||
								{versionStatus.status === "up-to-date" &&
 | 
			
		||||
									"All agent binaries are current"}
 | 
			
		||||
								{versionStatus.status === "update-available" &&
 | 
			
		||||
									"A newer version is available for download"}
 | 
			
		||||
								{versionStatus.status === "no-agent" &&
 | 
			
		||||
									"Download agent binaries to get started"}
 | 
			
		||||
								{versionStatus.status === "github-unavailable" &&
 | 
			
		||||
									"Cannot check for updates at this time"}
 | 
			
		||||
								{![
 | 
			
		||||
									"up-to-date",
 | 
			
		||||
									"update-available",
 | 
			
		||||
									"no-agent",
 | 
			
		||||
									"github-unavailable",
 | 
			
		||||
								].includes(versionStatus.status) &&
 | 
			
		||||
									"Version information unavailable"}
 | 
			
		||||
							</p>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<button
 | 
			
		||||
						type="button"
 | 
			
		||||
						onClick={() => checkUpdatesMutation.mutate()}
 | 
			
		||||
						disabled={checkUpdatesMutation.isPending}
 | 
			
		||||
						className="flex items-center px-4 py-2 bg-white dark:bg-secondary-700 text-secondary-700 dark:text-secondary-200 rounded-lg hover:bg-secondary-50 dark:hover:bg-secondary-600 border border-secondary-300 dark:border-secondary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow"
 | 
			
		||||
					>
 | 
			
		||||
						<RefreshCw
 | 
			
		||||
							className={`h-4 w-4 mr-2 ${downloadUpdateMutation.isPending ? "animate-spin" : ""}`}
 | 
			
		||||
							className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
 | 
			
		||||
						/>
 | 
			
		||||
						{downloadUpdateMutation.isPending
 | 
			
		||||
							? "Downloading..."
 | 
			
		||||
							: versionInfo?.currentVersion
 | 
			
		||||
								? "Download Updates"
 | 
			
		||||
								: "Download Agent Binaries"}
 | 
			
		||||
						{checkUpdatesMutation.isPending
 | 
			
		||||
							? "Checking..."
 | 
			
		||||
							: "Check for Updates"}
 | 
			
		||||
					</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{/* Version Status Card */}
 | 
			
		||||
			<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
 | 
			
		||||
				<div className="flex items-center justify-between mb-4">
 | 
			
		||||
					<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
						Agent Version Status
 | 
			
		||||
					</h3>
 | 
			
		||||
					<div className="flex items-center space-x-2">
 | 
			
		||||
						{StatusIcon && (
 | 
			
		||||
							<StatusIcon className={`h-5 w-5 ${versionStatus.color}`} />
 | 
			
		||||
			{/* Version Information Grid */}
 | 
			
		||||
			<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
 | 
			
		||||
				{/* Current Version Card */}
 | 
			
		||||
				<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600 hover:shadow-md transition-shadow duration-200">
 | 
			
		||||
					<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
 | 
			
		||||
						Current Version
 | 
			
		||||
					</h4>
 | 
			
		||||
					<p className="text-2xl font-bold text-secondary-900 dark:text-white">
 | 
			
		||||
						{versionInfo?.currentVersion || (
 | 
			
		||||
							<span className="text-lg text-secondary-400 dark:text-secondary-500">
 | 
			
		||||
								Not detected
 | 
			
		||||
							</span>
 | 
			
		||||
						)}
 | 
			
		||||
						<span className={`text-sm font-medium ${versionStatus.color}`}>
 | 
			
		||||
							{versionStatus.message}
 | 
			
		||||
						</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				{versionInfo && (
 | 
			
		||||
					<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
 | 
			
		||||
						<div>
 | 
			
		||||
							<span className="text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
								Current Version:
 | 
			
		||||
							</span>
 | 
			
		||||
							<span className="ml-2 font-medium text-secondary-900 dark:text-white">
 | 
			
		||||
								{versionInfo.currentVersion || "Unknown"}
 | 
			
		||||
				{/* Latest Version Card */}
 | 
			
		||||
				<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600 hover:shadow-md transition-shadow duration-200">
 | 
			
		||||
					<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
 | 
			
		||||
						Latest Available
 | 
			
		||||
					</h4>
 | 
			
		||||
					<p className="text-2xl font-bold text-secondary-900 dark:text-white">
 | 
			
		||||
						{versionInfo?.latestVersion || (
 | 
			
		||||
							<span className="text-lg text-secondary-400 dark:text-secondary-500">
 | 
			
		||||
								Unknown
 | 
			
		||||
							</span>
 | 
			
		||||
						)}
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				{/* Last Checked Card */}
 | 
			
		||||
				<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600 hover:shadow-md transition-shadow duration-200">
 | 
			
		||||
					<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
 | 
			
		||||
						Last Checked
 | 
			
		||||
					</h4>
 | 
			
		||||
					<p className="text-lg font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
						{versionInfo?.lastChecked
 | 
			
		||||
							? new Date(versionInfo.lastChecked).toLocaleString("en-US", {
 | 
			
		||||
									month: "short",
 | 
			
		||||
									day: "numeric",
 | 
			
		||||
									hour: "2-digit",
 | 
			
		||||
									minute: "2-digit",
 | 
			
		||||
								})
 | 
			
		||||
							: "Never"}
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{/* Download Updates Section */}
 | 
			
		||||
			<div className="bg-gradient-to-br from-primary-50 to-blue-50 dark:from-secondary-800 dark:to-secondary-800 rounded-xl shadow-sm p-8 border border-primary-200 dark:border-secondary-600">
 | 
			
		||||
				<div className="flex items-center justify-between">
 | 
			
		||||
					<div className="flex-1">
 | 
			
		||||
						<h3 className="text-xl font-bold text-secondary-900 dark:text-white mb-3">
 | 
			
		||||
							{!versionInfo?.currentVersion
 | 
			
		||||
								? "Get Started with Agent Binaries"
 | 
			
		||||
								: versionStatus.status === "update-available"
 | 
			
		||||
									? "New Agent Version Available"
 | 
			
		||||
									: "Agent Binaries"}
 | 
			
		||||
						</h3>
 | 
			
		||||
						<p className="text-secondary-700 dark:text-secondary-300 mb-4">
 | 
			
		||||
							{!versionInfo?.currentVersion
 | 
			
		||||
								? "No agent binaries detected. Download from GitHub to begin managing your agents."
 | 
			
		||||
								: versionStatus.status === "update-available"
 | 
			
		||||
									? `A new agent version (${versionInfo.latestVersion}) is available. Download the latest binaries from GitHub.`
 | 
			
		||||
									: "Download or redownload agent binaries from GitHub."}
 | 
			
		||||
						</p>
 | 
			
		||||
						<div className="flex items-center space-x-4">
 | 
			
		||||
							<button
 | 
			
		||||
								type="button"
 | 
			
		||||
								onClick={() => downloadUpdateMutation.mutate()}
 | 
			
		||||
								disabled={downloadUpdateMutation.isPending}
 | 
			
		||||
								className="flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-md hover:shadow-lg font-medium"
 | 
			
		||||
							>
 | 
			
		||||
								{downloadUpdateMutation.isPending ? (
 | 
			
		||||
									<>
 | 
			
		||||
										<RefreshCw className="h-5 w-5 mr-2 animate-spin" />
 | 
			
		||||
										Downloading...
 | 
			
		||||
									</>
 | 
			
		||||
								) : (
 | 
			
		||||
									<>
 | 
			
		||||
										<Download className="h-5 w-5 mr-2" />
 | 
			
		||||
										{!versionInfo?.currentVersion
 | 
			
		||||
											? "Download Binaries"
 | 
			
		||||
											: versionStatus.status === "update-available"
 | 
			
		||||
												? "Download New Agent Version"
 | 
			
		||||
												: "Redownload Binaries"}
 | 
			
		||||
									</>
 | 
			
		||||
								)}
 | 
			
		||||
							</button>
 | 
			
		||||
							<a
 | 
			
		||||
								href="https://github.com/PatchMon/PatchMon-agent/releases"
 | 
			
		||||
								target="_blank"
 | 
			
		||||
								rel="noopener noreferrer"
 | 
			
		||||
								className="flex items-center px-4 py-3 text-secondary-700 dark:text-secondary-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200 font-medium"
 | 
			
		||||
							>
 | 
			
		||||
								<ExternalLink className="h-4 w-4 mr-2" />
 | 
			
		||||
								View on GitHub
 | 
			
		||||
							</a>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span className="text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
								Latest Version:
 | 
			
		||||
							</span>
 | 
			
		||||
							<span className="ml-2 font-medium text-secondary-900 dark:text-white">
 | 
			
		||||
								{versionInfo.latestVersion || "Unknown"}
 | 
			
		||||
							</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div>
 | 
			
		||||
							<span className="text-secondary-500 dark:text-secondary-400">
 | 
			
		||||
								Last Checked:
 | 
			
		||||
							</span>
 | 
			
		||||
							<span className="ml-2 font-medium text-secondary-900 dark:text-white">
 | 
			
		||||
								{versionInfo.lastChecked
 | 
			
		||||
									? new Date(versionInfo.lastChecked).toLocaleString()
 | 
			
		||||
									: "Never"}
 | 
			
		||||
							</span>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{/* Supported Architectures */}
 | 
			
		||||
			{versionInfo?.supportedArchitectures &&
 | 
			
		||||
				versionInfo.supportedArchitectures.length > 0 && (
 | 
			
		||||
					<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600">
 | 
			
		||||
						<h4 className="text-lg font-semibold text-secondary-900 dark:text-white mb-4">
 | 
			
		||||
							Supported Architectures
 | 
			
		||||
						</h4>
 | 
			
		||||
						<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
 | 
			
		||||
							{versionInfo.supportedArchitectures.map((arch) => (
 | 
			
		||||
								<div
 | 
			
		||||
									key={arch}
 | 
			
		||||
									className="flex items-center justify-center px-4 py-3 bg-secondary-50 dark:bg-secondary-700 rounded-lg border border-secondary-200 dark:border-secondary-600"
 | 
			
		||||
								>
 | 
			
		||||
									<code className="text-sm font-mono text-secondary-700 dark:text-secondary-300">
 | 
			
		||||
										{arch}
 | 
			
		||||
									</code>
 | 
			
		||||
								</div>
 | 
			
		||||
							))}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				)}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -228,7 +228,33 @@ const Automation = () => {
 | 
			
		||||
		// Use the proxied URL through the frontend (port 3000)
 | 
			
		||||
		// This avoids CORS issues as everything goes through the same origin
 | 
			
		||||
		const url = `/bullboard?token=${encodeURIComponent(token)}`;
 | 
			
		||||
		window.open(url, "_blank", "width=1200,height=800");
 | 
			
		||||
		// Open in a new tab instead of a new window
 | 
			
		||||
		const bullBoardWindow = window.open(url, "_blank");
 | 
			
		||||
 | 
			
		||||
		// Add a message listener to handle authentication failures
 | 
			
		||||
		if (bullBoardWindow) {
 | 
			
		||||
			// Listen for authentication failures and refresh with token
 | 
			
		||||
			const checkAuth = () => {
 | 
			
		||||
				try {
 | 
			
		||||
					// Check if the Bull Board window is still open
 | 
			
		||||
					if (bullBoardWindow.closed) return;
 | 
			
		||||
 | 
			
		||||
					// Inject a script to handle authentication failures
 | 
			
		||||
					bullBoardWindow.postMessage(
 | 
			
		||||
						{
 | 
			
		||||
							type: "BULL_BOARD_TOKEN",
 | 
			
		||||
							token: token,
 | 
			
		||||
						},
 | 
			
		||||
						window.location.origin,
 | 
			
		||||
					);
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					console.log("Could not communicate with Bull Board window:", e);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			// Send token after a short delay to ensure Bull Board is loaded
 | 
			
		||||
			setTimeout(checkAuth, 1000);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const triggerManualJob = async (jobType, data = {}) => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user