"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, } from "lucide-react" import { fetchApi } from "../lib/api-config" interface NetworkInfo { interface: string type: string address: string subnet: string prefixlen: number 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([]) // 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("") // Password visibility const [visiblePasswords, setVisiblePasswords] = useState>(new Set()) useEffect(() => { loadInitialData() }, []) const loadInitialData = async () => { setLoading(true) try { // Load runtime info (checks for Proxmox 9.1+ OCI support) const runtimeRes = await fetchApi("/api/oci/runtime") if (runtimeRes.success && runtimeRes.available) { setRuntimeAvailable(true) setRuntimeInfo({ runtime: runtimeRes.runtime, version: runtimeRes.version }) } else { // Show version requirement message setRuntimeInfo({ 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] if (primaryNetwork?.address) { setHostIp(primaryNetwork.address) } } } 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 - for "host_only" mode, set routes to just the host IP const deployConfig = { ...config } if (config.access_mode === "host_only" && hostIp) { deployConfig.advertise_routes = [`${hostIp}/32`] } 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 if user needs to approve routes const needsApproval = deployConfig.advertise_routes?.length > 0 || deployConfig.exit_node || deployConfig.accept_routes if (needsApproval) { setDeployedConfig(deployConfig) 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 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": return (
{field.options?.map((opt) => (
setConfig({ ...config, [fieldName]: 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}

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

Configuration Summary

Hostname: {config.hostname || "proxmox-gateway"}
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 !== "none" || config.exit_node) && !deploying && (

After deployment, you{"'"}ll need to approve the subnet routes {config.exit_node && and exit node} in your Tailscale Admin Console for them to work.

)} {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 && ( <> )}
{/* Tailscale admin link */}
{/* 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.
{/* Post-Deploy Info Dialog */} Gateway Deployed Successfully One more step to complete the setup

Action Required 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. Go to Tailscale Admin Console
  2. Find the machine "{deployedConfig.hostname || "proxmox-gateway"}"
  3. Click on it and approve the pending routes/exit node
) } // 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.

{runtimeAvailable ? (
Proxmox VE {runtimeInfo?.version} - OCI support available
) : (
Requires Proxmox VE 9.1+ (current: {runtimeInfo?.version || "unknown"})
)}
{/* Wizard Dialog */} { if (!deploying) { setShowWizard(open) if (!open) { setCurrentStep(0) setDeployError("") } } }}> Secure Gateway Setup {/* Progress indicator */}
{wizardSteps.map((step, idx) => (
))}
{renderWizardContent()} {/* Navigation */}
{currentStep < wizardSteps.length - 1 ? ( ) : ( )}
) }