Files
mdg-filius/packages/doc/src/sheet.ts
T
julian.kiedaischandClaude Opus 5.5 7b943d3d96 feat(server): the relay joins modems of different projects by a code (K3)
A modem that offers a connection to another project is found by six
characters derived from its project and itself (modemCode): a copy of the
project has another code, so no two modems offer the same one. The room
names its modems of other projects to F's relay at a start and none at a
stop; the relay pairs each offer with the first modem that joins its code,
one to one as Java's server modem takes one client, and passes the frames
between the rooms' threads. Every client hears how each modem stands
(`modems`: connected, waiting, taken, unpaired, ambiguous, detached, or
unavailable without F). The document keeps the connection and the code.

Measured on F with two threads: a ping from one project's notebook to the
other's goes through; a third project that wants the same code reads
taken, and takes it when the second stops; a deleted project's modem
leaves with its room.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:24:19 +00:00

297 lines
14 KiB
TypeScript

// What the device sheet shows of a device, read straight from the document:
// the fields of FILIUS' settings panel of its kind (JHostKonfiguration,
// JSwitchKonfiguration, JModemKonfiguration, JVermittlungsrechnerKonfiguration
// with its forwarding table, JGatewayConfiguration).
import * as Y from 'yjs';
import {
defaultSwitchConfiguration,
DEFAULT_SWITCH_RETENTION_MILLISECONDS,
dhcpServerConfiguration,
firewallConfiguration,
forwardsIPv4ByDefault,
type FirewallConfiguration,
parsePortForwardingRow,
routeTableRows,
type DhcpServerConfiguration,
type GatewayPortForwardingRow,
stableGeneratedMacAddress,
type HostLabelPolicy,
type ManualRoute,
type NodeKind,
type RemoteLinkRelay,
type RouteRow,
} from '@mdg-filius/core';
import { modemPort } from '@mdg-filius/fls';
import type { ModemConnection } from './settings.js';
import { inOrder, ROOTS } from './projectDoc.js';
import { fromY } from './values.js';
import { cardAddress, 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;
/** Host.useIPAsName and useMACAsName. */
readonly labels: { readonly ipAddress: boolean; readonly macAddress: boolean };
/** The card's MAC address (the model keeps them in upper case, as Java shows them). */
readonly macAddress: string;
readonly ipAddress: string;
readonly subnetMask: string;
readonly defaultGateway: string;
readonly dnsServer: string;
readonly dhcpClient: boolean;
readonly ipForwarding: boolean;
readonly wireless: boolean;
/** The SSID of the network the card joins, '' for none. */
readonly ssid: string;
/** Java offers the wireless card only while the cable's port is free, or the card is wireless already. */
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 {
readonly kind: 'networkSwitch';
readonly name: string;
readonly ssid: string;
readonly retentionSeconds: number;
readonly cloud: boolean;
}
export interface ModemSheet {
readonly kind: 'remoteLink';
readonly name: string;
/** The pair in the project: two modems on one port. */
readonly port: number;
/** How it finds its partner (setModemConnection). */
readonly connection: ModemConnection;
/** Joining: the code of the modem it connects to ('' while none is given). */
readonly code: string;
}
/** A router's or home router's card. */
export interface InterfaceSheet {
readonly portId: string;
readonly ipAddress: string;
readonly subnetMask: string;
readonly macAddress: string;
/**
* The device at the other end of its cable (its label, as Java names it),
* the address of the card the cable plugs into there (a computer's, a
* router's; null for a switch or a modem, which have none) and the cable;
* null without one.
*/
readonly connected: { readonly device: string; readonly address: string | null; readonly link: string } | null;
}
/** A router's or home router's firewall as Java's dialog shows it (JFirewallDialog). */
export type FirewallSheet = Pick<FirewallConfiguration, 'isActive' | 'dropIcmp' | 'filterSynSegmentsOnly' | 'defaultPolicy' | 'rules'>;
function firewallSheet(node: Y.Map<unknown>): FirewallSheet {
const { isActive, dropIcmp, filterSynSegmentsOnly, defaultPolicy, rules } = firewallConfiguration((fromY(settingsOf(node).get('firewall')) ?? {}) as Partial<FirewallConfiguration>);
return { isActive, dropIcmp, filterSynSegmentsOnly, defaultPolicy, rules };
}
export interface RouterSheet {
readonly kind: 'router';
readonly name: string;
readonly gateway: string;
readonly rip: boolean;
/** Shown, not changed: Java's panel greys it out. */
readonly ipForwarding: boolean;
readonly interfaces: readonly InterfaceSheet[];
/** The manual rows, in their order. */
readonly routes: readonly ManualRoute[];
readonly firewall: FirewallSheet;
/** The whole table as Java shows it (Weiterleitungstabelle.holeTabelle): own addresses and networks, localhost, the manual rows, the gateway. */
readonly table: readonly RouteRow[];
}
export interface HomeRouterSheet {
readonly kind: 'gateway';
readonly name: string;
readonly gateway: string;
readonly ipForwarding: boolean;
/** DHCP for the WAN card. */
readonly dhcpClient: boolean;
readonly wan: InterfaceSheet;
readonly lan: InterfaceSheet;
readonly dhcpServer: DhcpServerSheet;
readonly firewall: FirewallSheet;
/** The port forwarding rows, each with whether Java's firmware forwards it (GatewayFirmware.validateData). */
readonly portForwarding: readonly (GatewayPortForwardingRow & { readonly forwarded: boolean })[];
}
export interface NameSheet {
readonly kind: 'unsupported';
readonly name: string;
}
export type DeviceSheet = HostSheet | SwitchSheet | ModemSheet | RouterSheet | HomeRouterSheet | NameSheet;
const settingsOf = (node: Y.Map<unknown>) => node.get('settings') as Y.Map<unknown>;
const setting = (node: Y.Map<unknown>, key: string) => settingsOf(node).get(key) as Y.Map<unknown> | undefined;
const text = (map: Y.Map<unknown> | undefined, key: string) => (map?.get(key) as string | undefined) ?? '';
/** A router's or home router's cards, each with what its cable joins. */
function interfacesOf(doc: Y.Doc, id: string, node: Y.Map<unknown>): InterfaceSheet[] {
const all = doc.getMap<Y.Map<unknown>>(ROOTS.nodes);
const links = inOrder(doc.getMap<Y.Map<unknown>>(ROOTS.links));
return (node.get('ports') as Y.Array<Y.Map<unknown>>).toArray().map((port) => {
const portId = port.get('id') as string;
const config = port.get('config') as Y.Map<unknown> | undefined;
const cable = links.find(([, link]) => (link.get('sourceNode') === id && link.get('sourcePort') === portId) || (link.get('targetNode') === id && link.get('targetPort') === portId));
const near = cable !== undefined && cable[1].get('sourceNode') === id && cable[1].get('sourcePort') === portId;
const other = cable === undefined ? undefined : all.get(cable[1].get(near ? 'targetNode' : 'sourceNode') as string);
return {
portId,
ipAddress: text(config, 'ipAddress'),
subnetMask: text(config, 'subnetMask'),
macAddress: (port.get('mac') as string | undefined) ?? stableGeneratedMacAddress(portId),
connected: cable === undefined || other === undefined ? null : { device: nodeLabel(other), address: cardAddress(other, cable[1].get(near ? 'targetPort' : 'sourcePort') as string), link: cable[0] },
};
});
}
/** 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);
const node = all.get(id);
if (node === undefined) return undefined;
const kind = node.get('kind') as NodeKind;
const name = node.get('name') as string;
switch (kind) {
case 'pc':
case 'notebook': {
const device = setting(node, 'device');
const wireless = setting(node, 'wireless');
const port = (node.get('ports') as Y.Array<Y.Map<unknown>>).get(0);
const portId = port?.get('id') as string | undefined;
const plugged = [...doc.getMap<Y.Map<unknown>>(ROOTS.links).values()].some((link) => link.get('sourceNode') === id || link.get('targetNode') === id);
const policy = node.get('labelPolicy') as HostLabelPolicy;
const isWireless = wireless?.get('isEnabled') === true;
const networks = new Set<string>();
for (const [other, candidate] of inOrder(all)) {
if (candidate.get('kind') !== 'networkSwitch') continue;
networks.add((setting(candidate, 'switch')?.get('ssid') as string | undefined) ?? defaultSwitchConfiguration(other).ssid);
}
return {
kind,
name,
labels: { ipAddress: policy === 'ipAddress' || policy === 'ipAndMAC', macAddress: policy === 'macAddress' || policy === 'ipAndMAC' },
macAddress: (port?.get('mac') as string | undefined) ?? (portId === undefined ? '' : stableGeneratedMacAddress(portId)),
ipAddress: (device?.get('ipAddress') as string | undefined) ?? '',
subnetMask: (device?.get('subnetMask') as string | undefined) ?? '',
defaultGateway: (device?.get('defaultGateway') as string | undefined) ?? '',
dnsServer: (device?.get('dnsServer') as string | undefined) ?? '',
dhcpClient: setting(node, 'dhcpClient')?.get('isEnabled') === true,
ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind),
wireless: isWireless,
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': {
const config = setting(node, 'switch');
return {
kind,
name,
ssid: (config?.get('ssid') as string | undefined) ?? defaultSwitchConfiguration(id).ssid,
// Whole seconds, as Java's panel shows them (retentionTime / 1000 in long arithmetic).
retentionSeconds: Math.trunc(((config?.get('retentionTimeMilliseconds') as number | undefined) ?? DEFAULT_SWITCH_RETENTION_MILLISECONDS) / 1000),
cloud: config?.get('showsAsCloud') === true,
};
}
case 'remoteLink': {
const config = setting(node, 'remoteLink');
const relay = fromY(config?.get('relay')) as RemoteLinkRelay | undefined;
return {
kind,
name,
port: modemPort(config?.get('pairIdentifier') as string | undefined),
connection: relay?.role ?? 'inProject',
code: relay?.role === 'join' ? relay.code : '',
};
}
case 'router': {
const interfaces = interfacesOf(doc, id, node);
const gateway = text(setting(node, 'device'), 'defaultGateway');
const routes = (fromY(settingsOf(node).get('routes')) ?? []) as unknown as ManualRoute[];
return {
kind,
name,
gateway,
rip: settingsOf(node).get('rip') === true,
ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind),
interfaces,
routes,
table: routeTableRows(interfaces, routes, gateway),
firewall: firewallSheet(node),
};
}
case 'gateway': {
const [wan, lan] = interfacesOf(doc, id, node);
const rows = (fromY(settingsOf(node).get('portForwarding')) ?? []) as unknown as GatewayPortForwardingRow[];
return {
kind,
name,
gateway: text(setting(node, 'device'), 'defaultGateway'),
ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind),
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),
firewall: firewallSheet(node),
portForwarding: rows.map((row) => ({ ...row, forwarded: parsePortForwardingRow(row) !== undefined })),
};
}
case 'unsupported':
return { kind, name };
}
}