Separate Caddy/services box from VLAN 99 management computer

The management computer (VLAN 99) only runs the switch manager tool
and holds SSH keys/TOTP secrets. Caddy and services (Plex, etc.) run
on a SEPARATE computer on LAN.

Backend:
- New /api/services/config endpoint to store services box LAN IP
- services-config.json persists caddy_ip separately from mgmt_ip
- Port forward creation targets caddy_ip (LAN services box), not
  mgmt_ip (VLAN 99 management computer)
- Deploy endpoint uses caddy_ip for all Caddy/NAT references
- _get_caddy_ip() helper reads from services config

Frontend:
- New "Services Host" panel: configure Caddy box LAN IP
- Checklist shows caddy_ip status, not mgmt_ip
- Port forward and deploy pass caddy_ip to backend
- Clear labels: "Services box" vs "Management computer"

Architecture:
  VLAN 99: management computer (this tool, SSH keys, TOTP)
  LAN: services computer (Caddy, Plex, Docker containers)
  WAN port forward 443 → services computer LAN IP

https://claude.ai/code/session_01Do9bsN39MTuy2GVv7yzSrE
This commit is contained in:
Claude
2026-03-28 12:45:34 +00:00
parent 4b28f8cfe5
commit 8a9fa02e25
2 changed files with 96 additions and 26 deletions
+38 -8
View File
@@ -5099,12 +5099,14 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) {
const [deploying, setDeploying] = useState(false);
const [deployResult, setDeployResult] = useState(null);
const [actionLoading, setActionLoading] = useState("");
const [caddyIpInput, setCaddyIpInput] = useState("");
const load = async () => {
try {
const [svc, st] = await Promise.all([API("/services"), API("/services/status")]);
setServices(svc.services || []);
setStatus(st);
if (st.caddy_ip && !caddyIpInput) setCaddyIpInput(st.caddy_ip);
} catch(e) { console.error(e); }
};
useEffect(() => { if (backendOk) load(); }, [backendOk]);
@@ -5125,6 +5127,17 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) {
await load();
};
const saveCaddyIp = async () => {
if (!session) { onNeedAuth(); return; }
if (!caddyIpInput.trim()) return;
try {
await API("/services/config", { method:"POST", body:{
token: session.token, config: { caddy_ip: caddyIpInput.trim() }
}});
await load();
} catch(e) { alert("Failed: " + e.message); }
};
const enableNatReflection = async () => {
if (!session) { onNeedAuth(); return; }
setActionLoading("nat");
@@ -5141,7 +5154,7 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) {
setActionLoading("pf");
try {
const r = await API("/services/create-port-forward", { method:"POST",
body:{ token: session.token, mgmt_ip: status?.mgmt_ip } });
body:{ token: session.token, caddy_ip: status?.caddy_ip } });
if (r.note) alert(r.note);
await load();
} catch(e) { alert("Failed: " + e.message); }
@@ -5153,7 +5166,7 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) {
setDeploying(true); setDeployResult(null);
try {
const r = await API("/services/deploy", { method:"POST",
body:{ token: session.token, mgmt_ip: status?.mgmt_ip } });
body:{ token: session.token, caddy_ip: status?.caddy_ip } });
setDeployResult(r);
await load();
} catch(e) { setDeployResult({ success: false, errors: [e.message] }); }
@@ -5203,10 +5216,32 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) {
</div>
</div>
{/* Services Host Config */}
<div className="panel">
<div className="ph">Services Host (Caddy Computer)</div>
<div className="pb">
<div style={{fontSize:12,color:"var(--dm)",marginBottom:10,lineHeight:1.6}}>
The computer running Caddy and your services (Plex, etc.) on LAN, separate from the
VLAN 99 management computer.
</div>
<div style={{display:"flex",gap:8,alignItems:"flex-end"}}>
<div className="field" style={{margin:0,flex:"0 0 200px"}}>
<label>Caddy / Services Box LAN IP</label>
<input value={caddyIpInput} onChange={e => setCaddyIpInput(e.target.value)}
placeholder="192.168.1.50"/>
</div>
<button className="btn bp" onClick={saveCaddyIp} disabled={!caddyIpInput.trim()}
style={{padding:"8px 16px"}}>Save</button>
</div>
</div>
</div>
{/* Status Checklist */}
<div className="panel">
<div className="ph">Setup Checklist</div>
<div className="pb">
<Check ok={status?.caddy_configured}
label={status?.caddy_ip ? `Services box: ${status.caddy_ip}` : "Services box IP not set — configure above"} />
<Check ok={status?.opnsense_configured} label="OPNsense API connected" />
<Check ok={status?.opnsense_ssh} label="OPNsense SSH connected" />
<Check ok={status?.nat_reflection}
@@ -5214,18 +5249,13 @@ function ServicesTab({ vlans, session, onNeedAuth, backendOk }) {
action={enableNatReflection} actionLabel="Enable NAT Reflection"
loading={actionLoading === "nat"} />
<Check ok={status?.port_forward_443}
label={`WAN port forward 443 → ${status?.mgmt_ip || "?"}:443 (Caddy)`}
label={`WAN port forward 443 → ${status?.caddy_ip || "?"}:443 (Caddy)`}
action={createPortForward} actionLabel="Create Port Forward"
loading={actionLoading === "pf"} />
<Check ok={status?.caddy_file_exists}
label="Caddyfile.services exists" />
<Check ok={services.length > 0}
label={`${services.length} service${services.length !== 1 ? "s" : ""} configured`} />
{status?.mgmt_ip && (
<div style={{fontSize:11,color:"var(--dm)",marginTop:4}}>
Management computer IP: <span style={{fontFamily:"monospace",color:"var(--ac)"}}>{status.mgmt_ip}</span>
</div>
)}
</div>
</div>