diff --git a/AppImage/components/secure-gateway-setup.tsx b/AppImage/components/secure-gateway-setup.tsx index d2bce420..9eb8969a 100644 --- a/AppImage/components/secure-gateway-setup.tsx +++ b/AppImage/components/secure-gateway-setup.tsx @@ -80,6 +80,7 @@ export function SecureGatewaySetup() { const [config, setConfig] = useState>({}) const [deploying, setDeploying] = useState(false) const [deployProgress, setDeployProgress] = useState("") + const [deployPercent, setDeployPercent] = useState(0) const [deployError, setDeployError] = useState("") // Installed state @@ -119,7 +120,6 @@ 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" }) } @@ -146,12 +146,9 @@ 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) } @@ -178,29 +175,15 @@ export function SecureGatewaySetup() { setDeploying(true) setDeployError("") setDeployProgress("Preparing deployment...") + setDeployPercent(5) 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) { @@ -208,10 +191,19 @@ export function SecureGatewaySetup() { } } } - // For "custom", the user has already selected networks manually setDeployProgress("Creating LXC container...") - + setDeployPercent(20) + + // Small delay to show progress + await new Promise(resolve => setTimeout(resolve, 500)) + setDeployProgress("Downloading Tailscale image...") + setDeployPercent(35) + + await new Promise(resolve => setTimeout(resolve, 300)) + setDeployProgress("Configuring container...") + setDeployPercent(50) + const result = await fetchApi("/api/oci/deploy", { method: "POST", body: JSON.stringify({ @@ -220,58 +212,47 @@ export function SecureGatewaySetup() { }) }) + setDeployProgress("Installing Tailscale...") + setDeployPercent(70) + + await new Promise(resolve => setTimeout(resolve, 300)) + setDeployProgress("Connecting to Tailscale network...") + setDeployPercent(85) + 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) + setDeployError(result.message || "Failed to deploy gateway") setDeploying(false) + setDeployPercent(0) return } setDeployProgress("Gateway deployed successfully!") + setDeployPercent(100) + + // 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() - // Wait and reload status, then show post-deploy info - setTimeout(async () => { - await loadStatus() + setTimeout(() => { setShowWizard(false) setDeploying(false) + setDeployPercent(0) 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) + }, 1000) } catch (err: any) { - setDeployError(err.message || "Deployment failed") + setDeployError(err.message || "Failed to deploy gateway") 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) + setDeployPercent(0) } } @@ -298,7 +279,6 @@ export function SecureGatewaySetup() { return } - // Success - close dialog and reload status setShowUpdateAuthKey(false) setNewAuthKey("") await loadStatus() @@ -309,16 +289,24 @@ 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 { - 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) - } + await fetchApi("/api/oci/installed/secure-gateway/remove", { method: "DELETE" }) + setShowRemoveConfirm(false) + await loadStatus() } catch (err) { console.error("Failed to remove:", err) } finally { @@ -326,13 +314,12 @@ export function SecureGatewaySetup() { } } - const loadLogs = async () => { + const handleViewLogs = async () => { + setShowLogs(true) setLogsLoading(true) try { - const result = await fetchApi("/api/oci/installed/secure-gateway/logs?lines=100") - if (result.success) { - setLogs(result.logs || "No logs available") - } + const result = await fetchApi("/api/oci/installed/secure-gateway/logs") + setLogs(result.logs || "No logs available") } catch (err) { setLogs("Failed to load logs") } finally { @@ -340,64 +327,71 @@ export function SecureGatewaySetup() { } } - const formatUptime = (seconds: number): string => { + const formatUptime = (seconds: number) => { 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` + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h` + return `${Math.floor(seconds / 86400)}d` } - const renderField = (fieldName: string) => { - const field = configSchema?.[fieldName] - if (!field) return null + const togglePasswordVisibility = (fieldName: string) => { + setVisiblePasswords(prev => { + const next = new Set(prev) + if (next.has(fieldName)) { + next.delete(fieldName) + } else { + next.add(fieldName) + } + return next + }) + } + // Render field based on type + const renderField = (fieldName: string, field: ConfigSchema[string]) => { // Check depends_on if (field.depends_on) { - const depValue = config[field.depends_on.field] - if (!field.depends_on.values.includes(depValue)) { + const dependsValue = config[field.depends_on.field] + if (!field.depends_on.values.includes(dependsValue)) { return null } } - const isVisible = visiblePasswords.has(fieldName) - switch (field.type) { case "password": + const isVisible = visiblePasswords.has(fieldName) return (
-