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 (
-