"use client" import { useState, useEffect } from "react" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" import { Checkbox } from "./ui/checkbox" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "./ui/dialog" import { ShieldCheck, Globe, ExternalLink, Loader2, CheckCircle, XCircle, Play, Square, RotateCw, Trash2, FileText, ChevronRight, ChevronDown, AlertTriangle, Info, Network, Eye, EyeOff, Settings, Wifi, Key, } from "lucide-react" import { fetchApi } from "../lib/api-config" interface NetworkInfo { interface: string type?: string address?: string ip?: string subnet: string prefixlen?: number recommended?: boolean } interface StorageInfo { name: string type: string total: number used: number avail: number active: boolean enabled: boolean recommended: boolean } interface AppStatus { state: "not_installed" | "running" | "stopped" | "error" health: string uptime_seconds: number last_check: string } interface ConfigSchema { [key: string]: { type: string label: string description: string placeholder?: string default?: any required?: boolean sensitive?: boolean env_var?: string help_url?: string help_text?: string options?: Array<{ value: string; label: string; description?: string }> depends_on?: { field: string; values: string[] } flag?: string warning?: string validation?: { pattern?: string; max_length?: number; message?: string } } } interface WizardStep { id: string title: string description: string fields?: string[] } export function SecureGatewaySetup() { // State const [loading, setLoading] = useState(true) const [runtimeAvailable, setRuntimeAvailable] = useState(false) const [runtimeInfo, setRuntimeInfo] = useState<{ runtime: string; version: string } | null>(null) const [appStatus, setAppStatus] = useState({ state: "not_installed", health: "unknown", uptime_seconds: 0, last_check: "" }) const [configSchema, setConfigSchema] = useState(null) const [wizardSteps, setWizardSteps] = useState([]) const [networks, setNetworks] = useState([]) const [storages, setStorages] = useState([]) // Wizard state const [showWizard, setShowWizard] = useState(false) const [currentStep, setCurrentStep] = useState(0) const [config, setConfig] = useState>({}) const [deploying, setDeploying] = useState(false) const [deployProgress, setDeployProgress] = useState("") const [deployError, setDeployError] = useState("") // Installed state const [actionLoading, setActionLoading] = useState(null) const [showLogs, setShowLogs] = useState(false) const [logs, setLogs] = useState("") const [logsLoading, setLogsLoading] = useState(false) const [showRemoveConfirm, setShowRemoveConfirm] = useState(false) const [showAuthKey, setShowAuthKey] = useState(false) // Post-deploy confirmation const [showPostDeployInfo, setShowPostDeployInfo] = useState(false) const [deployedConfig, setDeployedConfig] = useState>({}) // Host IP for "Host Only" mode const [hostIp, setHostIp] = useState("") // Update Auth Key const [showUpdateAuthKey, setShowUpdateAuthKey] = useState(false) const [newAuthKey, setNewAuthKey] = useState("") const [updateAuthKeyLoading, setUpdateAuthKeyLoading] = useState(false) const [updateAuthKeyError, setUpdateAuthKeyError] = useState("") // Password visibility const [visiblePasswords, setVisiblePasswords] = useState>(new Set()) useEffect(() => { loadInitialData() }, []) const loadInitialData = async () => { setLoading(true) try { // Secure Gateway uses standard LXC, not OCI containers // So we don't require PVE 9.1+ - it works on any Proxmox version setRuntimeAvailable(true) // Still load runtime info for reference const runtimeRes = await fetchApi("/api/oci/runtime") if (runtimeRes.success) { setRuntimeInfo({ runtime: runtimeRes.runtime || "proxmox-lxc", version: runtimeRes.version || "unknown" }) } // Load app definition const catalogRes = await fetchApi("/api/oci/catalog/secure-gateway") if (catalogRes.success && catalogRes.app) { setConfigSchema(catalogRes.app.config_schema || {}) setWizardSteps(catalogRes.app.ui?.wizard_steps || []) // Set defaults const defaults: Record = {} for (const [key, field] of Object.entries(catalogRes.app.config_schema || {})) { if (field.default !== undefined) { defaults[key] = field.default } } setConfig(defaults) } // Load status await loadStatus() // Load networks const networksRes = await fetchApi("/api/oci/networks") if (networksRes.success) { setNetworks(networksRes.networks || []) // Get host IP for "Host Only" mode const primaryNetwork = networksRes.networks?.find((n: NetworkInfo) => n.recommended) || networksRes.networks?.[0] // Backend returns "ip" field with the host IP address const hostIpValue = primaryNetwork?.ip || primaryNetwork?.address if (hostIpValue) { // Remove CIDR notation if present (e.g., "192.168.0.55/24" -> "192.168.0.55") const ip = hostIpValue.split("/")[0] setHostIp(ip) } } // Load available storages const storagesRes = await fetchApi("/api/oci/storages") if (storagesRes.success && storagesRes.storages?.length > 0) { setStorages(storagesRes.storages) // Set default storage (first recommended one) const recommended = storagesRes.storages.find((s: StorageInfo) => s.recommended) || storagesRes.storages[0] if (recommended) { setConfig(prev => ({ ...prev, storage: recommended.name })) } } } catch (err) { console.error("Failed to load data:", err) } finally { setLoading(false) } } const loadStatus = async () => { try { const statusRes = await fetchApi("/api/oci/status/secure-gateway") if (statusRes.success) { setAppStatus(statusRes.status) } } catch (err) { // Not installed is ok } } const handleDeploy = async () => { setDeploying(true) setDeployError("") setDeployProgress("Preparing deployment...") try { // Validate required fields const step = wizardSteps[currentStep] if (step?.fields) { for (const fieldName of step.fields) { const field = configSchema?.[fieldName] if (field?.required && !config[fieldName]) { setDeployError(`${field.label} is required`) setDeploying(false) return } } } // Prepare config based on access_mode const deployConfig = { ...config } if (config.access_mode === "host_only" && hostIp) { // Host only: just the host IP deployConfig.advertise_routes = [`${hostIp}/32`] } else if (config.access_mode === "proxmox_network") { // Proxmox network: use the recommended network (should already be set) if (!deployConfig.advertise_routes?.length) { const recommendedNetwork = networks.find((n) => n.recommended) || networks[0] if (recommendedNetwork) { deployConfig.advertise_routes = [recommendedNetwork.subnet] } } } // For "custom", the user has already selected networks manually setDeployProgress("Creating LXC container...") const result = await fetchApi("/api/oci/deploy", { method: "POST", body: JSON.stringify({ app_id: "secure-gateway", config: deployConfig }) }) if (!result.success) { // Make runtime errors more user-friendly let errorMsg = result.message || "Deployment failed" if (errorMsg.includes("9.1") || errorMsg.includes("OCI") || errorMsg.includes("not supported")) { errorMsg = "OCI containers require Proxmox VE 9.1 or later. Please upgrade your Proxmox installation to use this feature." } setDeployError(errorMsg) setDeploying(false) return } setDeployProgress("Gateway deployed successfully!") // Wait and reload status, then show post-deploy info setTimeout(async () => { await loadStatus() setShowWizard(false) setDeploying(false) setCurrentStep(0) // Show post-deploy confirmation - always show when access mode is set (routes need approval) const needsApproval = deployConfig.access_mode && deployConfig.access_mode !== "none" if (needsApproval) { // Ensure advertise_routes is set for the dialog const finalConfig = { ...deployConfig } if (deployConfig.access_mode === "host_only" && hostIp) { finalConfig.advertise_routes = [`${hostIp}/32`] } setDeployedConfig(finalConfig) setShowPostDeployInfo(true) } }, 2000) } catch (err: any) { setDeployError(err.message || "Deployment failed") setDeploying(false) } } const handleAction = async (action: "start" | "stop" | "restart") => { setActionLoading(action) try { const result = await fetchApi(`/api/oci/installed/secure-gateway/${action}`, { method: "POST" }) if (result.success) { await loadStatus() } } catch (err) { console.error(`Failed to ${action}:`, err) } finally { setActionLoading(null) } } const handleUpdateAuthKey = async () => { if (!newAuthKey.trim()) { setUpdateAuthKeyError("Auth Key is required") return } setUpdateAuthKeyLoading(true) setUpdateAuthKeyError("") try { const result = await fetchApi("/api/oci/installed/secure-gateway/update-auth-key", { method: "POST", body: JSON.stringify({ auth_key: newAuthKey.trim() }) }) if (!result.success) { setUpdateAuthKeyError(result.message || "Failed to update auth key") setUpdateAuthKeyLoading(false) return } // Success - close dialog and reload status setShowUpdateAuthKey(false) setNewAuthKey("") await loadStatus() } catch (err: any) { setUpdateAuthKeyError(err.message || "Failed to update auth key") } finally { setUpdateAuthKeyLoading(false) } } const handleRemove = async () => { setActionLoading("remove") try { const result = await fetchApi("/api/oci/installed/secure-gateway?remove_data=false", { method: "DELETE" }) if (result.success) { setAppStatus({ state: "not_installed", health: "unknown", uptime_seconds: 0, last_check: "" }) setShowRemoveConfirm(false) } } catch (err) { console.error("Failed to remove:", err) } finally { setActionLoading(null) } } const loadLogs = async () => { setLogsLoading(true) try { const result = await fetchApi("/api/oci/installed/secure-gateway/logs?lines=100") if (result.success) { setLogs(result.logs || "No logs available") } } catch (err) { setLogs("Failed to load logs") } finally { setLogsLoading(false) } } const formatUptime = (seconds: number): string => { if (seconds < 60) return `${seconds}s` if (seconds < 3600) return `${Math.floor(seconds / 60)}m` if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m` return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h` } const renderField = (fieldName: string) => { const field = configSchema?.[fieldName] if (!field) return null // Check depends_on if (field.depends_on) { const depValue = config[field.depends_on.field] if (!field.depends_on.values.includes(depValue)) { return null } } const isVisible = visiblePasswords.has(fieldName) switch (field.type) { case "password": return (
setConfig({ ...config, [fieldName]: e.target.value })} placeholder={field.placeholder} className="pr-10 bg-background border-border" />

{field.description}

{field.help_url && ( {field.help_text || "Learn more"} )}
) case "text": return (
setConfig({ ...config, [fieldName]: e.target.value })} placeholder={field.placeholder} className="bg-background border-border" />

{field.description}

) case "select": // Special handling for access_mode to auto-select networks const handleSelectChange = (value: string) => { const newConfig = { ...config, [fieldName]: value } // When access_mode changes to proxmox_network, auto-select the recommended network if (fieldName === "access_mode" && value === "proxmox_network") { const recommendedNetwork = networks.find((n) => n.recommended) || networks[0] if (recommendedNetwork) { newConfig.advertise_routes = [recommendedNetwork.subnet] } } // Clear routes when switching to host_only if (fieldName === "access_mode" && value === "host_only") { newConfig.advertise_routes = [] } // Clear routes when switching to custom (user will select manually) if (fieldName === "access_mode" && value === "custom") { newConfig.advertise_routes = [] } setConfig(newConfig) } return (
{field.options?.map((opt) => (
handleSelectChange(opt.value)} className={`p-3 rounded-lg border cursor-pointer transition-colors ${ config[fieldName] === opt.value ? "border-cyan-500 bg-cyan-500/10" : "border-border hover:border-muted-foreground/50" }`} >
{config[fieldName] === opt.value && (
)}

{opt.label}

{opt.description && (

{opt.description}

)} {/* Show selected network for proxmox_network */} {fieldName === "access_mode" && opt.value === "proxmox_network" && config[fieldName] === "proxmox_network" && (

{networks.find((n) => n.recommended)?.subnet || networks[0]?.subnet || "No network detected"}

)}
))}
) case "networks": return (

{field.description}

{networks.length === 0 ? (

No networks detected

) : ( networks.map((net) => { const selected = (config[fieldName] || []).includes(net.subnet) return (
{ const current = config[fieldName] || [] const updated = selected ? current.filter((s: string) => s !== net.subnet) : [...current, net.subnet] setConfig({ ...config, [fieldName]: updated }) }} className={`p-3 rounded-lg border cursor-pointer transition-colors flex items-center gap-3 ${ selected ? "border-cyan-500 bg-cyan-500/10" : "border-border hover:border-muted-foreground/50" }`} >
{net.subnet} {net.recommended && ( Recommended )}

{net.interface} ({net.type})

) }) )}
) case "boolean": return (
setConfig({ ...config, [fieldName]: !config[fieldName] })} className={`p-3 rounded-lg border cursor-pointer transition-colors flex items-start gap-3 ${ config[fieldName] ? "border-cyan-500 bg-cyan-500/10" : "border-border hover:border-muted-foreground/50" }`} >

{field.label}

{field.description}

{field.warning && config[fieldName] && (

{field.warning}

)}
) default: return null } } const renderWizardContent = () => { const step = wizardSteps[currentStep] if (!step) return null if (step.id === "intro") { return (

Secure Remote Access

Deploy a VPN gateway using Tailscale for secure, zero-trust access to your Proxmox infrastructure without opening ports.

What you{"'"}ll get:

  • Access ProxMenux Monitor from anywhere
  • Secure access to Proxmox web UI
  • Optionally expose VMs and LXC containers
  • End-to-end encryption
  • No port forwarding required

You{"'"}ll need a free Tailscale account. If you don{"'"}t have one, you can create it at{" "} tailscale.com

) } if (step.id === "deploy") { return (

Review & Deploy

Review your configuration before deploying the gateway.

{/* Storage selector */} {storages.length > 1 && (

Select where to create the container disk.

{storages.filter(s => s.active && s.enabled).map((storage) => (
setConfig({ ...config, storage: storage.name })} className={`p-3 rounded-lg border cursor-pointer transition-colors ${ config.storage === storage.name ? "border-cyan-500 bg-cyan-500/10" : "border-border hover:border-muted-foreground/50" }`} >
{config.storage === storage.name && (
)}
{storage.name} ({storage.type}) {storage.recommended && ( Recommended )}

{(storage.avail / 1024 / 1024 / 1024).toFixed(1)} GB available

))}
)}

Configuration Summary

Hostname: {config.hostname || "proxmox-gateway"}
{storages.length > 1 && (
Storage: {config.storage || storages[0]?.name}
)}
Access Mode: {config.access_mode === "host_only" ? "Host Only" : config.access_mode === "proxmox_network" ? "Proxmox Network" : "Custom Networks"}
{config.access_mode === "host_only" && hostIp && (
Host Access: {hostIp}/32
)} {(config.access_mode === "proxmox_network" || config.access_mode === "custom") && config.advertise_routes?.length > 0 && (
Networks: {config.advertise_routes.join(", ")}
)}
Exit Node: {config.exit_node ? "Yes" : "No"}
Accept Routes: {config.accept_routes ? "Yes" : "No"}
{/* Approval notice */} {(config.access_mode && config.access_mode !== "none") && !deploying && (

Important: After deployment, you must approve the subnet route in Tailscale Admin for remote access to work. {config.exit_node && You{"'"}ll also need to approve the exit node.}

We{"'"}ll show you exactly what to do after the gateway is deployed.

)} {deploying && (
{deployProgress}
)} {deployError && (

{deployError}

)}
) } // Regular step with fields return (

{step.title}

{step.description}

{step.fields?.map((fieldName) => renderField(fieldName))}
) } // Loading state if (loading) { return (
Secure Gateway
) } // Installed state if (appStatus.state !== "not_installed") { const isRunning = appStatus.state === "running" const isStopped = appStatus.state === "stopped" const isError = appStatus.state === "error" return ( <>
Secure Gateway
{isRunning ? : isStopped ? : } {isRunning ? "Connected" : isStopped ? "Stopped" : "Error"}
Tailscale VPN Gateway
{/* Status info */} {isRunning && appStatus.uptime_seconds > 0 && (
Uptime: {formatUptime(appStatus.uptime_seconds)}
)} {/* Actions */}
{isStopped && ( )} {isRunning && ( <> )}
{/* Update Auth Key button */}
Open Tailscale Admin
{/* Logs Dialog */} Secure Gateway Logs Recent container logs
{logsLoading ? (
) : (
                  {logs || "No logs available"}
                
)}
{/* Remove Confirm Dialog */} Remove Secure Gateway? This will stop and remove the gateway container. Your Tailscale state will be preserved for re-deployment.
{/* Update Auth Key Dialog */} { setShowUpdateAuthKey(open) if (!open) { setNewAuthKey("") setUpdateAuthKeyError("") } }}> Update Auth Key Enter a new Tailscale auth key to re-authenticate the gateway. This is useful if your previous key has expired.
setNewAuthKey(e.target.value)} placeholder="tskey-auth-..." className="font-mono text-sm" />

Generate a new key at{" "} Tailscale Admin > Settings > Keys

{updateAuthKeyError && (

{updateAuthKeyError}

)}
{/* Post-Deploy Info Dialog */} Gateway Deployed Successfully One more step to complete the setup

Next Step: Approve in Tailscale Admin

You need to approve the following settings in your Tailscale admin console for them to take effect:

    {deployedConfig.advertise_routes?.length > 0 && (
  • Subnet Routes: {deployedConfig.advertise_routes.join(", ")}
  • )} {deployedConfig.exit_node && (
  • Exit Node: Route all internet traffic
  • )}

How to approve:

  1. Click the button below to open Tailscale Admin
  2. Find {deployedConfig.hostname || "proxmox-gateway"} in the machines list
  3. Click on it to open machine details
  4. In the Subnets section, click Edit and enable the route
  5. {deployedConfig.exit_node && (
  6. In Routing Settings, enable Exit Node
  7. )}

Once approved, you can access your Proxmox host at{" "} {deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8006 (Proxmox UI) or{" "} {deployedConfig.advertise_routes?.[0]?.replace("/32", "") || hostIp}:8008 (ProxMenux Monitor) from any device with Tailscale.

) } // Not installed state return ( <>
Secure Gateway
VPN access without opening ports

Deploy a Tailscale VPN gateway for secure remote access to your Proxmox infrastructure. No port forwarding required.

{/* Wizard Dialog */} { if (!deploying) { setShowWizard(open) if (!open) { setCurrentStep(0) setDeployError("") } } }}> {/* Fixed Header */}
Secure Gateway Setup {/* Progress indicator - filter out "options" step if using Proxmox Only */}
{wizardSteps .filter((step) => !(config.access_mode === "host_only" && step.id === "options")) .map((step, idx) => { // Recalculate the actual step index accounting for skipped steps const actualIdx = wizardSteps.findIndex((s) => s.id === step.id) const adjustedCurrentStep = config.access_mode === "host_only" ? (currentStep > wizardSteps.findIndex((s) => s.id === "options") ? currentStep - 1 : currentStep) : currentStep return (
) })}
{/* Scrollable Content */}
{renderWizardContent()}
{/* Fixed Footer with Navigation */}
{currentStep < wizardSteps.length - 1 ? ( ) : ( )}
) }