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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type DeviceCategory,
|
||||
} from "@parking/devices";
|
||||
import { requireRole } from "../auth.js";
|
||||
import { backendIpForDevice, backendPort } from "../net.js";
|
||||
import { backendIpCandidates, backendIpForDevice, backendPort } from "../net.js";
|
||||
|
||||
// First-run setup API. The admin reads the driver catalog and assigns devices
|
||||
// per lane. See wiki/concepts/first-run-setup.md.
|
||||
@@ -22,6 +22,9 @@ interface AssignBody {
|
||||
category: DeviceCategory;
|
||||
driverId: string;
|
||||
config: Record<string, string | number | boolean>;
|
||||
/** Optional: the backend IP the device should push to (overrides auto-pick;
|
||||
* matters on multi-NIC hosts). */
|
||||
backendIp?: string;
|
||||
}
|
||||
|
||||
interface TestBody {
|
||||
@@ -113,6 +116,18 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
},
|
||||
);
|
||||
|
||||
// Candidate backend IPs the device can push to, for a given device host. The
|
||||
// wizard pre-fills with the on-subnet one and lets the admin override (matters
|
||||
// on multi-NIC hosts). See net.ts / wiki/concepts/device-input-flow.md.
|
||||
app.get<{ Querystring: { host?: string } }>(
|
||||
"/api/setup/backend-ips",
|
||||
{ preHandler: adminGuard },
|
||||
async (req) => {
|
||||
const candidates = backendIpCandidates(req.query.host ?? "");
|
||||
return { candidates, port: backendPort() };
|
||||
},
|
||||
);
|
||||
|
||||
// Assign a device to a lane. Validates the chosen driver + config, configures
|
||||
// the device (fix preconditions + set up Digest-authenticated input push — no
|
||||
// manual device-web-UI step by the admin), then persists. Fails the save if
|
||||
@@ -121,7 +136,7 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
"/api/setup/assign",
|
||||
{ preHandler: adminGuard },
|
||||
async (req, reply) => {
|
||||
const { lane, category, driverId, config } = req.body;
|
||||
const { lane, category, driverId, config, backendIp } = req.body;
|
||||
const driver = registry.get(driverId);
|
||||
if (!driver || driver.category !== category) {
|
||||
return reply.code(400).send({ error: `invalid driver for ${category}: ${driverId}` });
|
||||
@@ -162,10 +177,11 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
|
||||
if (hasPushConfig(device)) {
|
||||
const host = String(config.host ?? "");
|
||||
const backendIp = backendIpForDevice(host);
|
||||
if (!backendIp) {
|
||||
// Admin-provided backend IP wins; else auto-derive (on-subnet NIC).
|
||||
const pushHost = backendIp ?? backendIpForDevice(host);
|
||||
if (!pushHost) {
|
||||
return reply.code(400).send({
|
||||
error: `cannot determine the backend IP on the device's subnet (${host}). Set BACKEND_HOST_IP.`,
|
||||
error: `cannot determine the backend IP on the device's subnet (${host}). Pick one in setup or set BACKEND_HOST_IP.`,
|
||||
});
|
||||
}
|
||||
const pushUser = "dingtian";
|
||||
@@ -173,13 +189,16 @@ export async function setupRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
// (longer is silently truncated → auth mismatch), so keep it short.
|
||||
const pushPassword = randomBytes(12).toString("hex");
|
||||
await device.configureInputPush({
|
||||
host: backendIp,
|
||||
host: pushHost,
|
||||
port: backendPort(),
|
||||
pathBase: `/api/devices/${driverId}/${id}/input`,
|
||||
auth: { user: pushUser, password: pushPassword },
|
||||
});
|
||||
fullConfig.pushUser = pushUser;
|
||||
fullConfig.pushPassword = pushPassword;
|
||||
// Record the backend IP the device was told to push to — lets us detect
|
||||
// a later mismatch if the host's IP changes.
|
||||
fullConfig.backendIp = pushHost;
|
||||
}
|
||||
} catch (err) {
|
||||
return reply
|
||||
|
||||
Reference in New Issue
Block a user