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("auto-enrollment"); const [tokens, setTokens] = useState([]); const [host_groups, setHostGroups] = useState([]); const [loading, setLoading] = useState(true); const [show_create_modal, setShowCreateModal] = useState(false); const [show_edit_modal, setShowEditModal] = useState(false); const [edit_token, setEditToken] = useState(null); const [new_token, setNewToken] = useState(null); const [show_secret, setShowSecret] = useState(false); const [server_url, setServerUrl] = useState(""); const [force_proxmox_install, setForceProxmoxInstall] = useState(false); const [usage_type, setUsageType] = useState("proxmox-lxc"); const [selected_script_type, setSelectedScriptType] = useState("proxmox-lxc"); const [curl_flags, setCurlFlags] = useState("-s"); // Form state const [form_data, setFormData] = useState({ token_name: "", max_hosts_per_day: 100, default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", scopes: { host: [], }, }); const [copy_success, setCopySuccess] = useState({}); // Helper function to build enrollment URL with optional force flag and selected type const getEnrollmentUrl = (scriptType = selected_script_type) => { const baseUrl = `${server_url}/api/v1/auto-enrollment/script?type=${scriptType}&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); }; const toggle_scope_action = (resource, action) => { setFormData((prev) => { const current_scopes = prev.scopes || { [resource]: [] }; const resource_scopes = current_scopes[resource] || []; const updated_scopes = resource_scopes.includes(action) ? resource_scopes.filter((a) => a !== action) : [...resource_scopes, action]; return { ...prev, scopes: { ...current_scopes, [resource]: updated_scopes, }, }; }); }; // 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); // Set curl flags based on SSL settings setCurlFlags(response.data.ignore_ssl_self_signed ? "-sk" : "-s"); } catch (error) { console.error("Failed to load server URL:", error); setServerUrl(window.location.origin); setCurlFlags("-s"); } }; const create_token = async (e) => { e.preventDefault(); try { // Determine integration type based on active tab or usage_type let integration_type = "proxmox-lxc"; if (activeTab === "gethomepage") { integration_type = "gethomepage"; } else if (activeTab === "auto-enrollment") { // Use the usage_type selected in the modal integration_type = usage_type; } 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: integration_type, }, }; // 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; } // Add scopes for API credentials if (usage_type === "api" && form_data.scopes) { data.scopes = form_data.scopes; } const response = await api.post("/auto-enrollment/tokens", data); setNewToken(response.data.token); setShowCreateModal(false); load_tokens(); // Keep usage_type so the success modal can use it // Reset form setFormData({ token_name: "", max_hosts_per_day: 100, default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", scopes: { host: [], }, }); } 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 open_edit_modal = (token) => { setEditToken(token); setFormData({ token_name: token.token_name, max_hosts_per_day: token.max_hosts_per_day || 100, default_host_group_id: token.default_host_group_id || "", allowed_ip_ranges: token.allowed_ip_ranges?.join(", ") || "", expires_at: token.expires_at ? new Date(token.expires_at).toISOString().slice(0, 16) : "", scopes: token.scopes || { host: [] }, }); setShowEditModal(true); }; const update_token = async (e) => { e.preventDefault(); try { const data = { allowed_ip_ranges: form_data.allowed_ip_ranges ? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim()) : [], }; // Add expiration if provided if (form_data.expires_at) { data.expires_at = form_data.expires_at; } // Add scopes for API credentials if ( edit_token?.metadata?.integration_type === "api" && form_data.scopes ) { data.scopes = form_data.scopes; } await api.patch(`/auto-enrollment/tokens/${edit_token.id}`, data); setShowEditModal(false); setEditToken(null); load_tokens(); // Reset form setFormData({ token_name: "", max_hosts_per_day: 100, default_host_group_id: "", allowed_ip_ranges: "", expires_at: "", scopes: { host: [], }, }); } catch (error) { console.error("Failed to update token:", error); const error_message = error.response?.data?.errors ? error.response.data.errors.map((e) => e.msg).join(", ") : error.response?.data?.error || "Failed to update token"; alert(error_message); } }; 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 */}
{/* Auto-Enrollment & API Tab */} {activeTab === "auto-enrollment" && (
{/* Header with New Token Button */}

Auto-Enrollment & API Credentials

Manage tokens for Proxmox LXC auto-enrollment and API access

{/* Token List */} {loading ? (
) : tokens.filter( (token) => token.metadata?.integration_type === "proxmox-lxc" || token.metadata?.integration_type === "api", ).length === 0 ? (

No auto-enrollment or API tokens created yet.

Create a token to enable Proxmox auto-enrollment or API access.

) : (
{tokens .filter( (token) => token.metadata?.integration_type === "proxmox-lxc" || token.metadata?.integration_type === "api", ) .map((token) => (

{token.token_name}

{token.metadata?.integration_type === "proxmox-lxc" ? ( Proxmox LXC ) : ( API )} {token.is_active ? ( Active ) : ( Inactive )}
{token.token_key}
{token.metadata?.integration_type === "proxmox-lxc" && (

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

)} {token.metadata?.integration_type === "proxmox-lxc" && token.host_groups && (

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

)} {token.metadata?.integration_type === "api" && token.scopes && (

Scopes:{" "} {Object.entries(token.scopes) .map( ([resource, actions]) => `${resource}: ${Array.isArray(actions) ? actions.join(", ") : actions}`, ) .join(" | ")}

)} {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) )}

)}
{token.metadata?.integration_type === "api" && ( )}
))}
)} {/* Documentation Section */}

Documentation

{/* Proxmox Documentation */}

Auto-enrollment

Automatically discover and enroll hosts from Proxmox or direct enrollment.

View Guide
{/* API Documentation */}

Scoped credentials

Programmatic access to PatchMon data with granular scope-based permissions.

View Guide
)} {/* 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 Inventory Collection

Docker monitoring is now built into the PatchMon Go agent

{/* Info Message */}

Automatic Docker Discovery

The PatchMon Go agent automatically discovers Docker when it's available on your host and collects comprehensive inventory information:

  • Containers - Running and stopped containers with status, images, ports, and labels
  • Images - All Docker images with repository, tags, sizes, and sources
  • Volumes - Named and anonymous volumes with drivers, mountpoints, and usage
  • Networks - Docker networks with drivers, IPAM configuration, and connected containers
  • Real-time Updates - Container status changes are pushed instantly via WebSocket
{/* How It Works */}

How It Works

  1. Install the PatchMon Go agent on your host (see the Hosts page for installation instructions)
  2. The agent automatically detects if Docker is installed and running on the host
  3. During each collection cycle, the agent gathers Docker inventory data and sends it to the PatchMon server
  4. View your complete Docker inventory (containers, images, volumes, networks) in the{" "} Docker page
  5. Container status changes are pushed to the server in real-time via WebSocket connection
{/* No Configuration Required */}

No Additional Configuration Required

Once the Go agent is installed and Docker is running on your host, Docker inventory collection happens automatically. No separate Docker agent or cron jobs needed.

{/* Requirements */}

Requirements:

  • PatchMon Go agent must be installed and running
  • Docker daemon must be installed and running
  • Agent must have access to the Docker socket ( /var/run/docker.sock )
  • Typically requires running the agent as root or with Docker group permissions
)}
{/* Create Token Modal */} {show_create_modal && (

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

{/* Tabs for Auto-enrollment modal */} {activeTab === "auto-enrollment" && (
)}
{usage_type === "proxmox-lxc" && activeTab === "auto-enrollment" && ( <> )} {usage_type === "api" && activeTab === "auto-enrollment" && (
Scopes *

Host Permissions

{["get", "put", "patch", "update", "delete"].map( (action) => ( ), )}

Select the permissions this API credential should have

)}
)} {/* New Token Display Modal */} {new_token && (

{new_token.metadata?.integration_type === "gethomepage" || activeTab === "gethomepage" ? "API Key Created Successfully" : new_token.metadata?.integration_type === "api" || usage_type === "api" ? "API Credential Created Successfully" : "Token Created Successfully"}

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

{(new_token.metadata?.integration_type === "api" || usage_type === "api") && new_token.scopes && (
Granted Scopes
{Object.entries(new_token.scopes).map( ([resource, actions]) => (
{resource}: {" "} {Array.isArray(actions) ? actions.join(", ").toUpperCase() : actions}
), )}
)} {(new_token.metadata?.integration_type === "api" || usage_type === "api") && (
Usage Examples

Basic cURL request:

Filter by host group:

💡 Replace "Production" with your host group name or UUID

)} {(new_token.metadata?.integration_type === "proxmox-lxc" || usage_type === "proxmox-lxc") && (
Auto-Enrollment Command
{/* Script Type Toggle Buttons */}

{selected_script_type === "proxmox-lxc" ? "Run this command on your Proxmox host to automatically discover and enroll all running LXC containers:" : "Run this command on individual hosts to enroll them directly:"}

{/* Force Install Toggle */}

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

{/* Usage hint for direct-host */} {selected_script_type === "direct-host" && (

💡 Tip: Specify a custom name:{" "} FRIENDLY_NAME="My Server" sh

)}
)} {(new_token.metadata?.integration_type === "gethomepage" || activeTab === "gethomepage") && (