const express = require("express"); const { queueManager, QUEUE_NAMES } = require("../services/automation"); const { getConnectedApiIds } = require("../services/agentWs"); const { authenticateToken } = require("../middleware/auth"); const router = express.Router(); // Get all queue statistics router.get("/stats", authenticateToken, async (_req, res) => { try { const stats = await queueManager.getAllQueueStats(); res.json({ success: true, data: stats, }); } catch (error) { console.error("Error fetching queue stats:", error); res.status(500).json({ success: false, error: "Failed to fetch queue statistics", }); } }); // Get specific queue statistics router.get("/stats/:queueName", authenticateToken, async (req, res) => { try { const { queueName } = req.params; if (!Object.values(QUEUE_NAMES).includes(queueName)) { return res.status(400).json({ success: false, error: "Invalid queue name", }); } const stats = await queueManager.getQueueStats(queueName); res.json({ success: true, data: stats, }); } catch (error) { console.error("Error fetching queue stats:", error); res.status(500).json({ success: false, error: "Failed to fetch queue statistics", }); } }); // Get recent jobs for a queue router.get("/jobs/:queueName", authenticateToken, async (req, res) => { try { const { queueName } = req.params; const { limit = 10 } = req.query; if (!Object.values(QUEUE_NAMES).includes(queueName)) { return res.status(400).json({ success: false, error: "Invalid queue name", }); } const jobs = await queueManager.getRecentJobs( queueName, parseInt(limit, 10), ); // Format jobs for frontend const formattedJobs = jobs.map((job) => ({ id: job.id, name: job.name, status: job.finishedOn ? job.failedReason ? "failed" : "completed" : "active", progress: job.progress, data: job.data, returnvalue: job.returnvalue, failedReason: job.failedReason, processedOn: job.processedOn, finishedOn: job.finishedOn, createdAt: new Date(job.timestamp), attemptsMade: job.attemptsMade, delay: job.delay, })); res.json({ success: true, data: formattedJobs, }); } catch (error) { console.error("Error fetching recent jobs:", error); res.status(500).json({ success: false, error: "Failed to fetch recent jobs", }); } }); // Trigger manual GitHub update check router.post("/trigger/github-update", authenticateToken, async (_req, res) => { try { const job = await queueManager.triggerGitHubUpdateCheck(); res.json({ success: true, data: { jobId: job.id, message: "GitHub update check triggered successfully", }, }); } catch (error) { console.error("Error triggering GitHub update check:", error); res.status(500).json({ success: false, error: "Failed to trigger GitHub update check", }); } }); // Trigger manual session cleanup router.post( "/trigger/session-cleanup", authenticateToken, async (_req, res) => { try { const job = await queueManager.triggerSessionCleanup(); res.json({ success: true, data: { jobId: job.id, message: "Session cleanup triggered successfully", }, }); } catch (error) { console.error("Error triggering session cleanup:", error); res.status(500).json({ success: false, error: "Failed to trigger session cleanup", }); } }, ); // Trigger Agent Collection: enqueue report_now for connected agents only router.post( "/trigger/agent-collection", authenticateToken, async (_req, res) => { try { const queue = queueManager.queues[QUEUE_NAMES.AGENT_COMMANDS]; const apiIds = getConnectedApiIds(); if (!apiIds || apiIds.length === 0) { return res.json({ success: true, data: { enqueued: 0 } }); } const jobs = apiIds.map((apiId) => ({ name: "report_now", data: { api_id: apiId, type: "report_now" }, opts: { attempts: 3, backoff: { type: "fixed", delay: 2000 } }, })); await queue.addBulk(jobs); res.json({ success: true, data: { enqueued: jobs.length } }); } catch (error) { console.error("Error triggering agent collection:", error); res .status(500) .json({ success: false, error: "Failed to trigger agent collection" }); } }, ); // Trigger manual orphaned repo cleanup router.post( "/trigger/orphaned-repo-cleanup", authenticateToken, async (_req, res) => { try { const job = await queueManager.triggerOrphanedRepoCleanup(); res.json({ success: true, data: { jobId: job.id, message: "Orphaned repository cleanup triggered successfully", }, }); } catch (error) { console.error("Error triggering orphaned repository cleanup:", error); res.status(500).json({ success: false, error: "Failed to trigger orphaned repository cleanup", }); } }, ); // Trigger manual orphaned package cleanup router.post( "/trigger/orphaned-package-cleanup", authenticateToken, async (_req, res) => { try { const job = await queueManager.triggerOrphanedPackageCleanup(); res.json({ success: true, data: { jobId: job.id, message: "Orphaned package cleanup triggered successfully", }, }); } catch (error) { console.error("Error triggering orphaned package cleanup:", error); res.status(500).json({ success: false, error: "Failed to trigger orphaned package cleanup", }); } }, ); // Trigger manual Docker inventory cleanup router.post( "/trigger/docker-inventory-cleanup", authenticateToken, async (_req, res) => { try { const job = await queueManager.triggerDockerInventoryCleanup(); res.json({ success: true, data: { jobId: job.id, message: "Docker inventory cleanup triggered successfully", }, }); } catch (error) { console.error("Error triggering Docker inventory cleanup:", error); res.status(500).json({ success: false, error: "Failed to trigger Docker inventory cleanup", }); } }, ); // Get queue health status router.get("/health", authenticateToken, async (_req, res) => { try { const stats = await queueManager.getAllQueueStats(); const totalJobs = Object.values(stats).reduce((sum, queueStats) => { return sum + queueStats.waiting + queueStats.active + queueStats.failed; }, 0); const health = { status: "healthy", totalJobs, queues: Object.keys(stats).length, timestamp: new Date().toISOString(), }; // Check for unhealthy conditions if (totalJobs > 1000) { health.status = "warning"; health.message = "High number of queued jobs"; } const failedJobs = Object.values(stats).reduce((sum, queueStats) => { return sum + queueStats.failed; }, 0); if (failedJobs > 10) { health.status = "error"; health.message = "High number of failed jobs"; } res.json({ success: true, data: health, }); } catch (error) { console.error("Error checking queue health:", error); res.status(500).json({ success: false, error: "Failed to check queue health", }); } }); // Get automation overview (for dashboard cards) router.get("/overview", authenticateToken, async (_req, res) => { try { const stats = await queueManager.getAllQueueStats(); const { getSettings } = require("../services/settingsService"); const settings = await getSettings(); // Get recent jobs for each queue to show last run times const recentJobs = await Promise.all([ queueManager.getRecentJobs(QUEUE_NAMES.GITHUB_UPDATE_CHECK, 1), queueManager.getRecentJobs(QUEUE_NAMES.SESSION_CLEANUP, 1), queueManager.getRecentJobs(QUEUE_NAMES.ORPHANED_REPO_CLEANUP, 1), queueManager.getRecentJobs(QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP, 1), queueManager.getRecentJobs(QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP, 1), queueManager.getRecentJobs(QUEUE_NAMES.AGENT_COMMANDS, 1), ]); // Calculate overview metrics const overview = { scheduledTasks: stats[QUEUE_NAMES.GITHUB_UPDATE_CHECK].delayed + stats[QUEUE_NAMES.SESSION_CLEANUP].delayed + stats[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].delayed + stats[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].delayed + stats[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP].delayed, runningTasks: stats[QUEUE_NAMES.GITHUB_UPDATE_CHECK].active + stats[QUEUE_NAMES.SESSION_CLEANUP].active + stats[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].active + stats[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].active + stats[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP].active, failedTasks: stats[QUEUE_NAMES.GITHUB_UPDATE_CHECK].failed + stats[QUEUE_NAMES.SESSION_CLEANUP].failed + stats[QUEUE_NAMES.ORPHANED_REPO_CLEANUP].failed + stats[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP].failed + stats[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP].failed, totalAutomations: Object.values(stats).reduce((sum, queueStats) => { return ( sum + queueStats.completed + queueStats.failed + queueStats.active + queueStats.waiting + queueStats.delayed ); }, 0), // Automation details with last run times automations: [ { name: "GitHub Update Check", queue: QUEUE_NAMES.GITHUB_UPDATE_CHECK, description: "Checks for new PatchMon releases", schedule: "Daily at midnight", lastRun: recentJobs[0][0]?.finishedOn ? new Date(recentJobs[0][0].finishedOn).toLocaleString() : "Never", lastRunTimestamp: recentJobs[0][0]?.finishedOn || 0, status: recentJobs[0][0]?.failedReason ? "Failed" : recentJobs[0][0] ? "Success" : "Never run", stats: stats[QUEUE_NAMES.GITHUB_UPDATE_CHECK], }, { name: "Session Cleanup", queue: QUEUE_NAMES.SESSION_CLEANUP, description: "Cleans up expired user sessions", schedule: "Every hour", lastRun: recentJobs[1][0]?.finishedOn ? new Date(recentJobs[1][0].finishedOn).toLocaleString() : "Never", lastRunTimestamp: recentJobs[1][0]?.finishedOn || 0, status: recentJobs[1][0]?.failedReason ? "Failed" : recentJobs[1][0] ? "Success" : "Never run", stats: stats[QUEUE_NAMES.SESSION_CLEANUP], }, { name: "Orphaned Repo Cleanup", queue: QUEUE_NAMES.ORPHANED_REPO_CLEANUP, description: "Removes repositories with no associated hosts", schedule: "Daily at 2 AM", lastRun: recentJobs[2][0]?.finishedOn ? new Date(recentJobs[2][0].finishedOn).toLocaleString() : "Never", lastRunTimestamp: recentJobs[2][0]?.finishedOn || 0, status: recentJobs[2][0]?.failedReason ? "Failed" : recentJobs[2][0] ? "Success" : "Never run", stats: stats[QUEUE_NAMES.ORPHANED_REPO_CLEANUP], }, { name: "Orphaned Package Cleanup", queue: QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP, description: "Removes packages with no associated hosts", schedule: "Daily at 3 AM", lastRun: recentJobs[3][0]?.finishedOn ? new Date(recentJobs[3][0].finishedOn).toLocaleString() : "Never", lastRunTimestamp: recentJobs[3][0]?.finishedOn || 0, status: recentJobs[3][0]?.failedReason ? "Failed" : recentJobs[3][0] ? "Success" : "Never run", stats: stats[QUEUE_NAMES.ORPHANED_PACKAGE_CLEANUP], }, { name: "Docker Inventory Cleanup", queue: QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP, description: "Removes Docker containers and images for non-existent hosts", schedule: "Daily at 4 AM", lastRun: recentJobs[4][0]?.finishedOn ? new Date(recentJobs[4][0].finishedOn).toLocaleString() : "Never", lastRunTimestamp: recentJobs[4][0]?.finishedOn || 0, status: recentJobs[4][0]?.failedReason ? "Failed" : recentJobs[4][0] ? "Success" : "Never run", stats: stats[QUEUE_NAMES.DOCKER_INVENTORY_CLEANUP], }, { name: "Collect Host Statistics", queue: QUEUE_NAMES.AGENT_COMMANDS, description: "Collects package statistics from connected agents only", schedule: `Every ${settings.update_interval} minutes (Agent-driven)`, lastRun: recentJobs[5][0]?.finishedOn ? new Date(recentJobs[5][0].finishedOn).toLocaleString() : "Never", lastRunTimestamp: recentJobs[5][0]?.finishedOn || 0, status: recentJobs[5][0]?.failedReason ? "Failed" : recentJobs[5][0] ? "Success" : "Never run", stats: stats[QUEUE_NAMES.AGENT_COMMANDS], }, ].sort((a, b) => { // Sort by last run timestamp (most recent first) // If both have never run (timestamp 0), maintain original order if (a.lastRunTimestamp === 0 && b.lastRunTimestamp === 0) return 0; if (a.lastRunTimestamp === 0) return 1; // Never run goes to bottom if (b.lastRunTimestamp === 0) return -1; // Never run goes to bottom return b.lastRunTimestamp - a.lastRunTimestamp; // Most recent first }), }; res.json({ success: true, data: overview, }); } catch (error) { console.error("Error fetching automation overview:", error); res.status(500).json({ success: false, error: "Failed to fetch automation overview", }); } }); module.exports = router;