import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, CheckCircle, Save, Shield } from "lucide-react"; import { useEffect, useId, useState } from "react"; import { permissionsAPI, settingsAPI } from "../../utils/api"; const AgentUpdatesTab = () => { const updateIntervalId = useId(); const autoUpdateId = useId(); const signupEnabledId = useId(); const defaultRoleId = useId(); const ignoreSslId = useId(); const [formData, setFormData] = useState({ updateInterval: 60, autoUpdate: false, signupEnabled: false, defaultUserRole: "user", ignoreSslSelfSigned: false, }); const [errors, setErrors] = useState({}); const [isDirty, setIsDirty] = useState(false); const queryClient = useQueryClient(); // Fetch current settings const { data: settings, isLoading, error, } = useQuery({ queryKey: ["settings"], queryFn: () => settingsAPI.get().then((res) => res.data), }); // Fetch available roles for default user role dropdown const { data: roles, isLoading: rolesLoading } = useQuery({ queryKey: ["rolePermissions"], queryFn: () => permissionsAPI.getRoles().then((res) => res.data), }); // Update form data when settings are loaded useEffect(() => { if (settings) { const newFormData = { updateInterval: settings.update_interval || 60, autoUpdate: settings.auto_update || false, signupEnabled: settings.signup_enabled === true, defaultUserRole: settings.default_user_role || "user", ignoreSslSelfSigned: settings.ignore_ssl_self_signed === true, }; setFormData(newFormData); setIsDirty(false); } }, [settings]); // Update settings mutation const updateSettingsMutation = useMutation({ mutationFn: (data) => { return settingsAPI.update(data).then((res) => res.data); }, onSuccess: () => { queryClient.invalidateQueries(["settings"]); setIsDirty(false); setErrors({}); }, onError: (error) => { if (error.response?.data?.errors) { setErrors( error.response.data.errors.reduce((acc, err) => { acc[err.path] = err.msg; return acc; }, {}), ); } else { setErrors({ general: error.response?.data?.error || "Failed to update settings", }); } }, }); // Normalize update interval to safe presets const normalizeInterval = (minutes) => { let m = parseInt(minutes, 10); if (Number.isNaN(m)) return 60; if (m < 5) m = 5; if (m > 1440) m = 1440; // If less than 60 minutes, keep within 5-59 and step of 5 if (m < 60) { return Math.min(59, Math.max(5, Math.round(m / 5) * 5)); } // 60 or more: only allow exact hour multiples (60, 120, 180, 360, 720, 1440) const allowed = [60, 120, 180, 360, 720, 1440]; // Snap to nearest allowed value let nearest = allowed[0]; let bestDiff = Math.abs(m - nearest); for (const a of allowed) { const d = Math.abs(m - a); if (d < bestDiff) { bestDiff = d; nearest = a; } } return nearest; }; const handleInputChange = (field, value) => { setFormData((prev) => { const newData = { ...prev, [field]: field === "updateInterval" ? normalizeInterval(value) : value, }; return newData; }); setIsDirty(true); if (errors[field]) { setErrors((prev) => ({ ...prev, [field]: null })); } }; const validateForm = () => { const newErrors = {}; if ( !formData.updateInterval || formData.updateInterval < 5 || formData.updateInterval > 1440 ) { newErrors.updateInterval = "Update interval must be between 5 and 1440 minutes"; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSave = () => { if (validateForm()) { updateSettingsMutation.mutate(formData); } }; if (isLoading) { return (
); } if (error) { return (

Error loading settings

{error.response?.data?.error || "Failed to load settings"}

); } return (
{errors.general && (

{errors.general}

)}
{/* Update Interval */}
{/* Numeric input (concise width) */}
{ const val = parseInt(e.target.value, 10); if (!Number.isNaN(val)) { handleInputChange( "updateInterval", Math.min(1440, Math.max(5, val)), ); } else { handleInputChange("updateInterval", 60); } }} className={`w-28 border rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white ${ errors.updateInterval ? "border-red-300 dark:border-red-500" : "border-secondary-300 dark:border-secondary-600" }`} placeholder="60" />
{/* Quick presets */}
{[5, 10, 15, 30, 45, 60, 120, 180, 360, 720, 1440].map((m) => ( ))}
{/* Range slider */}
{ const raw = parseInt(e.target.value, 10); handleInputChange("updateInterval", normalizeInterval(raw)); }} className="w-auto accent-primary-600" aria-label="Update interval slider" style={{ width: "fit-content", minWidth: "500px" }} />
{errors.updateInterval && (

{errors.updateInterval}

)} {/* Helper text */}
Effective cadence: {(() => { const mins = parseInt(formData.updateInterval, 10) || 60; if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"}`; const hrs = Math.floor(mins / 60); const rem = mins % 60; return `${hrs} hour${hrs === 1 ? "" : "s"}${rem ? ` ${rem} min` : ""}`; })()}

This affects new installations and will update existing ones when they next reach out.

{/* Auto-Update Setting */}

When enabled, agents will automatically update themselves when a newer version is available during their regular update cycle.

{/* SSL Certificate Setting */}

When enabled, curl commands in agent scripts will use the -k flag to ignore SSL certificate validation errors. Use with caution on production systems as this reduces security.

{/* User Signup Setting */}
{/* Default User Role Dropdown */} {formData.signupEnabled && (

New users will be assigned this role when they register.

)}

When enabled, users can create their own accounts through the signup page. When disabled, only administrators can create user accounts.

{/* Security Notice */}

Security Notice

When enabling user self-registration, exercise caution on internal networks. Consider restricting access to trusted networks only and ensure proper role assignments to prevent unauthorized access to sensitive systems.

{/* Save Button */}
{updateSettingsMutation.isSuccess && (

Settings saved successfully!

)}
{/* Uninstall Instructions */}

Agent Uninstall Command

To completely remove PatchMon from a host:

{/* Go Agent Uninstall */}
sudo patchmon-agent uninstall
Options: --remove-config,{" "} --remove-logs, --remove-all,{" "} --force

⚠️ This command will remove all PatchMon files, configuration, and crontab entries

); }; export default AgentUpdatesTab;