diff --git a/web/src/components/domains/DomainCard.tsx b/web/src/components/domains/DomainCard.tsx
index 0371259..ec68ef1 100644
--- a/web/src/components/domains/DomainCard.tsx
+++ b/web/src/components/domains/DomainCard.tsx
@@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
-import { Trash2, CheckCircle, AlertCircle, Loader2, Plus, Undo2, Save, RotateCw } from 'lucide-react';
+import { Trash2, CheckCircle, AlertCircle, Loader2, Plus, Undo2, Save, RotateCw, Pencil } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useIsMobile } from '@/hooks/use-mobile';
import { DomainTopology } from './DomainTopology';
@@ -107,7 +107,7 @@ function describeChanges(server: DomainDraft, draft: DomainDraft): string[] {
return changes;
}
-/** Compact controls for mobile */
+/** Compact controls for mobile — vertical pipeline: Source → Routing → Destination */
function MobileControls({
draft, onUpdateDraft, domain, cert, ddnsRecord,
onProvisionCert, onRenewCert, isProvisioningCert, isRenewingCert,
@@ -138,14 +138,29 @@ function MobileControls({
};
return (
-
-
-
-
onUpdateDraft({ public: v })} />
+
+ {/* === Section 1: Source === */}
+
+
Source
+
+
+ onUpdateDraft({ public: v })} />
+
+ {draft.public && ddnsRecord && !ddnsRecord.ok && (
+
+
+ DDNS sync failed{ddnsRecord.error ? `: ${ddnsRecord.error}` : ''}
+
+ )}
+
+
+ onUpdateDraft({ subdomains: v })} />
+
-
-
+ {/* === Section 2: Routing === */}
+
+
Routing
{([
{ value: 'dns-only' as const, label: 'Direct' },
@@ -163,114 +178,112 @@ function MobileControls({
))}
+ {isDnsOnly && (
+
+
+ No firewall, proxy, or TLS protection — traffic goes directly to your server
+
+ )}
+ {isL7 && (
+ <>
+
+
+ {hasCert ? (
+ <>
+
+
TLS: {(cert!.cert.wildcard || cert!.coveredBy) ? `*.${cert!.domain}` : cert!.domain}
+ {cert!.cert.daysLeft != null &&
({cert!.cert.daysLeft}d)}
+ >
+ ) : (
+ <>
+
+
No TLS certificate
+ >
+ )}
+
+ {!hasCert && (
+
+ )}
+ {hasCert && daysLeft < 30 && (
+
+ )}
+
+ {!hasCert && (
+
+
+ L7 routing is inactive until a TLS certificate is provisioned
+
+ )}
+ >
+ )}
- {isL7 && (
-
-
- {hasCert ? (
- <>
-
-
TLS: {(cert!.cert.wildcard || cert!.coveredBy) ? `*.${cert!.domain}` : cert!.domain}
- {cert!.cert.daysLeft != null &&
({cert!.cert.daysLeft}d)}
- >
- ) : (
- <>
-
-
No TLS certificate
- >
- )}
-
- {!hasCert && (
-
- )}
- {hasCert && daysLeft < 30 && (
-
- )}
-
- )}
-
-
-
Backend:
+ {/* === Section 3: Destination === */}
+
+
Destination
{editingBackend ? (
-
+
{
if (e.key === 'Enter') handleBackendSave();
if (e.key === 'Escape') setEditingBackend(false);
}}
/>
-
-
-
+
+
+
+
+
) : (
-
+
+
+ {hasRoutes ? `${domain.routes!.length} backends` : draft.backendAddress}
+
+ {!hasRoutes && (
+
+ )}
+
+ )}
+ {isL7 && domain.routes && domain.routes.length > 0 && (
+
+
Route table
+ {domain.routes.map((route, i) => (
+
+
+ {route.paths?.length ? route.paths.join(', ') : '/*'}
+
+ {'->'}
+ {route.backend.address}
+ {route.ipAllow && route.ipAllow.length > 0 && (
+
+ {route.ipAllow.length} IP rules
+
+ )}
+
+ ))}
+
)}
-
- {/* Route table */}
- {isL7 && domain.routes && domain.routes.length > 0 && (
-
-
Route table
- {domain.routes.map((route, i) => (
-
-
- {route.paths?.length ? route.paths.join(', ') : '/*'}
-
- {'->'}
- {route.backend.address}
- {route.ipAllow && route.ipAllow.length > 0 && (
-
- {route.ipAllow.length} IP rules
-
- )}
-
- ))}
-
- )}
-
- {/* Warnings */}
- {isDnsOnly && (
-
-
- No firewall, proxy, or TLS protection — traffic goes directly to your server
-
- )}
- {isL7 && !hasCert && (
-
-
- L7 routing is inactive until a TLS certificate is provisioned
-
- )}
- {draft.public && ddnsRecord && !ddnsRecord.ok && (
-
-
- DDNS sync failed{ddnsRecord.error ? `: ${ddnsRecord.error}` : ''}
-
- )}
);
}
@@ -507,16 +520,18 @@ export function DomainCard({
)}
- {/* === Footer: subdomains + source + deregister === */}
+ {/* === Footer: subdomains (desktop only) + source + deregister === */}
-
+ {!isMobile && (
+
+ )}
Source: {domain.source}
diff --git a/web/src/components/domains/DomainTopology.tsx b/web/src/components/domains/DomainTopology.tsx
index 682b904..f60091a 100644
--- a/web/src/components/domains/DomainTopology.tsx
+++ b/web/src/components/domains/DomainTopology.tsx
@@ -1,5 +1,4 @@
import { useMemo, useEffect, useState, useCallback, useRef } from 'react';
-import { Link } from 'react-router';
import {
ReactFlow, Handle, Position, applyNodeChanges,
type Node, type Edge, type NodeProps, type NodeChange, type ReactFlowInstance,
@@ -169,23 +168,17 @@ function FlowNode({ data }: NodeProps) {
<>
{leftHandle}
- e.stopPropagation()}>
- }
- className="group-hover:border-primary/40"
- />
-
- e.stopPropagation()}>
- }
- className="group-hover:border-primary/40"
- />
-
+ }
+ />
+ }
+ />
{rightHandle}
>
@@ -317,15 +310,12 @@ function FlowNode({ data }: NodeProps) {
return (
<>
{leftHandle}
- e.stopPropagation()}>
- }
- className="hover:border-primary/40"
- />
-
+ }
+ />
{rightHandle}
>
);
@@ -336,16 +326,13 @@ function FlowNode({ data }: NodeProps) {
return (
<>
{leftHandle}
- e.stopPropagation()}>
- }
- className="hover:border-primary/40"
- tooltip="Your LAN router port-forwards these ports to Wild Central. See Dashboard for setup."
- />
-
+ }
+ tooltip="Your LAN router port-forwards these ports to Wild Central"
+ />
{rightHandle}
>
);