Setup wizard: show + override the backend push IP (multi-NIC hosts)
The backend IP baked into a push-capable device at assign time is auto-derived by subnet-matching a local NIC. That's non-deterministic when two NICs match the device subnet, and null when none does. Surface it: backendIpCandidates() lists all local IPv4 NICs (on-subnet first), GET /api/setup/backend-ips serves them, and the wizard renders an editable Backend push IP dropdown after a successful test (pre-filled with the auto-pick, warns when no NIC is on the device subnet). The chosen IP overrides the auto-pick on assign and is recorded in config.
This commit is contained in:
@@ -2,8 +2,10 @@ import { useState, useEffect } from "react";
|
||||
import {
|
||||
assignDevice,
|
||||
discoverDevices,
|
||||
fetchBackendIps,
|
||||
fetchCatalog,
|
||||
testDevice,
|
||||
type BackendIpCandidate,
|
||||
type Catalog,
|
||||
type CatalogEntry,
|
||||
type DeviceCategory,
|
||||
@@ -102,6 +104,39 @@ function CategoryPicker({
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanError, setScanError] = useState<string | null>(null);
|
||||
|
||||
// Backend push IP: which of OUR addresses the device should call back on. We
|
||||
// auto-pick the NIC on the device's subnet, but surface it editable here so a
|
||||
// multi-NIC host can be corrected (the chosen IP is baked into the device on
|
||||
// save). Only relevant for drivers that push (the field hides if no candidates).
|
||||
const [backendIps, setBackendIps] = useState<BackendIpCandidate[] | null>(null);
|
||||
const [backendIp, setBackendIp] = useState<string>("");
|
||||
|
||||
// (Re)load backend-IP candidates whenever the device host changes after a
|
||||
// successful test (the test confirms the host is real + reachable).
|
||||
const testedHost = tested ? String(mergedConfig().host ?? "") : "";
|
||||
useEffect(() => {
|
||||
if (!testedHost) {
|
||||
setBackendIps(null);
|
||||
return;
|
||||
}
|
||||
let live = true;
|
||||
fetchBackendIps(testedHost)
|
||||
.then(({ candidates }) => {
|
||||
if (!live) return;
|
||||
setBackendIps(candidates);
|
||||
// Pre-fill with the on-subnet auto-pick (the first candidate, since the
|
||||
// server sorts on-subnet first), unless the admin already chose one.
|
||||
setBackendIp((cur) => cur || candidates.find((c) => c.onDeviceSubnet)?.ip || "");
|
||||
})
|
||||
.catch(() => {
|
||||
if (live) setBackendIps(null);
|
||||
});
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [testedHost]);
|
||||
|
||||
async function scan() {
|
||||
if (!selected) return;
|
||||
setScanning(true);
|
||||
@@ -157,7 +192,13 @@ function CategoryPicker({
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
await assignDevice({ lane, category, driverId: selected.id, config: mergedConfig() });
|
||||
await assignDevice({
|
||||
lane,
|
||||
category,
|
||||
driverId: selected.id,
|
||||
config: mergedConfig(),
|
||||
...(backendIp ? { backendIp } : {}),
|
||||
});
|
||||
setSaved(true);
|
||||
} catch (e) {
|
||||
setSaveError((e as Error).message);
|
||||
@@ -260,6 +301,36 @@ function CategoryPicker({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backend push IP — only for push-capable devices (candidates present).
|
||||
Pre-filled with the auto-pick; editable for multi-NIC hosts. */}
|
||||
{backendIps && backendIps.length > 0 && (
|
||||
<div style={{ margin: "0.5rem 0 0" }}>
|
||||
<label>
|
||||
Backend push IP{" "}
|
||||
<select value={backendIp} onChange={(e) => setBackendIp(e.target.value)}>
|
||||
{!backendIps.some((c) => c.onDeviceSubnet) && (
|
||||
<option value="" disabled>
|
||||
Choose an address…
|
||||
</option>
|
||||
)}
|
||||
{backendIps.map((c) => (
|
||||
<option key={c.ip} value={c.ip}>
|
||||
{c.ip} ({c.iface}){c.onDeviceSubnet ? " — on device subnet" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{!backendIps.some((c) => c.onDeviceSubnet) && (
|
||||
<span style={{ marginLeft: 8, color: "#d97706" }}>
|
||||
⚠ no NIC on the device's subnet — the device may not reach the backend
|
||||
</span>
|
||||
)}
|
||||
<p style={{ margin: "0.25rem 0 0", color: "#666", fontSize: "0.85em" }}>
|
||||
The address this device will POST input events to.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{saveError && <p style={{ color: "crimson", margin: "0.5rem 0 0" }}>Save failed: {saveError}</p>}
|
||||
{saved && <p style={{ color: "#16a34a", margin: "0.5rem 0 0" }}>Saved and configured ✓</p>}
|
||||
</div>
|
||||
|
||||
@@ -136,11 +136,27 @@ export function testDevice(driverId: string, config: DeviceConfig): Promise<Test
|
||||
});
|
||||
}
|
||||
|
||||
export interface BackendIpCandidate {
|
||||
ip: string;
|
||||
iface: string;
|
||||
onDeviceSubnet: boolean;
|
||||
}
|
||||
|
||||
/** Local IPs the device could push to (on-subnet first), for the wizard to
|
||||
* pre-fill/override. Matters on multi-NIC hosts. */
|
||||
export function fetchBackendIps(
|
||||
host: string,
|
||||
): Promise<{ candidates: BackendIpCandidate[]; port: number }> {
|
||||
return apiFetch(`/api/setup/backend-ips?host=${encodeURIComponent(host)}`);
|
||||
}
|
||||
|
||||
export interface AssignBody {
|
||||
lane: number;
|
||||
category: DeviceCategory;
|
||||
driverId: string;
|
||||
config: DeviceConfig;
|
||||
/** Backend IP the device should push to (overrides auto-pick). */
|
||||
backendIp?: string;
|
||||
}
|
||||
|
||||
/** Save + configure the device (preconditions, push setup), then persist. */
|
||||
|
||||
Reference in New Issue
Block a user