feat(editor): F4.5c the DHCP server and the port forwarding in the device sheet

Measured first: SheetRun now types into FILIUS' DHCP dialog and keeps rows
in its port forwarding dialog. Java's DHCP server keeps its bounds as typed
(".0.0.1" too), never takes an empty gateway or DNS server, hands out the
device's gateway and DNS server unless its own (a home router always its
LAN address, where "manual settings" do nothing), drops a second static
assignment of a MAC address silently and lets "_" through its MAC pattern;
the port forwarding dialog keeps any row, its firmware forwards only the
valid ones. Here a value passes its rule or is not taken, with a word why.

- doc: DHCP server addresses and flags, static assignments (a MAC address
  once, ignoring case), port forwarding rows (TCP or UDP, musterPort, the
  LAN address); the sheet shows what the server hands out and which rows
  Java's firmware forwards (the core's parsePortForwardingRow, exported)
- editor: Java's dialogs as pages of the sheet with a back button, the
  three row tables on one RowsEditor (the forwarding table rebuilt on it);
  a sheet per device, so an open page closes when another device is chosen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 12:16:04 +00:00
co-authored by Claude Opus 5.5
parent eff9433673
commit c8edb87210
22 changed files with 1525 additions and 143 deletions
+53
View File
@@ -7,8 +7,12 @@ import * as Y from 'yjs';
import {
defaultSwitchConfiguration,
DEFAULT_SWITCH_RETENTION_MILLISECONDS,
dhcpServerConfiguration,
forwardsIPv4ByDefault,
parsePortForwardingRow,
routeTableRows,
type DhcpServerConfiguration,
type GatewayPortForwardingRow,
stableGeneratedMacAddress,
type HostLabelPolicy,
type ManualRoute,
@@ -20,6 +24,25 @@ import { inOrder, ROOTS } from './projectDoc.js';
import { fromY } from './values.js';
import { nodeLabel } from './view.js';
/** A device's DHCP server as Java's dialog shows it (JDHCPKonfiguration). */
export interface DhcpServerSheet {
readonly isActive: boolean;
readonly lowerBoundIPAddress: string;
readonly upperBoundIPAddress: string;
/** The device's own mask (the LAN card's on a home router), shown, not changed. */
readonly subnetMask: string;
/** What the server hands out (DHCPServer.determineGatewayip): its own setting, the device's gateway (0.0.0.0 without one), a home router's LAN address. */
readonly gateway: string;
readonly gatewayEditable: boolean;
/** determineDnsserverip: its own setting (always on a home router), else the device's DNS server or 0.0.0.0. */
readonly dnsServer: string;
readonly dnsServerEditable: boolean;
readonly useOwnSettings: boolean;
/** A home router's dialog leaves the choice without effect; the sheet offers it on a PC or notebook only. */
readonly ownSettingsOffered: boolean;
readonly staticAssignments: readonly { readonly macAddress: string; readonly ipAddress: string }[];
}
export interface HostSheet {
readonly kind: 'pc' | 'notebook';
readonly name: string;
@@ -40,6 +63,7 @@ export interface HostSheet {
readonly wirelessPossible: boolean;
/** The switches' SSIDs, in the order of the devices, each once: the networks to choose from. */
readonly networks: readonly string[];
readonly dhcpServer: DhcpServerSheet;
}
export interface SwitchSheet {
@@ -89,6 +113,9 @@ export interface HomeRouterSheet {
readonly dhcpClient: boolean;
readonly wan: InterfaceSheet;
readonly lan: InterfaceSheet;
readonly dhcpServer: DhcpServerSheet;
/** The port forwarding rows, each with whether Java's firmware forwards it (GatewayFirmware.validateData). */
readonly portForwarding: readonly (GatewayPortForwardingRow & { readonly forwarded: boolean })[];
}
export interface NameSheet {
@@ -123,6 +150,27 @@ function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSh
});
}
/** The DHCP server's sheet, from the device's own address settings (or a home router's LAN card). */
function dhcpServerSheet(node: Y.Map<unknown>, own: { readonly subnetMask: string; readonly gateway: string; readonly dnsServer: string }, home: boolean): DhcpServerSheet {
const stored = (fromY(settingsOf(node).get('dhcpServer')) ?? {}) as Partial<DhcpServerConfiguration>;
const server = dhcpServerConfiguration(stored);
const unset = (value: string) => (value === '' ? '0.0.0.0' : value);
const ownSettings = !home && server.useOwnSettings;
return {
isActive: server.isActive,
lowerBoundIPAddress: server.lowerBoundIPAddress,
upperBoundIPAddress: server.upperBoundIPAddress,
subnetMask: own.subnetMask,
gateway: home ? own.gateway : ownSettings ? server.gatewayIPAddress : unset(own.gateway),
gatewayEditable: ownSettings,
dnsServer: home || ownSettings ? server.dnsServerIPAddress : unset(own.dnsServer),
dnsServerEditable: home || ownSettings,
useOwnSettings: server.useOwnSettings,
ownSettingsOffered: !home,
staticAssignments: server.staticAssignments.map(({ macAddress, ipAddress }) => ({ macAddress, ipAddress })),
};
}
/** The sheet of a device, or undefined when the document has no such device. */
export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
@@ -160,6 +208,7 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
ssid: (wireless?.get('ssid') as string | undefined) ?? '',
wirelessPossible: isWireless || !plugged,
networks: [...networks],
dhcpServer: dhcpServerSheet(node, { subnetMask: text(device, 'subnetMask'), gateway: text(device, 'defaultGateway'), dnsServer: text(device, 'dnsServer') }, false),
};
}
case 'networkSwitch': {
@@ -192,6 +241,7 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
}
case 'gateway': {
const [wan, lan] = interfacesOf(doc, id, node);
const rows = (fromY(settingsOf(node).get('portForwarding')) ?? []) as unknown as GatewayPortForwardingRow[];
return {
kind,
name,
@@ -200,6 +250,9 @@ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined {
dhcpClient: setting(node, 'dhcpClient')?.get('isEnabled') === true,
wan: wan!,
lan: lan!,
// Java's home router hands out its LAN address as the gateway, with the LAN card's mask.
dhcpServer: dhcpServerSheet(node, { subnetMask: lan!.subnetMask, gateway: lan!.ipAddress, dnsServer: '' }, true),
portForwarding: rows.map((row) => ({ ...row, forwarded: parsePortForwardingRow(row) !== undefined })),
};
}
case 'unsupported':