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:
2026-06-14 18:47:23 +02:00
parent 7fd407ac82
commit 382c32f2bc
4 changed files with 152 additions and 7 deletions
+39
View File
@@ -28,3 +28,42 @@ export function backendIpForDevice(deviceHost: string): string | null {
export function backendPort(): number {
return Number(process.env.PORT ?? 3000);
}
export interface BackendIpCandidate {
ip: string;
iface: string;
/** True if this interface's subnet contains the device IP (the likely one). */
onDeviceSubnet: boolean;
}
/**
* List local IPv4 addresses the device could call back on, with the ones on the
* device's own subnet flagged + sorted first. Lets the admin see/override the
* auto-pick (important on multi-NIC hosts). BACKEND_HOST_IP, if set, is the only
* candidate (the deterministic override).
*/
export function backendIpCandidates(deviceHost: string): BackendIpCandidate[] {
if (process.env.BACKEND_HOST_IP) {
return [{ ip: process.env.BACKEND_HOST_IP, iface: "BACKEND_HOST_IP", onDeviceSubnet: true }];
}
const dev = deviceHost.split(".").map(Number);
const validDev = dev.length === 4 && !dev.some((o) => Number.isNaN(o));
const out: BackendIpCandidate[] = [];
for (const [iface, ifaces] of Object.entries(networkInterfaces())) {
for (const i of ifaces ?? []) {
if (i.family !== "IPv4" || i.internal) continue;
const addr = i.address.split(".").map(Number);
const mask = i.netmask.split(".").map(Number);
const onDeviceSubnet =
validDev &&
addr.length === 4 &&
mask.length === 4 &&
dev.every((o, k) => (o & mask[k]!) === (addr[k]! & mask[k]!));
out.push({ ip: i.address, iface, onDeviceSubnet });
}
}
// On-subnet candidates first.
return out.sort((a, b) => Number(b.onDeviceSubnet) - Number(a.onDeviceSubnet));
}