mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-21 15:09:21 +00:00
added reboot required flag
added kernel running/installed version Added dasboard card and filteres for reboot fixed security qty updated on dnf
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
# PatchMon Agent Removal Script
|
# PatchMon Agent Removal Script
|
||||||
# POSIX-compliant shell script (works with dash, ash, bash, etc.)
|
# 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
|
# This script completely removes PatchMon from the system
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -12,12 +14,30 @@ set -e
|
|||||||
# future (left for consistency with install script).
|
# future (left for consistency with install script).
|
||||||
CURL_FLAGS=""
|
CURL_FLAGS=""
|
||||||
|
|
||||||
# Colors for output
|
# Detect if running in silent mode (only with explicit SILENT env var)
|
||||||
RED='\033[0;31m'
|
SILENT_MODE=0
|
||||||
GREEN='\033[0;32m'
|
if [ -n "$SILENT" ]; then
|
||||||
YELLOW='\033[1;33m'
|
SILENT_MODE=1
|
||||||
BLUE='\033[0;34m'
|
fi
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
# 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
|
# Functions
|
||||||
error() {
|
error() {
|
||||||
@@ -26,15 +46,21 @@ error() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info() {
|
info() {
|
||||||
printf "%b\n" "${BLUE}ℹ️ $1${NC}"
|
if [ "$SILENT_MODE" -eq 0 ]; then
|
||||||
|
printf "%b\n" "${BLUE}ℹ️ $1${NC}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
success() {
|
success() {
|
||||||
printf "%b\n" "${GREEN}✅ $1${NC}"
|
if [ "$SILENT_MODE" -eq 0 ]; then
|
||||||
|
printf "%b\n" "${GREEN}✅ $1${NC}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
warning() {
|
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
|
# Check if running as root
|
||||||
@@ -43,7 +69,7 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
info "🗑️ Starting PatchMon Agent Removal..."
|
info "🗑️ Starting PatchMon Agent Removal..."
|
||||||
echo ""
|
[ "$SILENT_MODE" -eq 0 ] && echo ""
|
||||||
|
|
||||||
# Step 1: Stop systemd/OpenRC service if it exists
|
# Step 1: Stop systemd/OpenRC service if it exists
|
||||||
info "🛑 Stopping PatchMon service..."
|
info "🛑 Stopping PatchMon service..."
|
||||||
@@ -51,24 +77,75 @@ SERVICE_STOPPED=0
|
|||||||
|
|
||||||
# Check for systemd service
|
# Check for systemd service
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
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
|
if systemctl is-active --quiet patchmon-agent.service 2>/dev/null; then
|
||||||
warning "Stopping systemd service..."
|
SERVICE_STATUS=$(systemctl is-active patchmon-agent.service 2>/dev/null || echo "unknown")
|
||||||
systemctl stop patchmon-agent.service || true
|
warning "Service is active (status: $SERVICE_STATUS). Stopping it now..."
|
||||||
SERVICE_STOPPED=1
|
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
|
fi
|
||||||
|
|
||||||
|
# Check if service is enabled
|
||||||
if systemctl is-enabled --quiet patchmon-agent.service 2>/dev/null; then
|
if systemctl is-enabled --quiet patchmon-agent.service 2>/dev/null; then
|
||||||
warning "Disabling systemd service..."
|
ENABLED_STATUS=$(systemctl is-enabled patchmon-agent.service 2>/dev/null || echo "unknown")
|
||||||
systemctl disable patchmon-agent.service || true
|
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
|
fi
|
||||||
|
|
||||||
|
# Check for service file
|
||||||
if [ -f "/etc/systemd/system/patchmon-agent.service" ]; then
|
if [ -f "/etc/systemd/system/patchmon-agent.service" ]; then
|
||||||
warning "Removing systemd service file..."
|
warning "Found service file: /etc/systemd/system/patchmon-agent.service"
|
||||||
rm -f /etc/systemd/system/patchmon-agent.service
|
info "Removing service file..."
|
||||||
systemctl daemon-reload || true
|
if rm -f /etc/systemd/system/patchmon-agent.service 2>/dev/null; then
|
||||||
success "Systemd service removed"
|
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
|
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
|
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
|
fi
|
||||||
|
|
||||||
# Check for OpenRC service (Alpine Linux)
|
# Check for OpenRC service (Alpine Linux)
|
||||||
@@ -93,11 +170,47 @@ if command -v rc-service >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Stop any remaining running processes (legacy or manual starts)
|
# Stop any remaining running processes (legacy or manual starts)
|
||||||
|
info "🔍 Checking for running PatchMon processes..."
|
||||||
if pgrep -f "patchmon-agent" >/dev/null; then
|
if pgrep -f "patchmon-agent" >/dev/null; then
|
||||||
warning "Found running PatchMon processes, stopping them..."
|
PROCESS_COUNT=$(pgrep -f "patchmon-agent" | wc -l | tr -d ' ')
|
||||||
pkill -f "patchmon-agent" || true
|
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
|
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
|
SERVICE_STOPPED=1
|
||||||
|
else
|
||||||
|
info "No running PatchMon processes found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$SERVICE_STOPPED" -eq 1 ]; then
|
if [ "$SERVICE_STOPPED" -eq 1 ]; then
|
||||||
@@ -159,11 +272,13 @@ info "📁 Removing configuration files..."
|
|||||||
if [ -d "/etc/patchmon" ]; then
|
if [ -d "/etc/patchmon" ]; then
|
||||||
warning "Removing configuration directory: /etc/patchmon"
|
warning "Removing configuration directory: /etc/patchmon"
|
||||||
|
|
||||||
# Show what's being removed
|
# Show what's being removed (only in verbose mode)
|
||||||
info "📋 Files in /etc/patchmon:"
|
if [ "$SILENT_MODE" -eq 0 ]; then
|
||||||
ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do
|
info "📋 Files in /etc/patchmon:"
|
||||||
echo " $line"
|
ls -la /etc/patchmon/ 2>/dev/null | grep -v "^total" | while read -r line; do
|
||||||
done
|
echo " $line"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove the directory
|
# Remove the directory
|
||||||
rm -rf /etc/patchmon
|
rm -rf /etc/patchmon
|
||||||
@@ -182,83 +297,105 @@ else
|
|||||||
info "Log file not found"
|
info "Log file not found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 6: Clean up backup files (optional)
|
# Step 6: Clean up backup files
|
||||||
info "🧹 Cleaning up backup files..."
|
info "🧹 Checking backup files..."
|
||||||
BACKUP_COUNT=0
|
BACKUP_COUNT=0
|
||||||
|
BACKUP_REMOVED=0
|
||||||
|
|
||||||
# Count credential backups
|
if [ "$REMOVE_BACKUPS" -eq 1 ]; then
|
||||||
CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l || echo "0")
|
info "Removing backup files (REMOVE_BACKUPS=1)..."
|
||||||
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}"
|
|
||||||
|
|
||||||
# Show credential backups
|
# Remove credential backups (already removed with /etc/patchmon directory, but check anyway)
|
||||||
if [ "$CRED_BACKUPS" -gt 0 ]; then
|
if ls /etc/patchmon/credentials.backup.* >/dev/null 2>&1; then
|
||||||
echo " Credential backups:"
|
CRED_BACKUPS=$(ls /etc/patchmon/credentials.backup.* 2>/dev/null | wc -l | tr -d ' ')
|
||||||
ls /etc/patchmon/credentials.backup.* 2>/dev/null | while read -r file; do
|
warning "Removing $CRED_BACKUPS credential backup file(s)..."
|
||||||
echo " • $file"
|
rm -f /etc/patchmon/credentials.backup.*
|
||||||
done
|
BACKUP_COUNT=$((BACKUP_COUNT + CRED_BACKUPS))
|
||||||
|
BACKUP_REMOVED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Show agent backups
|
# Remove config backups (already removed with /etc/patchmon directory, but check anyway)
|
||||||
if [ "$AGENT_BACKUPS" -gt 0 ]; then
|
if ls /etc/patchmon/*.backup.* >/dev/null 2>&1; then
|
||||||
echo " Agent script backups:"
|
CONFIG_BACKUPS=$(ls /etc/patchmon/*.backup.* 2>/dev/null | wc -l | tr -d ' ')
|
||||||
ls /usr/local/bin/patchmon-agent.sh.backup.* 2>/dev/null | while read -r file; do
|
warning "Removing $CONFIG_BACKUPS config backup file(s)..."
|
||||||
echo " • $file"
|
rm -f /etc/patchmon/*.backup.*
|
||||||
done
|
BACKUP_COUNT=$((BACKUP_COUNT + CONFIG_BACKUPS))
|
||||||
|
BACKUP_REMOVED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Show log backups
|
# Remove Go agent backups
|
||||||
if [ "$LOG_BACKUPS" -gt 0 ]; then
|
if ls /usr/local/bin/patchmon-agent.backup.* >/dev/null 2>&1; then
|
||||||
echo " Log file backups:"
|
GO_AGENT_BACKUPS=$(ls /usr/local/bin/patchmon-agent.backup.* 2>/dev/null | wc -l | tr -d ' ')
|
||||||
ls /var/log/patchmon-agent.log.old.* 2>/dev/null | while read -r file; do
|
warning "Removing $GO_AGENT_BACKUPS Go agent backup file(s)..."
|
||||||
echo " • $file"
|
rm -f /usr/local/bin/patchmon-agent.backup.*
|
||||||
done
|
BACKUP_COUNT=$((BACKUP_COUNT + GO_AGENT_BACKUPS))
|
||||||
|
BACKUP_REMOVED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
# Remove legacy shell agent backups
|
||||||
printf "%b\n" "${BLUE}💡 Note: Backup files are preserved for safety${NC}"
|
if ls /usr/local/bin/patchmon-agent.sh.backup.* >/dev/null 2>&1; then
|
||||||
printf "%b\n" "${BLUE}💡 You can remove them manually if not needed${NC}"
|
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
|
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
|
fi
|
||||||
|
|
||||||
# Step 7: Remove dependencies (optional)
|
# Step 7: Final verification
|
||||||
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
|
|
||||||
info "🔍 Verifying removal..."
|
info "🔍 Verifying removal..."
|
||||||
REMAINING_FILES=0
|
REMAINING_FILES=0
|
||||||
|
|
||||||
@@ -294,21 +431,30 @@ if [ "$REMAINING_FILES" -eq 0 ]; then
|
|||||||
success "✅ PatchMon has been completely removed from the system!"
|
success "✅ PatchMon has been completely removed from the system!"
|
||||||
else
|
else
|
||||||
warning "⚠️ Some PatchMon files may still remain ($REMAINING_FILES items)"
|
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
|
fi
|
||||||
|
|
||||||
echo ""
|
if [ "$SILENT_MODE" -eq 0 ]; then
|
||||||
printf "%b\n" "${GREEN}📋 Removal Summary:${NC}"
|
echo ""
|
||||||
echo " • Agent binaries: Removed"
|
printf "%b\n" "${GREEN}📋 Removal Summary:${NC}"
|
||||||
echo " • System services: Removed (systemd/OpenRC)"
|
echo " • Agent binaries: Removed"
|
||||||
echo " • Configuration files: Removed"
|
echo " • System services: Removed (systemd/OpenRC)"
|
||||||
echo " • Log files: Removed"
|
echo " • Configuration files: Removed"
|
||||||
echo " • Crontab entries: Removed"
|
echo " • Log files: Removed"
|
||||||
echo " • Running processes: Stopped"
|
echo " • Crontab entries: Removed"
|
||||||
echo " • Backup files: Preserved (if any)"
|
echo " • Running processes: Stopped"
|
||||||
echo ""
|
if [ "$REMOVE_BACKUPS" -eq 1 ]; then
|
||||||
printf "%b\n" "${BLUE}🔧 Manual cleanup (if needed):${NC}"
|
echo " • Backup files: Removed"
|
||||||
echo " • Remove backup files: rm /etc/patchmon/credentials.backup.* /usr/local/bin/patchmon-agent.sh.backup.* /var/log/patchmon-agent.log.old.*"
|
else
|
||||||
echo " • Remove dependencies: apt remove jq curl (if not needed by other apps)"
|
echo " • Backup files: Preserved (${BACKUP_COUNT} files)"
|
||||||
echo ""
|
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!"
|
success "🎉 PatchMon removal completed!"
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
@@ -110,6 +110,7 @@ model hosts {
|
|||||||
swap_size Int?
|
swap_size Int?
|
||||||
system_uptime String?
|
system_uptime String?
|
||||||
notes String?
|
notes String?
|
||||||
|
needs_reboot Boolean? @default(false)
|
||||||
host_packages host_packages[]
|
host_packages host_packages[]
|
||||||
host_repositories host_repositories[]
|
host_repositories host_repositories[]
|
||||||
host_group_memberships host_group_memberships[]
|
host_group_memberships host_group_memberships[]
|
||||||
@@ -121,6 +122,7 @@ model hosts {
|
|||||||
@@index([machine_id])
|
@@index([machine_id])
|
||||||
@@index([friendly_name])
|
@@index([friendly_name])
|
||||||
@@index([hostname])
|
@@index([hostname])
|
||||||
|
@@index([needs_reboot])
|
||||||
}
|
}
|
||||||
|
|
||||||
model packages {
|
model packages {
|
||||||
|
|||||||
@@ -92,58 +92,63 @@ async function createDefaultDashboardPreferences(userId, userRole = "user") {
|
|||||||
requiredPermission: "can_view_hosts",
|
requiredPermission: "can_view_hosts",
|
||||||
order: 5,
|
order: 5,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cardId: "hostsNeedingReboot",
|
||||||
|
requiredPermission: "can_view_hosts",
|
||||||
|
order: 6,
|
||||||
|
},
|
||||||
|
|
||||||
// Repository-related cards
|
// Repository-related cards
|
||||||
{ cardId: "totalRepos", requiredPermission: "can_view_hosts", order: 6 },
|
{ cardId: "totalRepos", requiredPermission: "can_view_hosts", order: 7 },
|
||||||
|
|
||||||
// User management cards (admin only)
|
// User management cards (admin only)
|
||||||
{ cardId: "totalUsers", requiredPermission: "can_view_users", order: 7 },
|
{ cardId: "totalUsers", requiredPermission: "can_view_users", order: 8 },
|
||||||
|
|
||||||
// System/Report cards
|
// System/Report cards
|
||||||
{
|
{
|
||||||
cardId: "osDistribution",
|
cardId: "osDistribution",
|
||||||
requiredPermission: "can_view_reports",
|
requiredPermission: "can_view_reports",
|
||||||
order: 8,
|
order: 9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "osDistributionBar",
|
cardId: "osDistributionBar",
|
||||||
requiredPermission: "can_view_reports",
|
requiredPermission: "can_view_reports",
|
||||||
order: 9,
|
order: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "osDistributionDoughnut",
|
cardId: "osDistributionDoughnut",
|
||||||
requiredPermission: "can_view_reports",
|
requiredPermission: "can_view_reports",
|
||||||
order: 10,
|
order: 11,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "recentCollection",
|
cardId: "recentCollection",
|
||||||
requiredPermission: "can_view_hosts",
|
requiredPermission: "can_view_hosts",
|
||||||
order: 11,
|
order: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "updateStatus",
|
cardId: "updateStatus",
|
||||||
requiredPermission: "can_view_reports",
|
requiredPermission: "can_view_reports",
|
||||||
order: 12,
|
order: 13,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "packagePriority",
|
cardId: "packagePriority",
|
||||||
requiredPermission: "can_view_packages",
|
requiredPermission: "can_view_packages",
|
||||||
order: 13,
|
order: 14,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "packageTrends",
|
cardId: "packageTrends",
|
||||||
requiredPermission: "can_view_packages",
|
requiredPermission: "can_view_packages",
|
||||||
order: 14,
|
order: 15,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "recentUsers",
|
cardId: "recentUsers",
|
||||||
requiredPermission: "can_view_users",
|
requiredPermission: "can_view_users",
|
||||||
order: 15,
|
order: 16,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "quickStats",
|
cardId: "quickStats",
|
||||||
requiredPermission: "can_view_dashboard",
|
requiredPermission: "can_view_dashboard",
|
||||||
order: 16,
|
order: 17,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -290,26 +295,33 @@ router.get("/defaults", authenticateToken, async (_req, res) => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
order: 5,
|
order: 5,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cardId: "hostsNeedingReboot",
|
||||||
|
title: "Needs Reboots",
|
||||||
|
icon: "RotateCcw",
|
||||||
|
enabled: true,
|
||||||
|
order: 6,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
cardId: "totalRepos",
|
cardId: "totalRepos",
|
||||||
title: "Repositories",
|
title: "Repositories",
|
||||||
icon: "GitBranch",
|
icon: "GitBranch",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 6,
|
order: 7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "totalUsers",
|
cardId: "totalUsers",
|
||||||
title: "Users",
|
title: "Users",
|
||||||
icon: "Users",
|
icon: "Users",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 7,
|
order: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "osDistribution",
|
cardId: "osDistribution",
|
||||||
title: "OS Distribution",
|
title: "OS Distribution",
|
||||||
icon: "BarChart3",
|
icon: "BarChart3",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 8,
|
order: 9,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cardId: "osDistributionBar",
|
cardId: "osDistributionBar",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ router.get(
|
|||||||
erroredHosts,
|
erroredHosts,
|
||||||
securityUpdates,
|
securityUpdates,
|
||||||
offlineHosts,
|
offlineHosts,
|
||||||
|
hostsNeedingReboot,
|
||||||
totalHostGroups,
|
totalHostGroups,
|
||||||
totalUsers,
|
totalUsers,
|
||||||
totalRepos,
|
totalRepos,
|
||||||
@@ -106,6 +107,13 @@ router.get(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Hosts needing reboot
|
||||||
|
prisma.hosts.count({
|
||||||
|
where: {
|
||||||
|
needs_reboot: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
// Total host groups count
|
// Total host groups count
|
||||||
prisma.host_groups.count(),
|
prisma.host_groups.count(),
|
||||||
|
|
||||||
@@ -174,6 +182,7 @@ router.get(
|
|||||||
erroredHosts,
|
erroredHosts,
|
||||||
securityUpdates,
|
securityUpdates,
|
||||||
offlineHosts,
|
offlineHosts,
|
||||||
|
hostsNeedingReboot,
|
||||||
totalHostGroups,
|
totalHostGroups,
|
||||||
totalUsers,
|
totalUsers,
|
||||||
totalRepos,
|
totalRepos,
|
||||||
@@ -217,6 +226,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
|
|||||||
auto_update: true,
|
auto_update: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
api_id: true,
|
api_id: true,
|
||||||
|
needs_reboot: true,
|
||||||
host_group_memberships: {
|
host_group_memberships: {
|
||||||
include: {
|
include: {
|
||||||
host_groups: {
|
host_groups: {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ router.get("/:id", authenticateToken, async (req, res) => {
|
|||||||
os_version: true,
|
os_version: true,
|
||||||
status: true,
|
status: true,
|
||||||
last_update: true,
|
last_update: true,
|
||||||
|
needs_reboot: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -259,6 +260,7 @@ router.get("/:id/hosts", authenticateToken, async (req, res) => {
|
|||||||
status: true,
|
status: true,
|
||||||
last_update: true,
|
last_update: true,
|
||||||
created_at: true,
|
created_at: true,
|
||||||
|
needs_reboot: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
friendly_name: "asc",
|
friendly_name: "asc",
|
||||||
|
|||||||
@@ -533,6 +533,14 @@ router.post(
|
|||||||
.optional()
|
.optional()
|
||||||
.isString()
|
.isString()
|
||||||
.withMessage("Machine ID must be a string"),
|
.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) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -596,6 +604,10 @@ router.post(
|
|||||||
updateData.system_uptime = req.body.systemUptime;
|
updateData.system_uptime = req.body.systemUptime;
|
||||||
if (req.body.loadAverage) updateData.load_average = req.body.loadAverage;
|
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 this is the first update (status is 'pending'), change to 'active'
|
||||||
if (host.status === "pending") {
|
if (host.status === "pending") {
|
||||||
updateData.status = "active";
|
updateData.status = "active";
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ router.get("/", async (req, res) => {
|
|||||||
friendly_name: true,
|
friendly_name: true,
|
||||||
hostname: true,
|
hostname: true,
|
||||||
os_type: true,
|
os_type: true,
|
||||||
|
needs_reboot: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
current_version: true,
|
current_version: true,
|
||||||
@@ -236,6 +237,7 @@ router.get("/:packageId", async (req, res) => {
|
|||||||
os_type: true,
|
os_type: true,
|
||||||
os_version: true,
|
os_version: true,
|
||||||
last_update: true,
|
last_update: true,
|
||||||
|
needs_reboot: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,6 +367,7 @@ router.get("/:packageId/hosts", async (req, res) => {
|
|||||||
os_type: true,
|
os_type: true,
|
||||||
os_version: true,
|
os_version: true,
|
||||||
last_update: true,
|
last_update: true,
|
||||||
|
needs_reboot: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -386,6 +389,7 @@ router.get("/:packageId/hosts", async (req, res) => {
|
|||||||
needsUpdate: hp.needs_update,
|
needsUpdate: hp.needs_update,
|
||||||
isSecurityUpdate: hp.is_security_update,
|
isSecurityUpdate: hp.is_security_update,
|
||||||
lastChecked: hp.last_checked,
|
lastChecked: hp.last_checked,
|
||||||
|
needsReboot: hp.hosts.needs_reboot,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ router.get(
|
|||||||
os_version: true,
|
os_version: true,
|
||||||
status: true,
|
status: true,
|
||||||
last_update: true,
|
last_update: true,
|
||||||
|
needs_reboot: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ server {
|
|||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" 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
|
# Bull Board proxy - must come before the root location to avoid conflicts
|
||||||
location /bullboard {
|
location /bullboard {
|
||||||
|
|||||||
@@ -4,6 +4,18 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- Prevent search engine indexing -->
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
|
||||||
|
<meta name="googlebot" content="noindex, nofollow, noarchive, nosnippet" />
|
||||||
|
<meta name="bingbot" content="noindex, nofollow, noarchive, nosnippet" />
|
||||||
|
<meta name="description" content="" />
|
||||||
|
|
||||||
|
<!-- Prevent caching -->
|
||||||
|
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="expires" content="0" />
|
||||||
|
|
||||||
<title>PatchMon - Linux Patch Monitoring Dashboard</title>
|
<title>PatchMon - Linux Patch Monitoring Dashboard</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
|||||||
26
frontend/public/robots.txt
Normal file
26
frontend/public/robots.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Disallow all search engine crawlers
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
# Specifically target major search engines
|
||||||
|
User-agent: Googlebot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Bingbot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Slurp
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: DuckDuckBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Baiduspider
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: YandexBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
# Prevent indexing of all content
|
||||||
|
Disallow: /*
|
||||||
|
|
||||||
@@ -11,6 +11,12 @@ const app = express();
|
|||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const BACKEND_URL = process.env.BACKEND_URL || "http://backend:3001";
|
const BACKEND_URL = process.env.BACKEND_URL || "http://backend:3001";
|
||||||
|
|
||||||
|
// Add security headers to prevent search engine indexing
|
||||||
|
app.use((_req, res, next) => {
|
||||||
|
res.setHeader("X-Robots-Tag", "noindex, nofollow, noarchive, nosnippet");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Enable CORS for API calls
|
// Enable CORS for API calls
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlertCircle, CheckCircle, Save, Shield } from "lucide-react";
|
import { AlertCircle, CheckCircle, Save, Shield, X } from "lucide-react";
|
||||||
import { useEffect, useId, useState } from "react";
|
import { useEffect, useId, useState } from "react";
|
||||||
import { permissionsAPI, settingsAPI } from "../../utils/api";
|
import { permissionsAPI, settingsAPI } from "../../utils/api";
|
||||||
|
|
||||||
@@ -18,9 +18,60 @@ const AgentUpdatesTab = () => {
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Auto-hide toast after 3 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (toast) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setToast(null);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
const showToast = (message, type = "success") => {
|
||||||
|
setToast({ message, type });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback clipboard copy function for HTTP and older browsers
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
// Try modern clipboard API first
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Clipboard API failed, using fallback:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for HTTP or unsupported browsers
|
||||||
|
try {
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = "fixed";
|
||||||
|
textArea.style.left = "-999999px";
|
||||||
|
textArea.style.top = "-999999px";
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
|
if (successful) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error("execCommand failed");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fallback copy failed:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch current settings
|
// Fetch current settings
|
||||||
const {
|
const {
|
||||||
data: settings,
|
data: settings,
|
||||||
@@ -167,6 +218,53 @@ const AgentUpdatesTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Toast Notification */}
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className={`fixed top-4 right-4 z-50 max-w-md rounded-lg shadow-lg border-2 p-4 flex items-start space-x-3 animate-in slide-in-from-top-5 ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "bg-green-50 dark:bg-green-900/90 border-green-500 dark:border-green-600"
|
||||||
|
: "bg-red-50 dark:bg-red-900/90 border-red-500 dark:border-red-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 rounded-full p-1 ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "bg-green-100 dark:bg-green-800"
|
||||||
|
: "bg-red-100 dark:bg-red-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{toast.type === "success" ? (
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p
|
||||||
|
className={`text-sm font-medium ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "text-green-800 dark:text-green-100"
|
||||||
|
: "text-red-800 dark:text-red-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setToast(null)}
|
||||||
|
className={`flex-shrink-0 rounded-lg p-1 transition-colors ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "hover:bg-green-100 dark:hover:bg-green-800 text-green-600 dark:text-green-400"
|
||||||
|
: "hover:bg-red-100 dark:hover:bg-red-800 text-red-600 dark:text-red-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{errors.general && (
|
{errors.general && (
|
||||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-md p-4">
|
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-md p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -458,19 +556,74 @@ const AgentUpdatesTab = () => {
|
|||||||
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
|
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||||
<p className="mb-3">To completely remove PatchMon from a host:</p>
|
<p className="mb-3">To completely remove PatchMon from a host:</p>
|
||||||
|
|
||||||
{/* Go Agent Uninstall */}
|
{/* Agent Removal Script - Standard */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-red-700 dark:text-red-300 mb-1">
|
||||||
|
Standard Removal (preserves backups):
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
||||||
sudo patchmon-agent uninstall
|
curl {formData.ignoreSslSelfSigned ? "-sk" : "-s"}{" "}
|
||||||
|
{window.location.origin}/api/v1/hosts/remove | sudo sh
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
navigator.clipboard.writeText(
|
try {
|
||||||
"sudo patchmon-agent uninstall",
|
const curlFlags = formData.ignoreSslSelfSigned
|
||||||
);
|
? "-sk"
|
||||||
|
: "-s";
|
||||||
|
await copyToClipboard(
|
||||||
|
`curl ${curlFlags} ${window.location.origin}/api/v1/hosts/remove | sudo sh`,
|
||||||
|
);
|
||||||
|
showToast(
|
||||||
|
"Standard removal command copied!",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
showToast("Failed to copy to clipboard", "error");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Removal Script - Complete */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-red-700 dark:text-red-300 mb-1">
|
||||||
|
Complete Removal (includes backups):
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
||||||
|
curl {formData.ignoreSslSelfSigned ? "-sk" : "-s"}{" "}
|
||||||
|
{window.location.origin}/api/v1/hosts/remove | sudo
|
||||||
|
REMOVE_BACKUPS=1 sh
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const curlFlags = formData.ignoreSslSelfSigned
|
||||||
|
? "-sk"
|
||||||
|
: "-s";
|
||||||
|
await copyToClipboard(
|
||||||
|
`curl ${curlFlags} ${window.location.origin}/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1 sh`,
|
||||||
|
);
|
||||||
|
showToast(
|
||||||
|
"Complete removal command copied!",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
showToast("Failed to copy to clipboard", "error");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
||||||
>
|
>
|
||||||
@@ -478,16 +631,15 @@ const AgentUpdatesTab = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-red-600 dark:text-red-400">
|
<div className="text-xs text-red-600 dark:text-red-400">
|
||||||
Options: <code>--remove-config</code>,{" "}
|
This removes: binaries, systemd/OpenRC services,
|
||||||
<code>--remove-logs</code>, <code>--remove-all</code>,{" "}
|
configuration files, logs, crontab entries, and backup files
|
||||||
<code>--force</code>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-2 text-xs">
|
<p className="mt-2 text-xs text-red-700 dark:text-red-400">
|
||||||
⚠️ This command will remove all PatchMon files, configuration,
|
⚠️ Standard removal preserves backup files for safety. Use
|
||||||
and crontab entries
|
complete removal to delete everything.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import {
|
|||||||
} from "chart.js";
|
} from "chart.js";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
Folder,
|
Folder,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Package,
|
Package,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
Server,
|
Server,
|
||||||
Settings,
|
Settings,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -99,6 +101,20 @@ const Dashboard = () => {
|
|||||||
navigate("/repositories");
|
navigate("/repositories");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNeedsRebootClick = () => {
|
||||||
|
// Navigate to hosts with reboot filter, clearing any other filters
|
||||||
|
const newSearchParams = new URLSearchParams();
|
||||||
|
newSearchParams.set("reboot", "true");
|
||||||
|
navigate(`/hosts?${newSearchParams.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpToDateClick = () => {
|
||||||
|
// Navigate to hosts with upToDate filter, clearing any other filters
|
||||||
|
const newSearchParams = new URLSearchParams();
|
||||||
|
newSearchParams.set("filter", "upToDate");
|
||||||
|
navigate(`/hosts?${newSearchParams.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
const _handleOSDistributionClick = () => {
|
const _handleOSDistributionClick = () => {
|
||||||
navigate("/hosts?showFilters=true", { replace: true });
|
navigate("/hosts?showFilters=true", { replace: true });
|
||||||
};
|
};
|
||||||
@@ -308,9 +324,10 @@ const Dashboard = () => {
|
|||||||
[
|
[
|
||||||
"totalHosts",
|
"totalHosts",
|
||||||
"hostsNeedingUpdates",
|
"hostsNeedingUpdates",
|
||||||
|
"upToDateHosts",
|
||||||
"totalOutdatedPackages",
|
"totalOutdatedPackages",
|
||||||
"securityUpdates",
|
"securityUpdates",
|
||||||
"upToDateHosts",
|
"hostsNeedingReboot",
|
||||||
"totalHostGroups",
|
"totalHostGroups",
|
||||||
"totalUsers",
|
"totalUsers",
|
||||||
"totalRepos",
|
"totalRepos",
|
||||||
@@ -341,7 +358,7 @@ const Dashboard = () => {
|
|||||||
const getGroupClassName = (cardType) => {
|
const getGroupClassName = (cardType) => {
|
||||||
switch (cardType) {
|
switch (cardType) {
|
||||||
case "stats":
|
case "stats":
|
||||||
return "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4";
|
return "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4";
|
||||||
case "charts":
|
case "charts":
|
||||||
return "grid grid-cols-1 lg:grid-cols-3 gap-6";
|
return "grid grid-cols-1 lg:grid-cols-3 gap-6";
|
||||||
case "widecharts":
|
case "widecharts":
|
||||||
@@ -356,23 +373,33 @@ const Dashboard = () => {
|
|||||||
// Helper function to render a card by ID
|
// Helper function to render a card by ID
|
||||||
const renderCard = (cardId) => {
|
const renderCard = (cardId) => {
|
||||||
switch (cardId) {
|
switch (cardId) {
|
||||||
case "upToDateHosts":
|
case "hostsNeedingReboot":
|
||||||
return (
|
return (
|
||||||
<div className="card p-4">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 w-full text-left"
|
||||||
|
onClick={handleNeedsRebootClick}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNeedsRebootClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<TrendingUp className="h-5 w-5 text-success-600 mr-2" />
|
<RotateCcw className="h-5 w-5 text-orange-600 mr-2" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-0 flex-1">
|
<div className="w-0 flex-1">
|
||||||
<p className="text-sm text-secondary-500 dark:text-white">
|
<p className="text-sm text-secondary-500 dark:text-white">
|
||||||
Up to date
|
Needs Reboots
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||||
{stats.cards.upToDateHosts}/{stats.cards.totalHosts}
|
{stats.cards.hostsNeedingReboot}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
case "totalHosts":
|
case "totalHosts":
|
||||||
return (
|
return (
|
||||||
@@ -432,6 +459,35 @@ const Dashboard = () => {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "upToDateHosts":
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 w-full text-left"
|
||||||
|
onClick={handleUpToDateClick}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleUpToDateClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<CheckCircle className="h-5 w-5 text-success-600 mr-2" />
|
||||||
|
</div>
|
||||||
|
<div className="w-0 flex-1">
|
||||||
|
<p className="text-sm text-secondary-500 dark:text-white">
|
||||||
|
Up to date
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||||
|
{stats.cards.upToDateHosts}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
case "totalOutdatedPackages":
|
case "totalOutdatedPackages":
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Monitor,
|
Monitor,
|
||||||
Package,
|
Package,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
Server,
|
Server,
|
||||||
Shield,
|
Shield,
|
||||||
Terminal,
|
Terminal,
|
||||||
@@ -493,6 +494,12 @@ const HostDetail = () => {
|
|||||||
{getStatusIcon(isStale, host.stats.outdated_packages > 0)}
|
{getStatusIcon(isStale, host.stats.outdated_packages > 0)}
|
||||||
{getStatusText(isStale, host.stats.outdated_packages > 0)}
|
{getStatusText(isStale, host.stats.outdated_packages > 0)}
|
||||||
</div>
|
</div>
|
||||||
|
{host.needs_reboot && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||||||
|
<RotateCcw className="h-3 w-3" />
|
||||||
|
Reboot Required
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Info row with uptime and last updated */}
|
{/* Info row with uptime and last updated */}
|
||||||
<div className="flex items-center gap-4 text-sm text-secondary-600 dark:text-secondary-400">
|
<div className="flex items-center gap-4 text-sm text-secondary-600 dark:text-secondary-400">
|
||||||
@@ -994,7 +1001,7 @@ const HostDetail = () => {
|
|||||||
<Terminal className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
<Terminal className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||||
System Information
|
System Information
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{host.architecture && (
|
{host.architecture && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
||||||
@@ -1006,20 +1013,32 @@ const HostDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{host.kernel_version && (
|
{(host.kernel_version ||
|
||||||
<div>
|
host.installed_kernel_version) && (
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
<div className="flex flex-col gap-2">
|
||||||
Kernel Version
|
{host.kernel_version && (
|
||||||
</p>
|
<div>
|
||||||
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1">
|
||||||
{host.kernel_version}
|
Running Kernel
|
||||||
</p>
|
</p>
|
||||||
|
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm break-all">
|
||||||
|
{host.kernel_version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{host.installed_kernel_version && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1">
|
||||||
|
Installed Kernel
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm break-all">
|
||||||
|
{host.installed_kernel_version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty div to push SELinux status to the right */}
|
|
||||||
<div></div>
|
|
||||||
|
|
||||||
{host.selinux_status && (
|
{host.selinux_status && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
<p className="text-xs text-secondary-500 dark:text-secondary-300">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
GripVertical,
|
GripVertical,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
Search,
|
Search,
|
||||||
Server,
|
Server,
|
||||||
Square,
|
Square,
|
||||||
@@ -247,6 +248,7 @@ const Hosts = () => {
|
|||||||
const showFiltersParam = searchParams.get("showFilters");
|
const showFiltersParam = searchParams.get("showFilters");
|
||||||
const osFilterParam = searchParams.get("osFilter");
|
const osFilterParam = searchParams.get("osFilter");
|
||||||
const groupParam = searchParams.get("group");
|
const groupParam = searchParams.get("group");
|
||||||
|
const rebootParam = searchParams.get("reboot");
|
||||||
|
|
||||||
if (filter === "needsUpdates") {
|
if (filter === "needsUpdates") {
|
||||||
setShowFilters(true);
|
setShowFilters(true);
|
||||||
@@ -331,10 +333,11 @@ const Hosts = () => {
|
|||||||
},
|
},
|
||||||
{ id: "ws_status", label: "Connection", visible: true, order: 9 },
|
{ id: "ws_status", label: "Connection", visible: true, order: 9 },
|
||||||
{ id: "status", label: "Status", visible: true, order: 10 },
|
{ id: "status", label: "Status", visible: true, order: 10 },
|
||||||
{ id: "updates", label: "Updates", visible: true, order: 11 },
|
{ id: "needs_reboot", label: "Reboot", visible: true, order: 11 },
|
||||||
{ id: "notes", label: "Notes", visible: false, order: 12 },
|
{ id: "updates", label: "Updates", visible: true, order: 12 },
|
||||||
{ id: "last_update", label: "Last Update", visible: true, order: 13 },
|
{ id: "notes", label: "Notes", visible: false, order: 13 },
|
||||||
{ id: "actions", label: "Actions", visible: true, order: 14 },
|
{ 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");
|
const saved = localStorage.getItem("hosts-column-config");
|
||||||
@@ -356,8 +359,25 @@ const Hosts = () => {
|
|||||||
localStorage.removeItem("hosts-column-config");
|
localStorage.removeItem("hosts-column-config");
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
} else {
|
} else {
|
||||||
// Ensure ws_status column is visible in saved config
|
// Merge saved config with defaults to handle new columns
|
||||||
const updatedConfig = savedConfig.map((col) =>
|
// 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,
|
col.id === "ws_status" ? { ...col, visible: true } : col,
|
||||||
);
|
);
|
||||||
return updatedConfig;
|
return updatedConfig;
|
||||||
@@ -673,8 +693,9 @@ const Hosts = () => {
|
|||||||
osFilter === "all" ||
|
osFilter === "all" ||
|
||||||
host.os_type?.toLowerCase() === osFilter.toLowerCase();
|
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 filter = searchParams.get("filter");
|
||||||
|
const rebootParam = searchParams.get("reboot");
|
||||||
const matchesUrlFilter =
|
const matchesUrlFilter =
|
||||||
(filter !== "needsUpdates" ||
|
(filter !== "needsUpdates" ||
|
||||||
(host.updatesCount && host.updatesCount > 0)) &&
|
(host.updatesCount && host.updatesCount > 0)) &&
|
||||||
@@ -682,7 +703,9 @@ const Hosts = () => {
|
|||||||
(host.effectiveStatus || host.status) === "inactive") &&
|
(host.effectiveStatus || host.status) === "inactive") &&
|
||||||
(filter !== "upToDate" || (!host.isStale && host.updatesCount === 0)) &&
|
(filter !== "upToDate" || (!host.isStale && host.updatesCount === 0)) &&
|
||||||
(filter !== "stale" || host.isStale) &&
|
(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
|
// Hide stale filter
|
||||||
const matchesHideStale = !hideStale || !host.isStale;
|
const matchesHideStale = !hideStale || !host.isStale;
|
||||||
@@ -758,6 +781,11 @@ const Hosts = () => {
|
|||||||
aValue = a.updatesCount || 0;
|
aValue = a.updatesCount || 0;
|
||||||
bValue = b.updatesCount || 0;
|
bValue = b.updatesCount || 0;
|
||||||
break;
|
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":
|
case "last_update":
|
||||||
aValue = new Date(a.last_update);
|
aValue = new Date(a.last_update);
|
||||||
bValue = new Date(b.last_update);
|
bValue = new Date(b.last_update);
|
||||||
@@ -917,10 +945,11 @@ const Hosts = () => {
|
|||||||
},
|
},
|
||||||
{ id: "ws_status", label: "Connection", visible: true, order: 9 },
|
{ id: "ws_status", label: "Connection", visible: true, order: 9 },
|
||||||
{ id: "status", label: "Status", visible: true, order: 10 },
|
{ id: "status", label: "Status", visible: true, order: 10 },
|
||||||
{ id: "updates", label: "Updates", visible: true, order: 11 },
|
{ id: "needs_reboot", label: "Reboot", visible: true, order: 11 },
|
||||||
{ id: "notes", label: "Notes", visible: false, order: 12 },
|
{ id: "updates", label: "Updates", visible: true, order: 12 },
|
||||||
{ id: "last_update", label: "Last Update", visible: true, order: 13 },
|
{ id: "notes", label: "Notes", visible: false, order: 13 },
|
||||||
{ id: "actions", label: "Actions", visible: true, order: 14 },
|
{ id: "last_update", label: "Last Update", visible: true, order: 14 },
|
||||||
|
{ id: "actions", label: "Actions", visible: true, order: 15 },
|
||||||
];
|
];
|
||||||
updateColumnConfig(defaultConfig);
|
updateColumnConfig(defaultConfig);
|
||||||
};
|
};
|
||||||
@@ -1077,6 +1106,22 @@ const Hosts = () => {
|
|||||||
(host.effectiveStatus || host.status).slice(1)}
|
(host.effectiveStatus || host.status).slice(1)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case "needs_reboot":
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{host.needs_reboot ? (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||||||
|
<RotateCcw className="h-3 w-3" />
|
||||||
|
Required
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||||
|
<CheckCircle className="h-3 w-3" />
|
||||||
|
No
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case "updates":
|
case "updates":
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -1149,9 +1194,10 @@ const Hosts = () => {
|
|||||||
// Filter to show only up-to-date hosts
|
// Filter to show only up-to-date hosts
|
||||||
setStatusFilter("active");
|
setStatusFilter("active");
|
||||||
setShowFilters(true);
|
setShowFilters(true);
|
||||||
// Use the upToDate URL filter
|
// Clear conflicting filters and set upToDate filter
|
||||||
const newSearchParams = new URLSearchParams(window.location.search);
|
const newSearchParams = new URLSearchParams(window.location.search);
|
||||||
newSearchParams.set("filter", "upToDate");
|
newSearchParams.set("filter", "upToDate");
|
||||||
|
newSearchParams.delete("reboot"); // Clear reboot filter when switching to upToDate
|
||||||
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1159,9 +1205,10 @@ const Hosts = () => {
|
|||||||
// Filter to show hosts needing updates (regardless of status)
|
// Filter to show hosts needing updates (regardless of status)
|
||||||
setStatusFilter("all");
|
setStatusFilter("all");
|
||||||
setShowFilters(true);
|
setShowFilters(true);
|
||||||
// We'll use the existing needsUpdates URL filter logic
|
// Clear conflicting filters and set needsUpdates filter
|
||||||
const newSearchParams = new URLSearchParams(window.location.search);
|
const newSearchParams = new URLSearchParams(window.location.search);
|
||||||
newSearchParams.set("filter", "needsUpdates");
|
newSearchParams.set("filter", "needsUpdates");
|
||||||
|
newSearchParams.delete("reboot"); // Clear reboot filter when switching to needsUpdates
|
||||||
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1169,9 +1216,10 @@ const Hosts = () => {
|
|||||||
// Filter to show offline hosts (not connected via WebSocket)
|
// Filter to show offline hosts (not connected via WebSocket)
|
||||||
setStatusFilter("all");
|
setStatusFilter("all");
|
||||||
setShowFilters(true);
|
setShowFilters(true);
|
||||||
// Use a new URL filter for connection status
|
// Clear conflicting filters and set offline filter
|
||||||
const newSearchParams = new URLSearchParams(window.location.search);
|
const newSearchParams = new URLSearchParams(window.location.search);
|
||||||
newSearchParams.set("filter", "offline");
|
newSearchParams.set("filter", "offline");
|
||||||
|
newSearchParams.delete("reboot"); // Clear reboot filter when switching to offline
|
||||||
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1262,24 +1310,6 @@ const Hosts = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
|
||||||
onClick={handleUpToDateClick}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle className="h-5 w-5 text-success-600 mr-2" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-secondary-500 dark:text-white">
|
|
||||||
Up to Date
|
|
||||||
</p>
|
|
||||||
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
|
||||||
{hosts?.filter((h) => !h.isStale && h.updatesCount === 0)
|
|
||||||
.length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
||||||
@@ -1297,6 +1327,28 @@ const Hosts = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
||||||
|
onClick={() => {
|
||||||
|
const newSearchParams = new URLSearchParams();
|
||||||
|
newSearchParams.set("reboot", "true");
|
||||||
|
// Clear filter parameter when setting reboot filter
|
||||||
|
navigate(`/hosts?${newSearchParams.toString()}`, { replace: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RotateCcw className="h-5 w-5 text-orange-600 mr-2" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-secondary-500 dark:text-white">
|
||||||
|
Needs Reboots
|
||||||
|
</p>
|
||||||
|
<p className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||||
|
{hosts?.filter((h) => h.needs_reboot === true).length || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200 text-left w-full"
|
||||||
@@ -1679,6 +1731,17 @@ const Hosts = () => {
|
|||||||
{column.label}
|
{column.label}
|
||||||
{getSortIcon("updates")}
|
{getSortIcon("updates")}
|
||||||
</button>
|
</button>
|
||||||
|
) : column.id === "needs_reboot" ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
handleSort("needs_reboot")
|
||||||
|
}
|
||||||
|
className="flex items-center gap-2 hover:text-secondary-700"
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
{getSortIcon("needs_reboot")}
|
||||||
|
</button>
|
||||||
) : column.id === "last_update" ? (
|
) : column.id === "last_update" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Package,
|
Package,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
Search,
|
Search,
|
||||||
Server,
|
Server,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -370,6 +371,9 @@ const PackageDetail = () => {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
||||||
Last Updated
|
Last Updated
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
||||||
|
Reboot Required
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
|
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
|
||||||
@@ -420,6 +424,18 @@ const PackageDetail = () => {
|
|||||||
? formatRelativeTime(host.lastUpdate)
|
? formatRelativeTime(host.lastUpdate)
|
||||||
: "Never"}
|
: "Never"}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
{host.needsReboot ? (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||||||
|
<RotateCcw className="h-3 w-3" />
|
||||||
|
Required
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-secondary-500 dark:text-secondary-300">
|
||||||
|
No
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
Globe,
|
Globe,
|
||||||
Lock,
|
Lock,
|
||||||
|
RotateCcw,
|
||||||
Search,
|
Search,
|
||||||
Server,
|
Server,
|
||||||
Shield,
|
Shield,
|
||||||
@@ -556,6 +557,9 @@ const RepositoryDetail = () => {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
||||||
Last Update
|
Last Update
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
|
||||||
|
Reboot Required
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
|
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
|
||||||
@@ -604,6 +608,18 @@ const RepositoryDetail = () => {
|
|||||||
? formatRelativeTime(hostRepo.hosts.last_update)
|
? formatRelativeTime(hostRepo.hosts.last_update)
|
||||||
: "Never"}
|
: "Never"}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
{hostRepo.hosts.needs_reboot ? (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
|
||||||
|
<RotateCcw className="h-3 w-3" />
|
||||||
|
Required
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-secondary-500 dark:text-secondary-300">
|
||||||
|
No
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const Settings = () => {
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
|
||||||
// Tab management
|
// Tab management
|
||||||
const [activeTab, setActiveTab] = useState("server");
|
const [activeTab, setActiveTab] = useState("server");
|
||||||
@@ -60,6 +61,56 @@ const Settings = () => {
|
|||||||
// Get update notification state
|
// Get update notification state
|
||||||
const { updateAvailable } = useUpdateNotification();
|
const { updateAvailable } = useUpdateNotification();
|
||||||
|
|
||||||
|
// Auto-hide toast after 3 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (toast) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setToast(null);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
|
const showToast = (message, type = "success") => {
|
||||||
|
setToast({ message, type });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback clipboard copy function for HTTP and older browsers
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
// Try modern clipboard API first
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Clipboard API failed, using fallback:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for HTTP or unsupported browsers
|
||||||
|
try {
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = "fixed";
|
||||||
|
textArea.style.left = "-999999px";
|
||||||
|
textArea.style.top = "-999999px";
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
const successful = document.execCommand("copy");
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
|
if (successful) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new Error("execCommand failed");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fallback copy failed:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Tab configuration
|
// Tab configuration
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "server", name: "Server Configuration", icon: Server },
|
{ id: "server", name: "Server Configuration", icon: Server },
|
||||||
@@ -120,7 +171,7 @@ const Settings = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to get curl flags based on settings
|
// Helper function to get curl flags based on settings
|
||||||
const _getCurlFlags = () => {
|
const getCurlFlags = () => {
|
||||||
return settings?.ignore_ssl_self_signed ? "-sk" : "-s";
|
return settings?.ignore_ssl_self_signed ? "-sk" : "-s";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -442,6 +493,53 @@ const Settings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
|
{/* Toast Notification */}
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className={`fixed top-4 right-4 z-50 max-w-md rounded-lg shadow-lg border-2 p-4 flex items-start space-x-3 animate-in slide-in-from-top-5 ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "bg-green-50 dark:bg-green-900/90 border-green-500 dark:border-green-600"
|
||||||
|
: "bg-red-50 dark:bg-red-900/90 border-red-500 dark:border-red-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 rounded-full p-1 ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "bg-green-100 dark:bg-green-800"
|
||||||
|
: "bg-red-100 dark:bg-red-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{toast.type === "success" ? (
|
||||||
|
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p
|
||||||
|
className={`text-sm font-medium ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "text-green-800 dark:text-green-100"
|
||||||
|
: "text-red-800 dark:text-red-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setToast(null)}
|
||||||
|
className={`flex-shrink-0 rounded-lg p-1 transition-colors ${
|
||||||
|
toast.type === "success"
|
||||||
|
? "hover:bg-green-100 dark:hover:bg-green-800 text-green-600 dark:text-green-400"
|
||||||
|
: "hover:bg-red-100 dark:hover:bg-red-800 text-red-600 dark:text-red-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<p className="text-secondary-600 dark:text-secondary-300">
|
<p className="text-secondary-600 dark:text-secondary-300">
|
||||||
Configure your PatchMon server settings. These settings will be used
|
Configure your PatchMon server settings. These settings will be used
|
||||||
@@ -1159,19 +1257,74 @@ const Settings = () => {
|
|||||||
To completely remove PatchMon from a host:
|
To completely remove PatchMon from a host:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Go Agent Uninstall */}
|
{/* Agent Removal Script - Standard */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-red-700 dark:text-red-300 mb-1">
|
||||||
|
Standard Removal (preserves backups):
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
||||||
sudo patchmon-agent uninstall
|
curl {getCurlFlags()} {window.location.origin}
|
||||||
|
/api/v1/hosts/remove | sudo sh
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
navigator.clipboard.writeText(
|
try {
|
||||||
"sudo patchmon-agent uninstall",
|
await copyToClipboard(
|
||||||
);
|
`curl ${getCurlFlags()} ${window.location.origin}/api/v1/hosts/remove | sudo sh`,
|
||||||
|
);
|
||||||
|
showToast(
|
||||||
|
"Standard removal command copied!",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
showToast(
|
||||||
|
"Failed to copy to clipboard",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Removal Script - Complete */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-semibold text-red-700 dark:text-red-300 mb-1">
|
||||||
|
Complete Removal (includes backups):
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
||||||
|
curl {getCurlFlags()} {window.location.origin}
|
||||||
|
/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1
|
||||||
|
sh
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await copyToClipboard(
|
||||||
|
`curl ${getCurlFlags()} ${window.location.origin}/api/v1/hosts/remove | sudo REMOVE_BACKUPS=1 sh`,
|
||||||
|
);
|
||||||
|
showToast(
|
||||||
|
"Complete removal command copied!",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
showToast(
|
||||||
|
"Failed to copy to clipboard",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
||||||
>
|
>
|
||||||
@@ -1179,16 +1332,16 @@ const Settings = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-red-600 dark:text-red-400">
|
<div className="text-xs text-red-600 dark:text-red-400">
|
||||||
Options: <code>--remove-config</code>,{" "}
|
This removes: binaries, systemd/OpenRC services,
|
||||||
<code>--remove-logs</code>,{" "}
|
configuration files, logs, crontab entries, and
|
||||||
<code>--remove-all</code>, <code>--force</code>
|
backup files
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-2 text-xs">
|
<p className="mt-2 text-xs text-red-700 dark:text-red-400">
|
||||||
⚠️ This command will remove all PatchMon files,
|
⚠️ Standard removal preserves backup files for
|
||||||
configuration, and crontab entries
|
safety. Use complete removal to delete everything.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user