mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-24 00:23:36 +00:00
Compare commits
3 Commits
renovate/m
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0189a307ef | ||
|
|
50e546ee7e | ||
|
|
2174abf395 |
Binary file not shown.
@@ -341,20 +341,50 @@ const parseOrigins = (val) =>
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const allowedOrigins = parseOrigins(
|
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(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: (origin, callback) => {
|
origin: (origin, callback) => {
|
||||||
// Allow non-browser/SSR tools with no origin
|
// Allow non-browser/SSR tools with no origin
|
||||||
if (!origin) return callback(null, true);
|
if (!origin) return callback(null, true);
|
||||||
if (allowedOrigins.includes(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)
|
// Allow same-origin requests (e.g., Bull Board accessing its own API)
|
||||||
// This allows http://hostname:3001 to make requests to http://hostname:3001
|
// This allows http://hostname:3001 to make requests to http://hostname:3001
|
||||||
if (origin?.includes(":3001")) return callback(null, true);
|
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"));
|
return callback(new Error("Not allowed by CORS"));
|
||||||
},
|
},
|
||||||
credentials: true,
|
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);
|
app.use(limiter);
|
||||||
@@ -446,7 +476,7 @@ app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
|
|||||||
|
|
||||||
// Bull Board - will be populated after queue manager initializes
|
// Bull Board - will be populated after queue manager initializes
|
||||||
let bullBoardRouter = null;
|
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
|
// Mount Bull Board at /bullboard for cleaner URL
|
||||||
app.use(`/bullboard`, (_req, res, next) => {
|
app.use(`/bullboard`, (_req, res, next) => {
|
||||||
@@ -456,16 +486,176 @@ app.use(`/bullboard`, (_req, res, next) => {
|
|||||||
res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-none");
|
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();
|
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) => {
|
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")) {
|
if (req.path.includes("/static/") || req.path.includes("/favicon")) {
|
||||||
return next();
|
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
|
// Check for bull-board-session cookie first
|
||||||
const sessionId = req.cookies["bull-board-session"];
|
const sessionId = req.cookies["bull-board-session"];
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
@@ -486,6 +676,9 @@ app.use(`/bullboard`, async (req, res, next) => {
|
|||||||
if (!token && req.headers.authorization) {
|
if (!token && req.headers.authorization) {
|
||||||
token = req.headers.authorization.replace("Bearer ", "");
|
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 no token, deny access
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -514,13 +707,23 @@ app.use(`/bullboard`, async (req, res, next) => {
|
|||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set session cookie
|
// Set session cookie with proper configuration for domain access
|
||||||
res.cookie("bull-board-session", newSessionId, {
|
const isHttps = process.env.NODE_ENV === "production" || process.env.SERVER_PROTOCOL === "https";
|
||||||
|
const cookieOptions = {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
secure: isHttps,
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 3600000, // 1 hour
|
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
|
// Clean up old sessions periodically
|
||||||
if (bullBoardSessions.size > 100) {
|
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) => {
|
app.use(`/bullboard`, (req, res, next) => {
|
||||||
if (bullBoardRouter) {
|
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 bullBoardRouter(req, res, next);
|
||||||
}
|
}
|
||||||
return res.status(503).json({ error: "Bull Board not initialized yet" });
|
return res.status(503).json({ error: "Bull Board not initialized yet" });
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// Error handler specifically for Bull Board routes
|
// Error handler specifically for Bull Board routes
|
||||||
app.use("/bullboard", (err, req, res, _next) => {
|
app.use("/bullboard", (err, req, res, _next) => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const fs = require("node:fs").promises;
|
const fs = require("node:fs").promises;
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { exec } = require("node:child_process");
|
const { exec, spawn } = require("node:child_process");
|
||||||
const { promisify } = require("node:util");
|
const { promisify } = require("node:util");
|
||||||
const execAsync = promisify(exec);
|
const _execAsync = promisify(exec);
|
||||||
|
|
||||||
// Simple semver comparison function
|
// Simple semver comparison function
|
||||||
function compareVersions(version1, version2) {
|
function compareVersions(version1, version2) {
|
||||||
@@ -135,16 +135,34 @@ class AgentVersionService {
|
|||||||
|
|
||||||
// Execute the agent binary with help flag to get version info
|
// Execute the agent binary with help flag to get version info
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execAsync(`${agentPath} --help`, {
|
const child = spawn(agentPath, ["--help"], {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stderr) {
|
let stdout = "";
|
||||||
console.log("⚠️ Agent help stderr:", stderr);
|
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")
|
// 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,
|
/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
|
||||||
);
|
);
|
||||||
if (versionMatch) {
|
if (versionMatch) {
|
||||||
@@ -153,7 +171,7 @@ class AgentVersionService {
|
|||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"⚠️ Could not parse version from agent help output:",
|
"⚠️ Could not parse version from agent help output:",
|
||||||
stdout,
|
result.stdout,
|
||||||
);
|
);
|
||||||
this.currentVersion = null;
|
this.currentVersion = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,37 @@ function init(server, prismaClient) {
|
|||||||
server.on("upgrade", async (request, socket, head) => {
|
server.on("upgrade", async (request, socket, head) => {
|
||||||
try {
|
try {
|
||||||
const { pathname } = url.parse(request.url);
|
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();
|
socket.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ENV NODE_ENV=development \
|
|||||||
PM_LOG_TO_CONSOLE=true \
|
PM_LOG_TO_CONSOLE=true \
|
||||||
PORT=3001
|
PORT=3001
|
||||||
|
|
||||||
RUN apk add --no-cache openssl tini curl
|
RUN apk add --no-cache openssl tini curl libc6-compat
|
||||||
|
|
||||||
USER node
|
USER node
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ ENV NODE_ENV=production \
|
|||||||
JWT_REFRESH_EXPIRES_IN=7d \
|
JWT_REFRESH_EXPIRES_IN=7d \
|
||||||
SESSION_INACTIVITY_TIMEOUT_MINUTES=30
|
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
|
USER node
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ log() {
|
|||||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
|
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() {
|
get_agent_version() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
@@ -18,6 +18,32 @@ get_agent_version() {
|
|||||||
fi
|
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)
|
# Function to compare versions (returns 0 if $1 > $2)
|
||||||
version_greater() {
|
version_greater() {
|
||||||
# Use sort -V for version comparison
|
# Use sort -V for version comparison
|
||||||
@@ -28,6 +54,8 @@ version_greater() {
|
|||||||
update_agents() {
|
update_agents() {
|
||||||
local backup_agent="/app/agents_backup/patchmon-agent.sh"
|
local backup_agent="/app/agents_backup/patchmon-agent.sh"
|
||||||
local current_agent="/app/agents/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
|
# Check if agents directory exists
|
||||||
if [ ! -d "/app/agents" ]; then
|
if [ ! -d "/app/agents" ]; then
|
||||||
@@ -41,54 +69,72 @@ update_agents() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get versions
|
# Get versions from both script and binary
|
||||||
local backup_version=$(get_agent_version "$backup_agent")
|
local backup_script_version=$(get_agent_version "$backup_agent")
|
||||||
local current_version=$(get_agent_version "$current_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 "Agent version check:"
|
||||||
log " Image version: ${backup_version}"
|
log " Image script version: ${backup_script_version}"
|
||||||
log " Volume version: ${current_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
|
# Determine if update is needed
|
||||||
local needs_update=0
|
local needs_update=0
|
||||||
|
|
||||||
# Case 1: No agents in volume (first time setup)
|
# Case 1: No agents in volume at all (first time setup)
|
||||||
if [ -z "$(find /app/agents -maxdepth 1 -type f -name '*.sh' 2>/dev/null | head -n 1)" ]; then
|
if [ -z "$(find /app/agents -maxdepth 1 -type f 2>/dev/null | head -n 1)" ]; then
|
||||||
log "Agents directory is empty - performing initial copy"
|
log "Agents directory is empty - performing initial copy"
|
||||||
needs_update=1
|
needs_update=1
|
||||||
# Case 2: Backup version is newer
|
# Case 2: Binary exists but backup binary is newer
|
||||||
elif version_greater "$backup_version" "$current_version"; then
|
elif [ "$current_binary_version" != "0.0.0" ] && version_greater "$backup_binary_version" "$current_binary_version"; then
|
||||||
log "Newer agent version available (${backup_version} > ${current_version})"
|
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
|
needs_update=1
|
||||||
else
|
else
|
||||||
log "Agents are up to date"
|
log "Agents are up to date (binary: ${current_binary_version})"
|
||||||
needs_update=0
|
needs_update=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Perform update if needed
|
# Perform update if needed
|
||||||
if [ $needs_update -eq 1 ]; then
|
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
|
# 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_timestamp=$(date +%Y%m%d_%H%M%S)
|
||||||
local backup_name="/app/agents/patchmon-agent.sh.backup.${backup_timestamp}"
|
mkdir -p "/app/agents/backups"
|
||||||
cp "$current_agent" "$backup_name" 2>/dev/null || true
|
|
||||||
log "Previous agent backed up to: $(basename $backup_name)"
|
# 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
|
fi
|
||||||
|
|
||||||
# Copy new agents
|
# Copy new agents (both scripts and binaries)
|
||||||
cp -r /app/agents_backup/* /app/agents/
|
cp -r /app/agents_backup/* /app/agents/
|
||||||
|
|
||||||
# Make agent binaries executable
|
# Make agent binaries executable
|
||||||
chmod +x /app/agents/patchmon-agent-linux-* 2>/dev/null || true
|
chmod +x /app/agents/patchmon-agent-linux-* 2>/dev/null || true
|
||||||
|
|
||||||
# Verify update
|
# Verify update
|
||||||
local new_version=$(get_agent_version "$current_agent")
|
local new_binary_version=$(get_binary_version "$current_binary")
|
||||||
if [ "$new_version" = "$backup_version" ]; then
|
if [ "$new_binary_version" = "$backup_binary_version" ]; then
|
||||||
log "✅ Agents successfully updated to version ${new_version}"
|
log "✅ Agents successfully updated to version ${new_binary_version}"
|
||||||
else
|
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
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,17 +35,20 @@ server {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header Cookie $http_cookie; # Forward cookies to backend
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
proxy_connect_timeout 75s;
|
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
|
# Preserve original client IP through proxy chain
|
||||||
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
|
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
# CORS headers for Bull Board - let backend handle CORS properly
|
||||||
add_header Access-Control-Allow-Origin * always;
|
# Note: Backend handles CORS with proper origin validation and credentials
|
||||||
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;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
# Handle preflight requests
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
@@ -78,10 +81,8 @@ server {
|
|||||||
# Preserve original client IP through proxy chain
|
# Preserve original client IP through proxy chain
|
||||||
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
|
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
|
||||||
|
|
||||||
# CORS headers for API calls - even though backend is doing it
|
# CORS headers for API calls - let backend handle CORS properly
|
||||||
add_header Access-Control-Allow-Origin * always;
|
# Note: Backend handles CORS with proper origin validation and credentials
|
||||||
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;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
# Handle preflight requests
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
|
|||||||
@@ -1,9 +1,33 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
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";
|
import api from "../../utils/api";
|
||||||
|
|
||||||
const AgentManagementTab = () => {
|
const AgentManagementTab = () => {
|
||||||
const _queryClient = useQueryClient();
|
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
|
// Agent version queries
|
||||||
const {
|
const {
|
||||||
@@ -57,9 +81,11 @@ const AgentManagementTab = () => {
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
refetchVersion();
|
refetchVersion();
|
||||||
|
showToast("Successfully checked for updates", "success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Check updates error:", 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
|
// Show success message
|
||||||
const message =
|
const message =
|
||||||
data.data?.message || "Agent binaries downloaded successfully";
|
data.data?.message || "Agent binaries downloaded successfully";
|
||||||
alert(`✅ ${message}`);
|
showToast(message, "success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Download update error:", 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Toast Notification */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
{toast && (
|
||||||
<div>
|
<div
|
||||||
<h2 className="text-2xl font-bold text-secondary-900 dark:text-white">
|
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 ${
|
||||||
Agent Version Management
|
toast.type === "success"
|
||||||
</h2>
|
? "bg-green-50 dark:bg-green-900/90 border-green-500 dark:border-green-600"
|
||||||
<p className="text-secondary-600 dark:text-secondary-300">
|
: "bg-red-50 dark:bg-red-900/90 border-red-500 dark:border-red-600"
|
||||||
Monitor agent versions and download updates
|
}`}
|
||||||
</p>
|
>
|
||||||
</div>
|
<div
|
||||||
<div className="flex space-x-3">
|
className={`flex-shrink-0 rounded-full p-1 ${
|
||||||
<button
|
toast.type === "success"
|
||||||
type="button"
|
? "bg-green-100 dark:bg-green-800"
|
||||||
onClick={() => checkUpdatesMutation.mutate()}
|
: "bg-red-100 dark:bg-red-800"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<RefreshCw
|
{toast.type === "success" ? (
|
||||||
className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
|
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
/>
|
) : (
|
||||||
Check Updates
|
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||||
</button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
|
<p
|
||||||
{/* Download Updates Button */}
|
className={`text-sm font-medium ${
|
||||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
|
toast.type === "success"
|
||||||
<div className="flex items-center justify-between">
|
? "text-green-800 dark:text-green-100"
|
||||||
<div>
|
: "text-red-800 dark:text-red-100"
|
||||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
}`}
|
||||||
{versionInfo?.currentVersion
|
>
|
||||||
? "Download Agent Updates"
|
{toast.message}
|
||||||
: "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."}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => downloadUpdateMutation.mutate()}
|
onClick={() => setToast(null)}
|
||||||
disabled={downloadUpdateMutation.isPending}
|
className={`flex-shrink-0 rounded-lg p-1 transition-colors ${
|
||||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
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
|
<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
|
{checkUpdatesMutation.isPending
|
||||||
? "Downloading..."
|
? "Checking..."
|
||||||
: versionInfo?.currentVersion
|
: "Check for Updates"}
|
||||||
? "Download Updates"
|
|
||||||
: "Download Agent Binaries"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Version Status Card */}
|
{/* Version Information Grid */}
|
||||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
{/* Current Version Card */}
|
||||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
<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">
|
||||||
Agent Version Status
|
<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
|
||||||
</h3>
|
Current Version
|
||||||
<div className="flex items-center space-x-2">
|
</h4>
|
||||||
{StatusIcon && (
|
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||||
<StatusIcon className={`h-5 w-5 ${versionStatus.color}`} />
|
{versionInfo?.currentVersion || (
|
||||||
|
<span className="text-lg text-secondary-400 dark:text-secondary-500">
|
||||||
|
Not detected
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={`text-sm font-medium ${versionStatus.color}`}>
|
</p>
|
||||||
{versionStatus.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{versionInfo && (
|
{/* Latest Version Card */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
<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">
|
||||||
<div>
|
<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
|
||||||
<span className="text-secondary-500 dark:text-secondary-400">
|
Latest Available
|
||||||
Current Version:
|
</h4>
|
||||||
</span>
|
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||||
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
|
{versionInfo?.latestVersion || (
|
||||||
{versionInfo.currentVersion || "Unknown"}
|
<span className="text-lg text-secondary-400 dark:text-secondary-500">
|
||||||
|
Unknown
|
||||||
</span>
|
</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>
|
||||||
<div>
|
</div>
|
||||||
<span className="text-secondary-500 dark:text-secondary-400">
|
</div>
|
||||||
Latest Version:
|
</div>
|
||||||
</span>
|
|
||||||
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
|
{/* Supported Architectures */}
|
||||||
{versionInfo.latestVersion || "Unknown"}
|
{versionInfo?.supportedArchitectures &&
|
||||||
</span>
|
versionInfo.supportedArchitectures.length > 0 && (
|
||||||
</div>
|
<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600">
|
||||||
<div>
|
<h4 className="text-lg font-semibold text-secondary-900 dark:text-white mb-4">
|
||||||
<span className="text-secondary-500 dark:text-secondary-400">
|
Supported Architectures
|
||||||
Last Checked:
|
</h4>
|
||||||
</span>
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
|
{versionInfo.supportedArchitectures.map((arch) => (
|
||||||
{versionInfo.lastChecked
|
<div
|
||||||
? new Date(versionInfo.lastChecked).toLocaleString()
|
key={arch}
|
||||||
: "Never"}
|
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"
|
||||||
</span>
|
>
|
||||||
|
<code className="text-sm font-mono text-secondary-700 dark:text-secondary-300">
|
||||||
|
{arch}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -228,7 +228,33 @@ const Automation = () => {
|
|||||||
// Use the proxied URL through the frontend (port 3000)
|
// Use the proxied URL through the frontend (port 3000)
|
||||||
// This avoids CORS issues as everything goes through the same origin
|
// This avoids CORS issues as everything goes through the same origin
|
||||||
const url = `/bullboard?token=${encodeURIComponent(token)}`;
|
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 = {}) => {
|
const triggerManualJob = async (jobType, data = {}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user