feat(editor): F4.5b the device sheet for routers and home routers
Measured first: SheetRun now types into FILIUS' router and home router
panels, records five forwarding tables (holeTabelle) and steps in the
panel's table. Java turns a card address that passes the pattern but not
IP.ipCheck (".0.0.1") into null and the router panel throws; an empty
gateway is marked red yet taken; a refused table cell is stored as null;
a home router's DHCP client leaves its DHCP server running. None of the
nulls is adopted. The core's routeTableRows matches all five tables.
- doc: card addresses (the first card mirrored in the device's settings),
router gateway, RIP, manual routes (a new row once all four cells pass,
a cell, removal); the sheet reads cards with what their cable joins and
the whole table
- editor: Java's tabs (general, each card titled by its address, the
forwarding table greyed out under RIP; general, LAN, WAN on a home
router); the open card's cable drawn active in Java's green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
// What the device sheet does to the document: a device's settings changed as
|
||||
// FILIUS' settings panels change them (JHostKonfiguration,
|
||||
// JSwitchKonfiguration, JModemKonfiguration; measured with SheetRun), each a
|
||||
// transaction with the person's origin, like the design tools' commands.
|
||||
// JSwitchKonfiguration, JModemKonfiguration, JVermittlungsrechnerKonfiguration
|
||||
// with its JWeiterleitungsTabelle, JGatewayConfiguration; measured with
|
||||
// SheetRun), each a transaction with the person's origin, like the design
|
||||
// tools' commands.
|
||||
//
|
||||
// A text field's setter takes the text only when the field's rule does
|
||||
// (fields.ts) and says so; the device keeps its value otherwise. A host's
|
||||
@@ -15,9 +17,12 @@
|
||||
// search for the access point (updateWifiConnection) keeps the last switch
|
||||
// with the SSID it passes until a device that is neither comes after the
|
||||
// host — which switch depends on unrelated devices; here it is the first.
|
||||
// Nor: an interface address that passes the panel's pattern but not
|
||||
// IP.ipCheck (".0.0.1") becomes null in Java, and a router's panel then
|
||||
// throws; a table cell Java refuses is stored as null. Here neither is taken.
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, defaultSwitchConfiguration, forwardsIPv4ByDefault, type Id, type NodeKind } from '@mdg-filius/core';
|
||||
import { DEFAULT_REMOTE_LINK_LATENCY_MILLISECONDS, defaultSwitchConfiguration, forwardsIPv4ByDefault, type Id, type ManualRoute, type NodeKind } from '@mdg-filius/core';
|
||||
import { modemPair } from '@mdg-filius/fls';
|
||||
import { connect, type ConnectError } from './commands.js';
|
||||
import { checkField, type CheckedField } from './fields.js';
|
||||
@@ -86,7 +91,11 @@ export function setIpForwarding(doc: Y.Doc, id: Id, on: boolean, origin: unknown
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** DHCP for the configuration; switched on, it stops the device's own DHCP server (JHostKonfiguration.aenderungenAnnehmen). */
|
||||
/**
|
||||
* DHCP for the configuration. On a PC or notebook it stops the device's own
|
||||
* DHCP server (JHostKonfiguration.aenderungenAnnehmen); a home router's
|
||||
* panel (its WAN card) leaves its server running, as Java does.
|
||||
*/
|
||||
export function setDhcpClient(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
@@ -96,7 +105,7 @@ export function setDhcpClient(doc: Y.Doc, id: Id, on: boolean, origin: unknown):
|
||||
return;
|
||||
}
|
||||
settingsOf(node).set('dhcpClient', toY({ isEnabled: true }));
|
||||
(settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined)?.set('isActive', false);
|
||||
if (kindOf(node) === 'pc' || kindOf(node) === 'notebook') (settingsOf(node).get('dhcpServer') as Y.Map<unknown> | undefined)?.set('isActive', false);
|
||||
}, origin);
|
||||
}
|
||||
|
||||
@@ -196,3 +205,117 @@ export function setModemPort(doc: Y.Doc, id: Id, text: string, origin: unknown):
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
const portsOf = (node: Y.Map<unknown>) => node.get('ports') as Y.Array<Y.Map<unknown>>;
|
||||
|
||||
/**
|
||||
* A router's or home router's interface: its address or mask
|
||||
* (NetzwerkInterface.setIp, setSubnetzMaske). The device's own settings
|
||||
* mirror its first interface, as the project keeps them (the home router's
|
||||
* WAN card).
|
||||
*/
|
||||
export function setInterfaceAddress(doc: Y.Doc, id: Id, portId: Id, field: 'ipAddress' | 'subnetMask', text: string, origin: unknown): CheckedField {
|
||||
const node = nodes(doc).get(id);
|
||||
const ports = node === undefined ? [] : portsOf(node).toArray();
|
||||
const index = ports.findIndex((port) => port.get('id') === portId);
|
||||
if (index < 0) return checkField(field, text);
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField(field, text),
|
||||
(device, value) => {
|
||||
const port = ports[index]!;
|
||||
let config = port.get('config') as Y.Map<unknown> | undefined;
|
||||
if (config === undefined) {
|
||||
config = toY({ ipAddress: '192.168.0.10', subnetMask: '255.255.255.0' }) as Y.Map<unknown>;
|
||||
port.set('config', config);
|
||||
}
|
||||
config.set(field, value);
|
||||
if (index === 0) (settingsOf(device).get('device') as Y.Map<unknown> | undefined)?.set(field, value);
|
||||
},
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A router's or home router's standard gateway (setStandardGateway: the
|
||||
* gateway of every card), empty for none. A router keeps its own settings
|
||||
* only while it has a gateway, as the project keeps them.
|
||||
*/
|
||||
export function setRouterGateway(doc: Y.Doc, id: Id, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(
|
||||
doc,
|
||||
id,
|
||||
checkField('ipAddressOrEmpty', text),
|
||||
(node, value) => {
|
||||
const first = portsOf(node).get(0)?.get('config') as Y.Map<unknown> | undefined;
|
||||
const device = settingMap(node, 'device', {
|
||||
ipAddress: (first?.get('ipAddress') as string | undefined) ?? '',
|
||||
subnetMask: (first?.get('subnetMask') as string | undefined) ?? '',
|
||||
defaultGateway: '',
|
||||
dnsServer: '',
|
||||
});
|
||||
device.set('defaultGateway', value);
|
||||
if (kindOf(node) === 'router' && value === '' && device.get('dnsServer') === '') settingsOf(node).delete('device');
|
||||
},
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
/** RIP on a router, kept only when on (as the project keeps it). */
|
||||
export function setRip(doc: Y.Doc, id: Id, on: boolean, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
if (node === undefined) return;
|
||||
doc.transact(() => {
|
||||
if (on) settingsOf(node).set('rip', true);
|
||||
else settingsOf(node).delete('rip');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
/** A column of the forwarding table's manual rows. */
|
||||
export type RouteColumn = keyof ManualRoute;
|
||||
|
||||
export const ROUTE_COLUMNS: readonly RouteColumn[] = ['destinationNetwork', 'subnetMask', 'gateway', 'interfaceIPAddress'];
|
||||
|
||||
/** Each column's rule (JWeiterleitungsTabelle: musterIpAdresse, musterSubNetz for the mask). */
|
||||
const routeRule = (column: RouteColumn) => (column === 'subnetMask' ? 'subnetMask' : 'ipAddress');
|
||||
|
||||
function routesOf(node: Y.Map<unknown>): Y.Array<Y.Map<unknown>> {
|
||||
let routes = settingsOf(node).get('routes') as Y.Array<Y.Map<unknown>> | undefined;
|
||||
if (routes === undefined) {
|
||||
routes = new Y.Array<Y.Map<unknown>>();
|
||||
settingsOf(node).set('routes', routes);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* A new manual row, taken once all four cells pass (Java keeps a new row in
|
||||
* the table until Weiterleitungstabelle.validate takes it); the columns
|
||||
* that do not pass otherwise.
|
||||
*/
|
||||
export function addRoute(doc: Y.Doc, id: Id, cells: Readonly<Record<RouteColumn, string>>, origin: unknown): { readonly route: ManualRoute } | { readonly invalid: readonly RouteColumn[] } {
|
||||
const checked = ROUTE_COLUMNS.map((column) => [column, checkField(routeRule(column), cells[column])] as const);
|
||||
const invalid = checked.filter(([, each]) => 'invalid' in each).map(([column]) => column);
|
||||
const node = nodes(doc).get(id);
|
||||
if (invalid.length > 0 || node === undefined) return { invalid };
|
||||
const route = Object.fromEntries(checked.map(([column, each]) => [column, (each as { value: string }).value])) as unknown as ManualRoute;
|
||||
doc.transact(() => routesOf(node).push([toY({ ...route }) as Y.Map<unknown>]), origin);
|
||||
return { route };
|
||||
}
|
||||
|
||||
/** A cell of a manual row (by its place among the manual rows). */
|
||||
export function setRouteCell(doc: Y.Doc, id: Id, index: number, column: RouteColumn, text: string, origin: unknown): CheckedField {
|
||||
return setChecked(doc, id, checkField(routeRule(column), text), (node, value) => (settingsOf(node).get('routes') as Y.Array<Y.Map<unknown>> | undefined)?.get(index)?.set(column, value), origin);
|
||||
}
|
||||
|
||||
/** Removes a manual row (by its place among the manual rows); the list goes with its last row, as the project keeps it. */
|
||||
export function removeRoute(doc: Y.Doc, id: Id, index: number, origin: unknown): void {
|
||||
const node = nodes(doc).get(id);
|
||||
const routes = node === undefined ? undefined : (settingsOf(node).get('routes') as Y.Array<Y.Map<unknown>> | undefined);
|
||||
if (node === undefined || routes === undefined || index < 0 || index >= routes.length) return;
|
||||
doc.transact(() => {
|
||||
routes.delete(index, 1);
|
||||
if (routes.length === 0) settingsOf(node).delete('routes');
|
||||
}, origin);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user