import { AlertCircle, BookOpen, CheckCircle, Container, Copy, Eye, EyeOff, Plus, Server, Trash2, X, } from "lucide-react"; import { useEffect, useId, useState } from "react"; import SettingsLayout from "../../components/SettingsLayout"; import api from "../../utils/api"; const Integrations = () => { // Generate unique IDs for form elements const token_name_id = useId(); const token_key_id = useId(); const token_secret_id = useId(); const token_base64_id = useId(); const gethomepage_config_id = useId(); const [activeTab, setActiveTab] = useState("proxmox"); const [tokens, setTokens] = useState([]); const [host_groups, setHostGroups] = useState([]); const [loading, setLoading] = useState(true); const [show_create_modal, setShowCreateModal] = useState(false); const [new_token, setNewToken] = useState(null); const [show_secret, setShowSecret] = useState(false); const [server_url, setServerUrl] = useState(""); const [force_proxmox_install, setForceProxmoxInstall] = useState(false); // Form state const [form_data, setFormData] = useState({ token_name: "", max_hosts_per_day: 100, default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", }); const [copy_success, setCopySuccess] = useState({}); // Helper function to build Proxmox enrollment URL with optional force flag const getProxmoxUrl = () => { const baseUrl = `${server_url}/api/v1/auto-enrollment/proxmox-lxc?token_key=${new_token.token_key}&token_secret=${new_token.token_secret}`; return force_proxmox_install ? `${baseUrl}&force=true` : baseUrl; }; const handleTabChange = (tabName) => { setActiveTab(tabName); }; // biome-ignore lint/correctness/useExhaustiveDependencies: Only run on mount useEffect(() => { load_tokens(); load_host_groups(); load_server_url(); }, []); const load_tokens = async () => { try { setLoading(true); const response = await api.get("/auto-enrollment/tokens"); setTokens(response.data); } catch (error) { console.error("Failed to load tokens:", error); } finally { setLoading(false); } }; const load_host_groups = async () => { try { const response = await api.get("/host-groups"); setHostGroups(response.data); } catch (error) { console.error("Failed to load host groups:", error); } }; const load_server_url = async () => { try { const response = await api.get("/settings"); setServerUrl(response.data.server_url || window.location.origin); } catch (error) { console.error("Failed to load server URL:", error); setServerUrl(window.location.origin); } }; const create_token = async (e) => { e.preventDefault(); try { const data = { token_name: form_data.token_name, max_hosts_per_day: Number.parseInt(form_data.max_hosts_per_day, 10), allowed_ip_ranges: form_data.allowed_ip_ranges ? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim()) : [], metadata: { integration_type: activeTab === "gethomepage" ? "gethomepage" : "proxmox-lxc", }, }; // Only add optional fields if they have values if (form_data.default_host_group_id) { data.default_host_group_id = form_data.default_host_group_id; } if (form_data.expires_at) { data.expires_at = form_data.expires_at; } const response = await api.post("/auto-enrollment/tokens", data); setNewToken(response.data.token); setShowCreateModal(false); load_tokens(); // Reset form setFormData({ token_name: "", max_hosts_per_day: 100, default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", }); } catch (error) { console.error("Failed to create token:", error); const error_message = error.response?.data?.errors ? error.response.data.errors.map((e) => e.msg).join(", ") : error.response?.data?.error || "Failed to create token"; alert(error_message); } }; const delete_token = async (id, name) => { if ( !confirm( `Are you sure you want to delete the token "${name}"? This action cannot be undone.`, ) ) { return; } try { await api.delete(`/auto-enrollment/tokens/${id}`); load_tokens(); } catch (error) { console.error("Failed to delete token:", error); alert(error.response?.data?.error || "Failed to delete token"); } }; const toggle_token_active = async (id, current_status) => { try { await api.patch(`/auto-enrollment/tokens/${id}`, { is_active: !current_status, }); load_tokens(); } catch (error) { console.error("Failed to toggle token:", error); alert(error.response?.data?.error || "Failed to toggle token"); } }; const copy_to_clipboard = async (text, key) => { // Check if Clipboard API is available if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); setCopySuccess({ ...copy_success, [key]: true }); setTimeout(() => { setCopySuccess({ ...copy_success, [key]: false }); }, 2000); return; } catch (error) { console.error("Clipboard API failed:", error); // Fall through to fallback method } } // Fallback method for older browsers or non-secure contexts 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) { setCopySuccess({ ...copy_success, [key]: true }); setTimeout(() => { setCopySuccess({ ...copy_success, [key]: false }); }, 2000); } else { console.error("Fallback copy failed"); alert("Failed to copy to clipboard. Please copy manually."); } } catch (fallbackError) { console.error("Fallback copy failed:", fallbackError); alert("Failed to copy to clipboard. Please copy manually."); } }; const format_date = (date_string) => { if (!date_string) return "Never"; return new Date(date_string).toLocaleString(); }; return (
{/* Header */}

Integrations

Manage auto-enrollment tokens for Proxmox and other integrations

{/* Tabs Navigation */}
{/* Future tabs can be added here */}
{/* Tab Content */}
{/* Proxmox Tab */} {activeTab === "proxmox" && (
{/* Header with New Token Button */}

Proxmox LXC Auto-Enrollment

Automatically discover and enroll LXC containers from Proxmox hosts

{/* Token List */} {loading ? (
) : tokens.length === 0 ? (

No auto-enrollment tokens created yet.

Create a token to enable automatic host enrollment from Proxmox.

) : (
{tokens.map((token) => (

{token.token_name}

Proxmox LXC {token.is_active ? ( Active ) : ( Inactive )}
{token.token_key}

Usage: {token.hosts_created_today}/ {token.max_hosts_per_day} hosts today

{token.host_groups && (

Default Group:{" "} {token.host_groups.name}

)} {token.allowed_ip_ranges?.length > 0 && (

Allowed IPs:{" "} {token.allowed_ip_ranges.join(", ")}

)}

Created: {format_date(token.created_at)}

{token.last_used_at && (

Last Used: {format_date(token.last_used_at)}

)} {token.expires_at && (

Expires: {format_date(token.expires_at)} {new Date(token.expires_at) < new Date() && ( (Expired) )}

)}
))}
)} {/* Documentation Section */}

How to Use Auto-Enrollment

Documentation
  1. Create a new auto-enrollment token using the button above
  2. Copy the one-line installation command shown in the success dialog
  3. SSH into your Proxmox host as root
  4. Paste and run the command - it will automatically discover and enroll all running LXC containers
  5. View enrolled containers in the Hosts page

💡 Tip: You can run the same command multiple times safely - already enrolled containers will be automatically skipped.

)} {/* GetHomepage Tab */} {activeTab === "gethomepage" && (
{/* Header with New API Key Button */}

GetHomepage Widget Integration

Create API keys to display PatchMon statistics in your GetHomepage dashboard

{/* API Keys List */} {loading ? (
) : tokens.filter( (token) => token.metadata?.integration_type === "gethomepage", ).length === 0 ? (

No GetHomepage API keys created yet.

Create an API key to enable GetHomepage widget integration.

) : (
{tokens .filter( (token) => token.metadata?.integration_type === "gethomepage", ) .map((token) => (

{token.token_name}

GetHomepage {token.is_active ? ( Active ) : ( Inactive )}
{token.token_key}

Created: {format_date(token.created_at)}

{token.last_used_at && (

Last Used: {format_date(token.last_used_at)}

)} {token.expires_at && (

Expires: {format_date(token.expires_at)} {new Date(token.expires_at) < new Date() && ( (Expired) )}

)}
))}
)} {/* Documentation Section */}

How to Use GetHomepage Integration

Documentation
  1. Create a new API key using the button above
  2. Copy the API key and secret from the success dialog
  3. Add the following widget configuration to your GetHomepage{" "} services.yml {" "} file:
											{`- PatchMon:
    href: ${server_url}
    description: PatchMon Statistics
    icon: ${server_url}/assets/favicon.svg
    widget:
      type: customapi
      url: ${server_url}/api/v1/gethomepage/stats
      headers:
        Authorization: Basic BASE64_ENCODED_CREDENTIALS
      mappings:
        - field: total_hosts
          label: Total Hosts
        - field: hosts_needing_updates
          label: Needs Updates
        - field: security_updates
          label: Security Updates`}
										

How to generate BASE64_ENCODED_CREDENTIALS:

											{`echo -n "YOUR_API_KEY:YOUR_API_SECRET" | base64`}
										

Replace YOUR_API_KEY and YOUR_API_SECRET with your actual credentials, then run this command to get the base64 string.

Additional Widget Examples

You can create multiple widgets to display different statistics:

Security Updates Widget:
type: customapi
key: security_updates
value: hosts_with_security_updates
label: Security Updates
Up-to-Date Hosts Widget:
type: customapi
key: up_to_date_hosts
value: total_hosts
label: Up-to-Date Hosts
Recent Activity Widget:
type: customapi
key: recent_updates_24h
value: total_hosts
label: Updates (24h)
)} {/* Docker Tab */} {activeTab === "docker" && (
{/* Header */}

Docker Container Monitoring

Monitor Docker containers and images for available updates

{/* Installation Instructions */}

Agent Installation

  1. Make sure you have the PatchMon credentials file set up on your host ( /etc/patchmon/credentials )
  2. SSH into your Docker host where you want to monitor containers
  3. Run the installation command below
  4. The agent will automatically collect Docker container and image information every 5 minutes
  5. View your Docker inventory in the Docker page
{/* Installation Command */}

Quick Installation (One-Line Command)

Download and install the Docker agent:

💡 This will download the agent, make it executable, and set up a cron job to run every 5 minutes

{/* Manual Installation Steps */}

Manual Installation Steps

Step 1: Download the agent

Step 2: Make it executable

Step 3: Test the agent

Step 4: Set up automatic collection (every 5 minutes)

{/* Prerequisites */}

Prerequisites:

  • Docker must be installed and running on the host
  • PatchMon credentials file must exist at{" "} /etc/patchmon/credentials
  • The host must have network access to your PatchMon server
  • The agent must run as root (or with sudo)
)}
{/* Create Token Modal */} {show_create_modal && (

{activeTab === "gethomepage" ? "Create GetHomepage API Key" : "Create Auto-Enrollment Token"}

{activeTab === "proxmox" && ( <> )}
)} {/* New Token Display Modal */} {new_token && (

{activeTab === "gethomepage" ? "API Key Created Successfully" : "Token Created Successfully"}

Important: Save these credentials - the secret won't be shown again.

{activeTab === "proxmox" && (
One-Line Installation Command

Run this command on your Proxmox host to download and execute the enrollment script:

{/* Force Install Toggle */}

Enable this if your LXC containers have broken packages (CloudPanel, WHM, etc.) that block apt-get operations

💡 This command will automatically discover and enroll all running LXC containers.

)} {activeTab === "gethomepage" && (