diff --git a/agents/patchmon-agent-linux-386 b/agents/patchmon-agent-linux-386 index 34edb1a..68759ae 100755 Binary files a/agents/patchmon-agent-linux-386 and b/agents/patchmon-agent-linux-386 differ diff --git a/agents/patchmon-agent-linux-amd64 b/agents/patchmon-agent-linux-amd64 index 2149e7e..6716736 100755 Binary files a/agents/patchmon-agent-linux-amd64 and b/agents/patchmon-agent-linux-amd64 differ diff --git a/agents/patchmon-agent-linux-arm b/agents/patchmon-agent-linux-arm index 998c3e6..1c5237a 100755 Binary files a/agents/patchmon-agent-linux-arm and b/agents/patchmon-agent-linux-arm differ diff --git a/agents/patchmon-agent-linux-arm64 b/agents/patchmon-agent-linux-arm64 index bda1e54..5b6ca50 100755 Binary files a/agents/patchmon-agent-linux-arm64 and b/agents/patchmon-agent-linux-arm64 differ diff --git a/agents/patchmon_remove.sh b/agents/patchmon_remove.sh index b4e10a4..6860497 100755 --- a/agents/patchmon_remove.sh +++ b/agents/patchmon_remove.sh @@ -2,7 +2,9 @@ # PatchMon Agent Removal Script # POSIX-compliant shell script (works with dash, ash, bash, etc.) -# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/remove | sh +# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/remove | sudo sh +# curl -s {PATCHMON_URL}/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1 sh +# curl -s {PATCHMON_URL}/api/v1/hosts/remove | sudo SILENT=1 sh # This script completely removes PatchMon from the system set -e @@ -12,12 +14,30 @@ set -e # future (left for consistency with install script). CURL_FLAGS="" -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color +# Detect if running in silent mode (only with explicit SILENT env var) +SILENT_MODE=0 +if [ -n "$SILENT" ]; then + SILENT_MODE=1 +fi + +# Check if backup files should be removed (default: preserve for safety) +# Usage: REMOVE_BACKUPS=1 when piping the script +REMOVE_BACKUPS="${REMOVE_BACKUPS:-0}" + +# Colors for output (disabled in silent mode) +if [ "$SILENT_MODE" -eq 0 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' # No Color +else + RED='' + GREEN='' + YELLOW='' + BLUE='' + NC='' +fi # Functions error() { @@ -26,15 +46,21 @@ error() { } info() { - printf "%b\n" "${BLUE}ℹ️ $1${NC}" + if [ "$SILENT_MODE" -eq 0 ]; then + printf "%b\n" "${BLUE}ℹ️ $1${NC}" + fi } success() { - printf "%b\n" "${GREEN}✅ $1${NC}" + if [ "$SILENT_MODE" -eq 0 ]; then + printf "%b\n" "${GREEN}✅ $1${NC}" + fi } warning() { - printf "%b\n" "${YELLOW}⚠️ $1${NC}" + if [ "$SILENT_MODE" -eq 0 ]; then + printf "%b\n" "${YELLOW}⚠️ $1${NC}" + fi } # Check if running as root @@ -43,7 +69,7 @@ if [ "$(id -u)" -ne 0 ]; then fi info "🗑️ Starting PatchMon Agent Removal..." -echo "" +[ "$SILENT_MODE" -eq 0 ] && echo "" # Step 1: Stop systemd/OpenRC service if it exists info "🛑 Stopping PatchMon service..." @@ -51,24 +77,75 @@ SERVICE_STOPPED=0 # Check for systemd service if command -v systemctl >/dev/null 2>&1; then + info "📋 Checking systemd service status..." + + # Check if service is active if systemctl is-active --quiet patchmon-agent.service 2>/dev/null; then - warning "Stopping systemd service..." - systemctl stop patchmon-agent.service || true - SERVICE_STOPPED=1 + SERVICE_STATUS=$(systemctl is-active patchmon-agent.service 2>/dev/null || echo "unknown") + warning "Service is active (status: $SERVICE_STATUS). Stopping it now..." + if systemctl stop patchmon-agent.service 2>/dev/null; then + success "✓ Service stopped successfully" + SERVICE_STOPPED=1 + else + warning "✗ Failed to stop service (continuing anyway...)" + fi + # Verify it stopped + sleep 1 + if systemctl is-active --quiet patchmon-agent.service 2>/dev/null; then + warning "⚠️ Service is STILL ACTIVE after stop command!" + else + info "✓ Verified: Service is no longer active" + fi + else + info "Service is not active" fi + # Check if service is enabled if systemctl is-enabled --quiet patchmon-agent.service 2>/dev/null; then - warning "Disabling systemd service..." - systemctl disable patchmon-agent.service || true + ENABLED_STATUS=$(systemctl is-enabled patchmon-agent.service 2>/dev/null || echo "unknown") + warning "Service is enabled (status: $ENABLED_STATUS). Disabling it now..." + if systemctl disable patchmon-agent.service 2>/dev/null; then + success "✓ Service disabled successfully" + else + warning "✗ Failed to disable service (may already be disabled)" + fi + else + info "Service is not enabled" fi + # Check for service file if [ -f "/etc/systemd/system/patchmon-agent.service" ]; then - warning "Removing systemd service file..." - rm -f /etc/systemd/system/patchmon-agent.service - systemctl daemon-reload || true - success "Systemd service removed" + warning "Found service file: /etc/systemd/system/patchmon-agent.service" + info "Removing service file..." + if rm -f /etc/systemd/system/patchmon-agent.service 2>/dev/null; then + success "✓ Service file removed" + else + warning "✗ Failed to remove service file (check permissions)" + fi + + info "Reloading systemd daemon..." + if systemctl daemon-reload 2>/dev/null; then + success "✓ Systemd daemon reloaded" + else + warning "✗ Failed to reload systemd daemon" + fi + SERVICE_STOPPED=1 + + # Verify the file is gone + if [ -f "/etc/systemd/system/patchmon-agent.service" ]; then + warning "⚠️ Service file STILL EXISTS after removal!" + else + info "✓ Verified: Service file removed" + fi + else + info "Service file not found at /etc/systemd/system/patchmon-agent.service" fi + + # Final status check + info "📊 Final systemd status check..." + FINAL_STATUS=$(systemctl is-active patchmon-agent.service 2>&1 || echo "not-found") + info "Service status: $FINAL_STATUS" fi # Check for OpenRC service (Alpine Linux) @@ -93,11 +170,47 @@ if command -v rc-service >/dev/null 2>&1; then fi # Stop any remaining running processes (legacy or manual starts) +info "🔍 Checking for running PatchMon processes..." if pgrep -f "patchmon-agent" >/dev/null; then - warning "Found running PatchMon processes, stopping them..." - pkill -f "patchmon-agent" || true + PROCESS_COUNT=$(pgrep -f "patchmon-agent" | wc -l | tr -d ' ') + warning "Found $PROCESS_COUNT running PatchMon process(es)" + + # Show process details + if [ "$SILENT_MODE" -eq 0 ]; then + info "Process details:" + ps aux | grep "[p]atchmon-agent" | while IFS= read -r line; do + echo " $line" + done + fi + + warning "Sending SIGTERM to all patchmon-agent processes..." + if pkill -f "patchmon-agent" 2>/dev/null; then + success "✓ Sent SIGTERM signal" + else + warning "Failed to send SIGTERM (processes may have already stopped)" + fi + sleep 2 + + # Check if processes still exist + if pgrep -f "patchmon-agent" >/dev/null; then + REMAINING=$(pgrep -f "patchmon-agent" | wc -l | tr -d ' ') + warning "⚠️ $REMAINING process(es) still running! Sending SIGKILL..." + pkill -9 -f "patchmon-agent" 2>/dev/null || true + sleep 1 + + if pgrep -f "patchmon-agent" >/dev/null; then + warning "⚠️ CRITICAL: Processes still running after SIGKILL!" + else + success "✓ All processes terminated" + fi + else + success "✓ All processes stopped successfully" + fi + SERVICE_STOPPED=1 +else + info "No running PatchMon processes found" fi if [ "$SERVICE_STOPPED" -eq 1 ]; then @@ -159,11 +272,13 @@ info "📁 Removing configuration files..." if [ -d "/etc/patchmon" ]; then warning "Removing configuration directory: /etc/patchmon" - # Show what's being removed - info "📋 Files in /etc/patchmon:" - ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do - echo " $line" - done + # Show what's being removed (only in verbose mode) + if [ "$SILENT_MODE" -eq 0 ]; then + info "📋 Files in /etc/patchmon:" + ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do + echo " $line" + done + fi # Remove the directory rm -rf /etc/patchmon @@ -182,83 +297,105 @@ else info "Log file not found" fi -# Step 6: Clean up backup files (optional) -info "🧹 Cleaning up backup files..." +# Step 6: Clean up backup files +info "🧹 Checking backup files..." BACKUP_COUNT=0 +BACKUP_REMOVED=0 -# Count credential backups -CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l || echo "0") -if [ "$CRED_BACKUPS" -gt 0 ]; then - BACKUP_COUNT=$((BACKUP_COUNT + CRED_BACKUPS)) -fi - -# Count agent backups -AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | wc -l || echo "0") -if [ "$AGENT_BACKUPS" -gt 0 ]; then - BACKUP_COUNT=$((BACKUP_COUNT + AGENT_BACKUPS)) -fi - -# Count log backups -LOG_BACKUPS=$(ls /var/log/patchmon-agent.log.old.* 2>/dev/null | wc -l || echo "0") -if [ "$LOG_BACKUPS" -gt 0 ]; then - BACKUP_COUNT=$((BACKUP_COUNT + LOG_BACKUPS)) -fi - -if [ "$BACKUP_COUNT" -gt 0 ]; then - warning "Found $BACKUP_COUNT backup files" - echo "" - printf "%b\n" "${YELLOW}📋 Backup files found:${NC}" +if [ "$REMOVE_BACKUPS" -eq 1 ]; then + info "Removing backup files (REMOVE_BACKUPS=1)..." - # Show credential backups - if [ "$CRED_BACKUPS" -gt 0 ]; then - echo " Credential backups:" - ls /etc/patchmon/credentials.backup.* 2>/dev/null | while read -r file; do - echo " • $file" - done + # Remove credential backups (already removed with /etc/patchmon directory, but check anyway) + if ls /etc/patchmon/credentials.backup.* >/dev/null 2>&1; then + CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l | tr -d ' ') + warning "Removing $CRED_BACKUPS credential backup file(s)..." + rm -f /etc/patchmon/credentials.backup.* + BACKUP_COUNT=$((BACKUP_COUNT + CRED_BACKUPS)) + BACKUP_REMOVED=1 fi - # Show agent backups - if [ "$AGENT_BACKUPS" -gt 0 ]; then - echo " Agent script backups:" - ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | while read -r file; do - echo " • $file" - done + # Remove config backups (already removed with /etc/patchmon directory, but check anyway) + if ls /etc/patchmon/*.backup.* >/dev/null 2>&1; then + CONFIG_BACKUPS=$(ls /etc/patchmon/*.backup.* 2>/dev/null | wc -l | tr -d ' ') + warning "Removing $CONFIG_BACKUPS config backup file(s)..." + rm -f /etc/patchmon/*.backup.* + BACKUP_COUNT=$((BACKUP_COUNT + CONFIG_BACKUPS)) + BACKUP_REMOVED=1 fi - # Show log backups - if [ "$LOG_BACKUPS" -gt 0 ]; then - echo " Log file backups:" - ls /var/log/patchmon-agent.log.old.* 2>/dev/null | while read -r file; do - echo " • $file" - done + # Remove Go agent backups + if ls /usr/local/bin/patchmon-agent.backup.* >/dev/null 2>&1; then + GO_AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.backup.* 2>/dev/null | wc -l | tr -d ' ') + warning "Removing $GO_AGENT_BACKUPS Go agent backup file(s)..." + rm -f /usr/local/bin/patchmon-agent.backup.* + BACKUP_COUNT=$((BACKUP_COUNT + GO_AGENT_BACKUPS)) + BACKUP_REMOVED=1 fi - echo "" - printf "%b\n" "${BLUE}💡 Note: Backup files are preserved for safety${NC}" - printf "%b\n" "${BLUE}💡 You can remove them manually if not needed${NC}" + # Remove legacy shell agent backups + if ls /usr/local/bin/patchmon-agent.sh.backup.* >/dev/null 2>&1; then + SHELL_AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | wc -l | tr -d ' ') + warning "Removing $SHELL_AGENT_BACKUPS legacy agent backup file(s)..." + rm -f /usr/local/bin/patchmon-agent.sh.backup.* + BACKUP_COUNT=$((BACKUP_COUNT + SHELL_AGENT_BACKUPS)) + BACKUP_REMOVED=1 + fi + + # Remove log backups + if ls /var/log/patchmon-agent.log.old.* >/dev/null 2>&1; then + LOG_BACKUPS=$(ls /var/log/patchmon-agent.log.old.* 2>/dev/null | wc -l | tr -d ' ') + warning "Removing $LOG_BACKUPS log backup file(s)..." + rm -f /var/log/patchmon-agent.log.old.* + BACKUP_COUNT=$((BACKUP_COUNT + LOG_BACKUPS)) + BACKUP_REMOVED=1 + fi + + if [ "$BACKUP_REMOVED" -eq 1 ]; then + success "Removed $BACKUP_COUNT backup file(s)" + else + info "No backup files found to remove" + fi else - info "No backup files found" + # Just count backup files without removing + CRED_BACKUPS=0 + CONFIG_BACKUPS=0 + GO_AGENT_BACKUPS=0 + SHELL_AGENT_BACKUPS=0 + LOG_BACKUPS=0 + + if ls /etc/patchmon/credentials.backup.* >/dev/null 2>&1; then + CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l | tr -d ' ') + fi + + if ls /etc/patchmon/*.backup.* >/dev/null 2>&1; then + CONFIG_BACKUPS=$(ls /etc/patchmon/*.backup.* 2>/dev/null | wc -l | tr -d ' ') + fi + + if ls /usr/local/bin/patchmon-agent.backup.* >/dev/null 2>&1; then + GO_AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.backup.* 2>/dev/null | wc -l | tr -d ' ') + fi + + if ls /usr/local/bin/patchmon-agent.sh.backup.* >/dev/null 2>&1; then + SHELL_AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | wc -l | tr -d ' ') + fi + + if ls /var/log/patchmon-agent.log.old.* >/dev/null 2>&1; then + LOG_BACKUPS=$(ls /var/log/patchmon-agent.log.old.* 2>/dev/null | wc -l | tr -d ' ') + fi + + BACKUP_COUNT=$((CRED_BACKUPS + CONFIG_BACKUPS + GO_AGENT_BACKUPS + SHELL_AGENT_BACKUPS + LOG_BACKUPS)) + + if [ "$BACKUP_COUNT" -gt 0 ]; then + info "Found $BACKUP_COUNT backup file(s) - preserved for safety" + if [ "$SILENT_MODE" -eq 0 ]; then + printf "%b\n" "${BLUE}💡 To remove backups, run with: REMOVE_BACKUPS=1${NC}" + fi + else + info "No backup files found" + fi fi -# Step 7: Remove dependencies (optional) -info "📦 Checking for PatchMon-specific dependencies..." -if command -v jq >/dev/null 2>&1; then - warning "jq is installed (used by PatchMon)" - printf "%b\n" "${BLUE}💡 Note: jq may be used by other applications${NC}" - printf "%b\n" "${BLUE}💡 Consider keeping it unless you're sure it's not needed${NC}" -else - info "jq not found" -fi - -if command -v curl >/dev/null 2>&1; then - warning "curl is installed (used by PatchMon)" - printf "%b\n" "${BLUE}💡 Note: curl is commonly used by many applications${NC}" - printf "%b\n" "${BLUE}💡 Consider keeping it unless you're sure it's not needed${NC}" -else - info "curl not found" -fi - -# Step 8: Final verification +# Step 7: Final verification info "🔍 Verifying removal..." REMAINING_FILES=0 @@ -294,21 +431,30 @@ if [ "$REMAINING_FILES" -eq 0 ]; then success "✅ PatchMon has been completely removed from the system!" else warning "⚠️ Some PatchMon files may still remain ($REMAINING_FILES items)" - printf "%b\n" "${BLUE}💡 You may need to remove them manually${NC}" + if [ "$SILENT_MODE" -eq 0 ]; then + printf "%b\n" "${BLUE}💡 You may need to remove them manually${NC}" + fi fi -echo "" -printf "%b\n" "${GREEN}📋 Removal Summary:${NC}" -echo " • Agent binaries: Removed" -echo " • System services: Removed (systemd/OpenRC)" -echo " • Configuration files: Removed" -echo " • Log files: Removed" -echo " • Crontab entries: Removed" -echo " • Running processes: Stopped" -echo " • Backup files: Preserved (if any)" -echo "" -printf "%b\n" "${BLUE}🔧 Manual cleanup (if needed):${NC}" -echo " • Remove backup files: rm /etc/patchmon/credentials.backup.* /usr/local/bin/patchmon-agent.sh.backup.* /var/log/patchmon-agent.log.old.*" -echo " • Remove dependencies: apt remove jq curl (if not needed by other apps)" -echo "" +if [ "$SILENT_MODE" -eq 0 ]; then + echo "" + printf "%b\n" "${GREEN}📋 Removal Summary:${NC}" + echo " • Agent binaries: Removed" + echo " • System services: Removed (systemd/OpenRC)" + echo " • Configuration files: Removed" + echo " • Log files: Removed" + echo " • Crontab entries: Removed" + echo " • Running processes: Stopped" + if [ "$REMOVE_BACKUPS" -eq 1 ]; then + echo " • Backup files: Removed" + else + echo " • Backup files: Preserved (${BACKUP_COUNT} files)" + fi + echo "" + if [ "$REMOVE_BACKUPS" -eq 0 ] && [ "$BACKUP_COUNT" -gt 0 ]; then + printf "%b\n" "${BLUE}🔧 Manual cleanup (if needed):${NC}" + echo " • Remove backup files: curl -s \${PATCHMON_URL}/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1 sh" + echo "" + fi +fi success "🎉 PatchMon removal completed!" diff --git a/backend/prisma/migrations/20251116000000_add_needs_reboot_field/migration.sql b/backend/prisma/migrations/20251116000000_add_needs_reboot_field/migration.sql new file mode 100644 index 0000000..ae92107 --- /dev/null +++ b/backend/prisma/migrations/20251116000000_add_needs_reboot_field/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "hosts" ADD COLUMN "needs_reboot" BOOLEAN DEFAULT false; +ALTER TABLE "hosts" ADD COLUMN "installed_kernel_version" TEXT; + +-- CreateIndex +CREATE INDEX "hosts_needs_reboot_idx" ON "hosts"("needs_reboot"); + diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index c7b99d9..6e01920 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -110,6 +110,7 @@ model hosts { swap_size Int? system_uptime String? notes String? + needs_reboot Boolean? @default(false) host_packages host_packages[] host_repositories host_repositories[] host_group_memberships host_group_memberships[] @@ -121,6 +122,7 @@ model hosts { @@index([machine_id]) @@index([friendly_name]) @@index([hostname]) + @@index([needs_reboot]) } model packages { diff --git a/backend/src/routes/dashboardPreferencesRoutes.js b/backend/src/routes/dashboardPreferencesRoutes.js index 81b0787..d17a9bd 100644 --- a/backend/src/routes/dashboardPreferencesRoutes.js +++ b/backend/src/routes/dashboardPreferencesRoutes.js @@ -92,58 +92,63 @@ async function createDefaultDashboardPreferences(userId, userRole = "user") { requiredPermission: "can_view_hosts", order: 5, }, + { + cardId: "hostsNeedingReboot", + requiredPermission: "can_view_hosts", + order: 6, + }, // Repository-related cards - { cardId: "totalRepos", requiredPermission: "can_view_hosts", order: 6 }, + { cardId: "totalRepos", requiredPermission: "can_view_hosts", order: 7 }, // User management cards (admin only) - { cardId: "totalUsers", requiredPermission: "can_view_users", order: 7 }, + { cardId: "totalUsers", requiredPermission: "can_view_users", order: 8 }, // System/Report cards { cardId: "osDistribution", requiredPermission: "can_view_reports", - order: 8, + order: 9, }, { cardId: "osDistributionBar", requiredPermission: "can_view_reports", - order: 9, + order: 10, }, { cardId: "osDistributionDoughnut", requiredPermission: "can_view_reports", - order: 10, + order: 11, }, { cardId: "recentCollection", requiredPermission: "can_view_hosts", - order: 11, + order: 12, }, { cardId: "updateStatus", requiredPermission: "can_view_reports", - order: 12, + order: 13, }, { cardId: "packagePriority", requiredPermission: "can_view_packages", - order: 13, + order: 14, }, { cardId: "packageTrends", requiredPermission: "can_view_packages", - order: 14, + order: 15, }, { cardId: "recentUsers", requiredPermission: "can_view_users", - order: 15, + order: 16, }, { cardId: "quickStats", requiredPermission: "can_view_dashboard", - order: 16, + order: 17, }, ]; @@ -290,26 +295,33 @@ router.get("/defaults", authenticateToken, async (_req, res) => { enabled: true, order: 5, }, + { + cardId: "hostsNeedingReboot", + title: "Needs Reboots", + icon: "RotateCcw", + enabled: true, + order: 6, + }, { cardId: "totalRepos", title: "Repositories", icon: "GitBranch", enabled: true, - order: 6, + order: 7, }, { cardId: "totalUsers", title: "Users", icon: "Users", enabled: true, - order: 7, + order: 8, }, { cardId: "osDistribution", title: "OS Distribution", icon: "BarChart3", enabled: true, - order: 8, + order: 9, }, { cardId: "osDistributionBar", diff --git a/backend/src/routes/dashboardRoutes.js b/backend/src/routes/dashboardRoutes.js index bfc03db..6bcb211 100644 --- a/backend/src/routes/dashboardRoutes.js +++ b/backend/src/routes/dashboardRoutes.js @@ -41,6 +41,7 @@ router.get( erroredHosts, securityUpdates, offlineHosts, + hostsNeedingReboot, totalHostGroups, totalUsers, totalRepos, @@ -106,6 +107,13 @@ router.get( }, }), + // Hosts needing reboot + prisma.hosts.count({ + where: { + needs_reboot: true, + }, + }), + // Total host groups count prisma.host_groups.count(), @@ -174,6 +182,7 @@ router.get( erroredHosts, securityUpdates, offlineHosts, + hostsNeedingReboot, totalHostGroups, totalUsers, totalRepos, @@ -217,6 +226,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => { auto_update: true, notes: true, api_id: true, + needs_reboot: true, host_group_memberships: { include: { host_groups: { diff --git a/backend/src/routes/hostGroupRoutes.js b/backend/src/routes/hostGroupRoutes.js index 9102967..b840436 100644 --- a/backend/src/routes/hostGroupRoutes.js +++ b/backend/src/routes/hostGroupRoutes.js @@ -59,6 +59,7 @@ router.get("/:id", authenticateToken, async (req, res) => { os_version: true, status: true, last_update: true, + needs_reboot: true, }, }, }, @@ -259,6 +260,7 @@ router.get("/:id/hosts", authenticateToken, async (req, res) => { status: true, last_update: true, created_at: true, + needs_reboot: true, }, orderBy: { friendly_name: "asc", diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 491c098..9723612 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -533,6 +533,14 @@ router.post( .optional() .isString() .withMessage("Machine ID must be a string"), + body("needsReboot") + .optional() + .isBoolean() + .withMessage("Needs reboot must be a boolean"), + body("rebootReason") + .optional() + .isString() + .withMessage("Reboot reason must be a string"), ], async (req, res) => { try { @@ -596,6 +604,10 @@ router.post( updateData.system_uptime = req.body.systemUptime; if (req.body.loadAverage) updateData.load_average = req.body.loadAverage; + // Reboot Status + if (req.body.needsReboot !== undefined) + updateData.needs_reboot = req.body.needsReboot; + // If this is the first update (status is 'pending'), change to 'active' if (host.status === "pending") { updateData.status = "active"; diff --git a/backend/src/routes/packageRoutes.js b/backend/src/routes/packageRoutes.js index cf25841..15d3f75 100644 --- a/backend/src/routes/packageRoutes.js +++ b/backend/src/routes/packageRoutes.js @@ -142,6 +142,7 @@ router.get("/", async (req, res) => { friendly_name: true, hostname: true, os_type: true, + needs_reboot: true, }, }, current_version: true, @@ -236,6 +237,7 @@ router.get("/:packageId", async (req, res) => { os_type: true, os_version: true, last_update: true, + needs_reboot: true, }, }, }, @@ -365,6 +367,7 @@ router.get("/:packageId/hosts", async (req, res) => { os_type: true, os_version: true, last_update: true, + needs_reboot: true, }, }, }, @@ -386,6 +389,7 @@ router.get("/:packageId/hosts", async (req, res) => { needsUpdate: hp.needs_update, isSecurityUpdate: hp.is_security_update, lastChecked: hp.last_checked, + needsReboot: hp.hosts.needs_reboot, })); res.json({ diff --git a/backend/src/routes/repositoryRoutes.js b/backend/src/routes/repositoryRoutes.js index 37c35b8..0d0dafc 100644 --- a/backend/src/routes/repositoryRoutes.js +++ b/backend/src/routes/repositoryRoutes.js @@ -119,6 +119,7 @@ router.get( os_version: true, status: true, last_update: true, + needs_reboot: true, }, }, }, diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template index 8936ae5..621e239 100644 --- a/docker/nginx.conf.template +++ b/docker/nginx.conf.template @@ -24,6 +24,9 @@ server { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Prevent search engine indexing + add_header X-Robots-Tag "noindex, nofollow, noarchive, nosnippet" always; # Bull Board proxy - must come before the root location to avoid conflicts location /bullboard { diff --git a/frontend/index.html b/frontend/index.html index 253c75c..2c6f5e0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,18 @@ + + + + + + + + + + + +
+ {toast.message} +
+To completely remove PatchMon from a host:
- {/* Go Agent Uninstall */} + {/* Agent Removal Script - Standard */}--remove-config,{" "}
- --remove-logs, --remove-all,{" "}
- --force
+ This removes: binaries, systemd/OpenRC services,
+ configuration files, logs, crontab entries, and backup files
- ⚠️ This command will remove all PatchMon files, configuration, - and crontab entries +
+ ⚠️ Standard removal preserves backup files for safety. Use + complete removal to delete everything.
@@ -1006,20 +1013,32 @@ const HostDetail = () => {
- Kernel Version -
-- {host.kernel_version} -
+ {(host.kernel_version || + host.installed_kernel_version) && ( ++ Running Kernel +
++ {host.kernel_version} +
++ Installed Kernel +
++ {host.installed_kernel_version} +
+diff --git a/frontend/src/pages/Hosts.jsx b/frontend/src/pages/Hosts.jsx index 265a5a4..455acd7 100644 --- a/frontend/src/pages/Hosts.jsx +++ b/frontend/src/pages/Hosts.jsx @@ -16,6 +16,7 @@ import { GripVertical, Plus, RefreshCw, + RotateCcw, Search, Server, Square, @@ -247,6 +248,7 @@ const Hosts = () => { const showFiltersParam = searchParams.get("showFilters"); const osFilterParam = searchParams.get("osFilter"); const groupParam = searchParams.get("group"); + const rebootParam = searchParams.get("reboot"); if (filter === "needsUpdates") { setShowFilters(true); @@ -331,10 +333,11 @@ const Hosts = () => { }, { id: "ws_status", label: "Connection", visible: true, order: 9 }, { id: "status", label: "Status", visible: true, order: 10 }, - { id: "updates", label: "Updates", visible: true, order: 11 }, - { id: "notes", label: "Notes", visible: false, order: 12 }, - { id: "last_update", label: "Last Update", visible: true, order: 13 }, - { id: "actions", label: "Actions", visible: true, order: 14 }, + { id: "needs_reboot", label: "Reboot", visible: true, order: 11 }, + { id: "updates", label: "Updates", visible: true, order: 12 }, + { id: "notes", label: "Notes", visible: false, order: 13 }, + { id: "last_update", label: "Last Update", visible: true, order: 14 }, + { id: "actions", label: "Actions", visible: true, order: 15 }, ]; const saved = localStorage.getItem("hosts-column-config"); @@ -356,8 +359,25 @@ const Hosts = () => { localStorage.removeItem("hosts-column-config"); return defaultConfig; } else { - // Ensure ws_status column is visible in saved config - const updatedConfig = savedConfig.map((col) => + // Merge saved config with defaults to handle new columns + // This preserves user's visibility preferences while adding new columns + const mergedConfig = defaultConfig.map((defaultCol) => { + const savedCol = savedConfig.find( + (col) => col.id === defaultCol.id, + ); + if (savedCol) { + // Use saved visibility preference, but keep default order and label + return { + ...defaultCol, + visible: savedCol.visible, + }; + } + // New column not in saved config, use default + return defaultCol; + }); + + // Ensure ws_status column is visible + const updatedConfig = mergedConfig.map((col) => col.id === "ws_status" ? { ...col, visible: true } : col, ); return updatedConfig; @@ -673,8 +693,9 @@ const Hosts = () => { osFilter === "all" || host.os_type?.toLowerCase() === osFilter.toLowerCase(); - // URL filter for hosts needing updates, inactive hosts, up-to-date hosts, stale hosts, or offline hosts + // URL filter for hosts needing updates, inactive hosts, up-to-date hosts, stale hosts, offline hosts, or reboot required const filter = searchParams.get("filter"); + const rebootParam = searchParams.get("reboot"); const matchesUrlFilter = (filter !== "needsUpdates" || (host.updatesCount && host.updatesCount > 0)) && @@ -682,7 +703,9 @@ const Hosts = () => { (host.effectiveStatus || host.status) === "inactive") && (filter !== "upToDate" || (!host.isStale && host.updatesCount === 0)) && (filter !== "stale" || host.isStale) && - (filter !== "offline" || wsStatusMap[host.api_id]?.connected !== true); + (filter !== "offline" || + wsStatusMap[host.api_id]?.connected !== true) && + (!rebootParam || host.needs_reboot === true); // Hide stale filter const matchesHideStale = !hideStale || !host.isStale; @@ -758,6 +781,11 @@ const Hosts = () => { aValue = a.updatesCount || 0; bValue = b.updatesCount || 0; break; + case "needs_reboot": + // Sort by boolean: false (0) comes before true (1) + aValue = a.needs_reboot ? 1 : 0; + bValue = b.needs_reboot ? 1 : 0; + break; case "last_update": aValue = new Date(a.last_update); bValue = new Date(b.last_update); @@ -917,10 +945,11 @@ const Hosts = () => { }, { id: "ws_status", label: "Connection", visible: true, order: 9 }, { id: "status", label: "Status", visible: true, order: 10 }, - { id: "updates", label: "Updates", visible: true, order: 11 }, - { id: "notes", label: "Notes", visible: false, order: 12 }, - { id: "last_update", label: "Last Update", visible: true, order: 13 }, - { id: "actions", label: "Actions", visible: true, order: 14 }, + { id: "needs_reboot", label: "Reboot", visible: true, order: 11 }, + { id: "updates", label: "Updates", visible: true, order: 12 }, + { id: "notes", label: "Notes", visible: false, order: 13 }, + { id: "last_update", label: "Last Update", visible: true, order: 14 }, + { id: "actions", label: "Actions", visible: true, order: 15 }, ]; updateColumnConfig(defaultConfig); }; @@ -1077,6 +1106,22 @@ const Hosts = () => { (host.effectiveStatus || host.status).slice(1)}