diff --git a/AppImage/components/secure-gateway-setup.tsx b/AppImage/components/secure-gateway-setup.tsx index 2ff1817f..d2bce420 100644 --- a/AppImage/components/secure-gateway-setup.tsx +++ b/AppImage/components/secure-gateway-setup.tsx @@ -119,6 +119,7 @@ export function SecureGatewaySetup() { setRuntimeAvailable(true) setRuntimeInfo({ runtime: runtimeRes.runtime, version: runtimeRes.version }) } else { + // Show version requirement message setRuntimeInfo({ runtime: "proxmox-lxc", version: runtimeRes.version || "unknown" }) } @@ -145,9 +146,12 @@ export function SecureGatewaySetup() { 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) } @@ -173,15 +177,30 @@ export function SecureGatewaySetup() { const handleDeploy = async () => { setDeploying(true) setDeployError("") - setDeployProgress("Creating LXC container...") + 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) { @@ -189,23 +208,10 @@ export function SecureGatewaySetup() { } } } + // For "custom", the user has already selected networks manually - // Show progress messages while deploying - const messages = [ - "Creating LXC container...", - "Downloading Alpine Linux template...", - "Configuring container...", - "Installing Tailscale...", - "Connecting to Tailscale network..." - ] - let msgIndex = 0 - const progressInterval = setInterval(() => { - msgIndex = (msgIndex + 1) % messages.length - if (msgIndex < messages.length - 1) { - setDeployProgress(messages[msgIndex]) - } - }, 2000) - + setDeployProgress("Creating LXC container...") + const result = await fetchApi("/api/oci/deploy", { method: "POST", body: JSON.stringify({ @@ -214,40 +220,58 @@ export function SecureGatewaySetup() { }) }) - clearInterval(progressInterval) - if (!result.success) { - setDeployError(result.message || "Failed to deploy gateway") + // 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!") - - // Show post-deploy confirmation - const needsApproval = deployConfig.access_mode && deployConfig.access_mode !== "none" - if (needsApproval) { - const finalConfig = { ...deployConfig } - if (deployConfig.access_mode === "host_only" && hostIp) { - finalConfig.advertise_routes = [`${hostIp}/32`] - } - setDeployedConfig(finalConfig) - setShowPostDeployInfo(true) - } - - await loadStatus() - setTimeout(() => { + // Wait and reload status, then show post-deploy info + setTimeout(async () => { + await loadStatus() setShowWizard(false) setDeploying(false) - setDeployPercent(0) setCurrentStep(0) - }, 1000) + + // 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 || "Failed to deploy gateway") + setDeployError(err.message || "Deployment failed") setDeploying(false) - setDeployPercent(0) + } + } + + 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) } } @@ -274,6 +298,7 @@ export function SecureGatewaySetup() { return } + // Success - close dialog and reload status setShowUpdateAuthKey(false) setNewAuthKey("") await loadStatus() @@ -284,24 +309,16 @@ export function SecureGatewaySetup() { } } - const handleAction = async (action: "start" | "stop" | "restart") => { - setActionLoading(action) - try { - await fetchApi(`/api/oci/installed/secure-gateway/${action}`, { method: "POST" }) - await loadStatus() - } catch (err) { - console.error(`Failed to ${action}:`, err) - } finally { - setActionLoading(null) - } - } - const handleRemove = async () => { setActionLoading("remove") try { - await fetchApi("/api/oci/installed/secure-gateway", { method: "DELETE" }) - setShowRemoveConfirm(false) - await loadStatus() + 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 { @@ -309,12 +326,13 @@ export function SecureGatewaySetup() { } } - const handleViewLogs = async () => { - setShowLogs(true) + const loadLogs = async () => { setLogsLoading(true) try { - const result = await fetchApi("/api/oci/installed/secure-gateway/logs") - setLogs(result.logs || "No logs available") + 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 { @@ -322,71 +340,64 @@ export function SecureGatewaySetup() { } } - const formatUptime = (seconds: number) => { + 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` - return `${Math.floor(seconds / 86400)}d` + 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 togglePasswordVisibility = (fieldName: string) => { - setVisiblePasswords(prev => { - const next = new Set(prev) - if (next.has(fieldName)) { - next.delete(fieldName) - } else { - next.add(fieldName) - } - return next - }) - } + const renderField = (fieldName: string) => { + const field = configSchema?.[fieldName] + if (!field) return null - // Render field based on type - const renderField = (fieldName: string, field: ConfigSchema[string]) => { // Check depends_on if (field.depends_on) { - const dependsValue = config[field.depends_on.field] - if (!field.depends_on.values.includes(dependsValue)) { + 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": - const isVisible = visiblePasswords.has(fieldName) return (
-