# React Networks - API Reference (Embedded) This file documents the API of the `@mp70/react-networks` package (packages/network-diagrams). Public React components are `NetworkDiagram`, `FibreFlowMap`, `DiagramErrorBoundary`, and `OtdrTraceStrip`; utilities, types, and constants below remain as documented. ## Entry points The package has six entry points. `./otdr` is a React client entry with no React Flow; the four other subpaths below `.` are server-safe (zero React/React Flow at runtime): - `@mp70/react-networks` (`.`) — React components (`NetworkDiagram`, `FibreFlowMap`, `DiagramErrorBoundary`, `OtdrTraceStrip`) plus diagram utilities, hooks, and types. - `@mp70/react-networks/otdr` — the OTDR loss-budget strip alone (`OtdrTraceStrip`, `formatOtdrDistance`, `otdrEventKindLabel`, `otdrTraceStripPalette`, `OtdrTraceStrip*` types). Needs React, pulls no React Flow. Same symbols as the root entry; nothing is deprecated. - `@mp70/react-networks/model` — pure data model: core types, the handle-id grammar, port-contract, port-position, document adapters, path-layout, and the fibre connection policy (`classifyFibreEndpoint`, `evaluateFibreConnectionRule`, `normalizeHandle`, `FibreEndpointClass`). - `@mp70/react-networks/netbox` — NetBox 4.5 REST converters (`netboxToDiagramDocument`, `netboxToNetworkDiagram`, `netboxRackToNetworkNode`, …) + `NetBox*` DTO types. Input is `{ racks, devices, cables, deviceTypes?, interfaces? }` as returned by `/api/dcim//`; pass `deviceTypes` for device heights (a 4.5 device nests only a brief device type) and `interfaces` with `includeInterfacePorts: true` for port-level cables. - `@mp70/react-networks/inventory` — generic CMDB/inventory converters (`inventoryToDiagramDocument`, `inventoryToNetworkDiagram`, `generateLayout`, …) + `Inventory*DTO` types. - `@mp70/react-networks/validation` — the power validation engine (`runPowerValidation`, `PowerRule` / `PowerIssue`, the graph builder, IEC 60320 connector tables) plus the **opt-in** `standardPowerRules` preset. The engine ships zero default rules. Peer dependencies: `react` (`^18 || ^19`), `react-dom`, and `@xyflow/react` (`^12`). Consumers must import both `@xyflow/react/dist/style.css` and `@mp70/react-networks/index.css`. > Only `NetworkDiagram`, `FibreFlowMap`, `DiagramErrorBoundary`, and `OtdrTraceStrip` are > importable React components. Since 0.9.0, individual node/edge components > (`RackNode`, `DeviceNode`, `FiberNode`, `FiberEdge`, `PowerEdge`, `VerticalPDU`, > splice / tube / cable / coupler / patch-panel / closure / fibre-split nodes, …) > are registered **internally** by `NetworkDiagram` / `FibreFlowMap` and are **no > longer exported**. Per-component tables below are retained as a reference for > `node.data` / `edge.data` shapes; the `import { RackNode }` lines in those > sections are historical and do not resolve. # API Reference ## Components ### DiagramErrorBoundary Error boundary that catches errors in the diagram tree (e.g. React Flow, custom nodes) and renders a fallback instead of crashing the app. Wrap with this for production. ```tsx import { DiagramErrorBoundary } from '@mp70/react-networks'; ``` #### Props (DiagramErrorBoundaryProps) | Prop | Type | Default | Description | |------|------|---------|-------------| | `children` | `ReactNode` | - | Content to render (e.g. NetworkDiagram) | | `fallback` | `ReactNode \| (error: Error, reset: () => void) => ReactNode` | - | Optional fallback UI when an error occurs | | `onError` | `(error: Error, errorInfo: ErrorInfo) => void` | - | Optional callback when an error is caught (e.g. for logging) | #### Example ```tsx (

Diagram failed: {error.message}

)} onError={(err, info) => logToService(err, info)} >
``` ### NetworkDiagram The main component for rendering interactive network diagrams. ```tsx import { NetworkDiagram } from '@mp70/react-networks'; ``` #### Props (NetworkDiagramProps) | Prop | Type | Default | Description | |------|------|---------|-------------| | `nodes` | `NetworkNode[]` | `[]` | Controlled nodes array | | `edges` | `NetworkEdge[]` | `[]` | Controlled edges array | | `onNodesChange` | `(nodes: NetworkNode[]) => void` | - | Callback when nodes change (controlled mode) | | `onEdgesChange` | `(edges: NetworkEdge[]) => void` | - | Callback when edges change (controlled mode) | | `initialNodes` | `NetworkNode[]` | `[]` | Initial nodes for uncontrolled mode | | `initialEdges` | `NetworkEdge[]` | `[]` | Initial edges for uncontrolled mode | | `debug` | `boolean` | `false` | Enable debug mode | | `onNodeClick` | `(node: NetworkNode \| null) => void` | - | Callback when a node is clicked or selection changes (null when deselected) | | `onEdgeClick` | `(edge: NetworkEdge) => void` | - | Callback when an edge is clicked | | `nodeTypes` | `NodeTypes` | - | Custom React Flow node types to merge with defaults | | `edgeTypes` | `EdgeTypes` | - | Custom React Flow edge types to merge with defaults | | `onConnect` | `(connection: Connection) => void` | - | Optional onConnect override (React Flow connection handler) | | `onEdgeUpdate` | `(oldEdge: Edge, newConnection: Connection) => void` | - | Optional onEdgeUpdate override | | `isValidConnection` | `(connection: Connection) => boolean` | - | Optional isValidConnection override | | `connectionMode` | `ConnectionMode` | - | Optional connection mode override | | `onFaceChange` | `(nodeId: string, face: DeviceFace) => void` | - | Callback when device face changes | | `onRackFaceChange` | `(rackId: string, face: DeviceFace) => void` | - | Callback when rack face changes | | `className` | `string` | - | CSS class name | | `style` | `React.CSSProperties` | - | Inline styles | | `alignRacksToBottom` | `boolean` | `false` | If true, vertically align rack bottoms to the same baseline | | `colorMode` | `'light' \| 'dark' \| 'system'` | - | Color mode for the diagram | | `onInit` | `(instance: any) => void` | - | Callback when React Flow instance is initialized | | `onFlowMethods` | `(methods: { toObject, setViewport, setNodes, setEdges, toImage, getNodes, getEdges, fitView }) => void` | - | Callback to get React Flow methods for save/restore | | `showDownloadButton` | `boolean` | - | Show download button | | `downloadButtonPosition` | `'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left'` | - | Download button position | | `downloadButtonText` | `string` | - | Custom download button text | | `downloadButtonStyle` | `React.CSSProperties` | - | Download button styling options | | `useSmoothstepEdgesForTubes` | `boolean` | - | Use smoothstep edges for tube connections. Default is fiber. | | `interaction` | `NetworkDiagramInteractionOptions` | - | Fine-grained interaction controls for editing, pan, zoom, minimap, and controls | | `fitViewOptions` | `FitViewOptions` | - | Optional fitView options passed to React Flow | #### Example ```tsx console.log('Node clicked:', node)} onEdgeClick={(edge) => console.log('Edge clicked:', edge)} onFaceChange={(nodeId, face) => console.log(`Node ${nodeId} face: ${face}`)} onRackFaceChange={(rackId, face) => console.log(`Rack ${rackId} face: ${face}`)} /> ``` #### Controlled mode and persistent layout corrections When both `nodes` and `edges` are supplied the diagram runs controlled: `onNodesChange`/`onEdgesChange` receive the full next array for every persistent change and the consumer is expected to apply it to its own state. The diagram also derives persistent layout corrections from node data — a coupler inside a patch panel gaining `extent: 'parent'`, a splice placed from `holderPosition`, a racked device placed from `uPosition` (authoritative: a stored y that disagrees with the device's U slot is snapped to it on load and whenever `parentId`/`uPosition`/`uHeight` change; x is kept), a vertical PDU clamped inside its rack, and rack-baseline alignment when `alignRacksToBottom` is set. Corrections are committed through the same pipeline as explicit edits, so **each distinct correction produces exactly one `onNodesChange` call** (and one `onDocumentChange` emission in document mode) carrying the corrected array. Precedence after that single notification: - Accepting the notified array is stable — no further notifications. - A controlled value that keeps disagreeing with a notified correction is treated as authoritative: the correction is neither re-applied nor re-notified, so stale persistent state cannot create a notification loop. - Removing a node drops its notification history; re-adding it can notify again. Transient rendering state — measured dimensions and selection synchronization — never produces these persistence notifications; document-mode `onDocumentChange` additionally dirty-checks serialized content before emitting. ### RackNode Rack visualization component with U positioning support. Used as a React Flow node type; props come from React Flow NodeProps. Documented here is the node **data** shape. ```tsx import { RackNode } from '@mp70/react-networks'; ``` RackNode receives React Flow NodeProps; the main configuration is in **node.data** (see table below). Face callbacks are provided by NetworkDiagram. #### Node data (RackNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | - | Rack display label | | `uHeight` | `number` | - | Rack height in U units | | `face` | `'front' \| 'rear'` | - | Current face (front or rear) | | `uNumberingDirection` | `'bottom-up' \| 'top-down'` | - | U numbering direction | | `customHeaderText` | `string` | - | Custom header text for rack | | `rackWidthPx` | `number` | - | Optional rack width in pixels (default from constants) | ### DeviceNode Device visualization component with port management. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { DeviceNode } from '@mp70/react-networks'; ``` #### Node data (DeviceNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | - | Device display label | | `ports` | `PortBlock[]` | - | Front port blocks | | `rearPorts` | `PortBlock[]` | - | Rear port blocks | | `face` | `'front' \| 'rear'` | - | Current face | | `uPosition` | `number` | - | U position within rack | | `width` | `Width` | - | Full or half width | | `powerSide` | `'left' \| 'right'` | - | Power port side for rear face | | `status` | `'active' \| 'inactive' \| 'maintenance'` | - | Device status | | `frontImageUrl` | `string` | - | Optional front background image URL | | `rearImageUrl` | `string` | - | Optional rear background image URL | | `frontImageAttribution` | `string` | - | Optional attribution URL for front image | | `rearImageAttribution` | `string` | - | Optional attribution URL for rear image | | `imageFit` | `'cover' \| 'contain' \| 'fill' \| 'none' \| 'scale-down'` | - | Background image sizing | | `imagePosition` | `string` | - | Background position | | `imageRepeat` | `string` | - | Background repeat | | `stretchToFit` | `boolean` | - | When true, stretch device image to fill bounds | ### FiberNode Fiber connection point component. Internal since 0.9.0 — not importable; configuration is in **node.data** on a `NetworkNode` of type `'fiber'`. #### Node data (FiberNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | - | Fiber node label | | `status` | `'active' \| 'inactive' \| 'maintenance'` | `'active'` | Status for border/indicator color | | `type` | `'single-mode' \| 'multi-mode'` | - | Fiber type (if used) | | `connections` | `string[]` | - | Connected node IDs (if used) | ### FiberEdge Renders a fiber edge (single-fiber or ribbon). Used as a React Flow edge type; configuration is in **edge.data**. ```tsx import { FiberEdge } from '@mp70/react-networks'; ``` #### Edge data (NetworkEdge.data for type 'fiber') | Field | Type | Default | Description | |-------|------|---------|-------------| | `color` | `string` | - | Edge stroke color | | `fiberId` | `number` | - | Single fiber ID (1-24) for traditional edges | | `ribbonFiberIds` | `number[]` | - | Array of fiber IDs (6-12) for ribbon edges; when present edge is ribbon | | `isRibbon` | `boolean` | - | Convenience flag for ribbon fiber edge | | `highlight` | `boolean` | - | Highlight edge with pulsing stroke | | `highlightColor` | `string` | - | Override highlight color | | `highlightWidth` | `number` | - | Override highlight max stroke width | | `bandwidth` | `string` | - | Optional label | | `length` | `number` | - | Cable length | | `kind` | `'fiber' \| 'power' \| 'network'` | - | Edge kind | ### PowerEdge Renders a power cable edge (C13/C19 styling). Internal since 0.9.0 — not importable; configuration is in **edge.data** on a `NetworkEdge` of type `'power'`. #### Edge data (NetworkEdge.data for type 'power') | Field | Type | Default | Description | |-------|------|---------|-------------| | `connectorType` | `'C13' \| 'C19'` | `'C13'` | Connector type (C19 = thicker solid line, C13 = dashed) | | `color` | `string` | - | Edge stroke color (default amber) | | `energized` | `boolean` | - | Energisation overlay: `true` draws the live styling + flow arrow, `false` the de-energised styling, omitted = no overlay | | `flowDirection` | `'source-to-target' \| 'target-to-source'` | - | Direction of the flow arrow, relative to the edge's own endpoints | Set both fields with `applyEnergizationToEdges` (see *Power boards and energisation*) rather than by hand. ### DistributionBoardNode Renders a distribution board (consumer unit / panelboard) as a faceplate of breaker ways. Internal — place a `NetworkNode` of type `'distribution-board'`. Boards are **free-floating by default**: `distribution-board` is deliberately not in `RACK_ASSIGNABLE_NODE_TYPES`; opt a board into rack assignment with `rackMountable: true`. #### Node data (DistributionBoardNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `ways` | `BreakerWay[]` | - | Breaker ways, rendered top-to-bottom in column order. Each way mounts exactly one power handle (`buildBreakerWayHandleId`) | | `wayState` | `Record` | - | Per-way switch/health state keyed by way id | | `voltageV` | `number` | - | Nominal board voltage (e.g. 230, 400) | | `codeMode` | `'NEC' \| 'IEC'` | - | Code vocabulary consumer rules read the board under | | `maxUtilization` | `number` | `80` | Continuous-load derate ceiling, percent | | `rackMountable` | `boolean` | `false` | Opt-in flag marking the board rack-mountable | | `wayColumns` | `number` | `1` | Way columns in the generated faceplate | **BreakerWay:** `{ id, label, poles?, ratingA?, phase?: 'L1' \| 'L2' \| 'L3' \| '3P', position? }` **BreakerWayInstanceState:** `{ switchState: 'closed' \| 'open' \| 'tripped'; status? }` Device nodes gained the matching load fields: `maximumDrawW` (nameplate watts, NetBox `maximum_draw`), `allocatedDrawW` (budgeted watts, NetBox `allocated_draw`), and `expectedFeeds` (`1 \| 2` — `2` marks an A+B device, so losing one feed is `degraded` rather than unpowered). ### TubeNode Tube node representing a buffer tube with fiber handles. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { TubeNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `fibersPerTube` | `number` | `12` | Number of fibers (1-24) | | `tubeIndex` | `number` | `1` | Tube index for TIA-598 color (1-12 cycle) | | `startOn` | `'left-center' \| 'left' \| 'right'` | `'left-center'` | Handle layout: left-center = handles on top/bottom, left/right = side handles | | `mode` | `'ribbon'` | - | Set for ribbon mode | | `ribbon` | `boolean` | - | Alternative flag for ribbon mode | ### CableNode Multi-fiber cable node (1-24 fibers) with colored handles. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { CableNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `fiberCount` | `number` | `12` | Number of fibers (1-24) | | `cableId` | `string` | `'CBL-002'` | Cable identifier for label | | `label` | `string` | - | Primary label (default: e.g. "12f Cable") | | `color` | `string` | - | Cable border/handle color | | `rotation` | `number` | `0` | Rotation in degrees (0 = top, 90 = right, etc.) | ### MultiTubeCableNode Multi-tube cable node (1-24 tubes). Used as a React Flow node type; configuration is in **node.data**. ```tsx import { MultiTubeCableNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `tubeCount` | `number` | `12` | Number of tubes (1-24) | | `fibersPerTube` | `number` | `12` | Fibers per tube | | `fiberCount` | `number` | - | Total fiber count (default: tubeCount * fibersPerTube) | | `cableId` | `string` | `'CBL-002'` | Cable identifier | | `label` | `string` | - | Primary label | | `tubeStartIndex` | `number` | `1` | First tube index for TIA-598 colors | | `color` | `string` | - | Cable border color | ### CouplerNode Coupler/splice panel node with left/right ports. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { CouplerNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `connector` | `'SC' \| 'LC'` | `'LC'` | Connector type | | `couplerFiberCount` | `1 \| 2 \| 4` | `2` | Number of fibers (ports) | | `label` | `string` | - | Label | | `portLabelsLeft` | `string[]` | - | Optional port labels left side | | `portLabelsRight` | `string[]` | - | Optional port labels right side | | `slotPosition` | `number` | - | Slot position inside panel (1-based) | ### PatchPanelNode Container node for patch panels; dimensions can be explicit or auto from children. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { PatchPanelNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | `'Patch Panel'` | Panel label | | `header` | `string` | - | Header text | | `panelWidth` | `number` | - | Explicit width in pixels (else calculated from children) | | `panelHeight` | `number` | - | Explicit height in pixels (else calculated from children) | ### ClosureNode Container node for closures; dimensions can be explicit or auto from children. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { ClosureNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | `'Closure'` | Closure label | | `header` | `string` | - | Header text | | `closureWidth` | `number` | - | Explicit width (or panelWidth) | | `closureHeight` | `number` | - | Explicit height (or panelHeight) | | `panelWidth` | `number` | - | Alternative to closureWidth | | `panelHeight` | `number` | - | Alternative to closureHeight | ### FibreSplitNode Fiber splitter or WDM node (trapezoid: narrow input, wider outputs). Used as a React Flow node type; configuration is in **node.data**. ```tsx import { FibreSplitNode } from '@mp70/react-networks'; ``` #### Node data | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | - | Label (default "Splitter" or "WDM") | | `splitterType` | `'splitter' \| 'WDM'` | `'splitter'` | Type of splitter | | `inputCount` | `1 \| 2` | `1` | Number of input fibers on the left | | `outputCount` | `number` | `8` | Number of output fibers on the right (1-48) | ### VerticalPDU ### VerticalPDU Power distribution unit component (vertical C13/C19 ports). Used as a React Flow node type; configuration is in **node.data**. ```tsx import { VerticalPDU } from '@mp70/react-networks'; ``` #### Node data (VerticalPduNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | - | PDU label | | `name` | `string` | - | Equipment name, distinct from the drawn label | | `status` | `'active' \| 'inactive' \| 'maintenance'` | `'active'` | Status for styling | | `heightCm` | `number` | `100` | Height in centimeters | | `powerPorts` | `number` | `36` | Number of outlets (cycle: 8×C13, 2×C19) | | `portTypes` | `Array<'C13' \| 'C19'>` | - | Explicit outlet connector sequence; overrides the cycle | | `pduPortGroups` | `PortBlock[]` | - | Grouped outlets with custom ids/types/positions | | `widthPx` | `number` | `15` | Width in pixels | | `mgmt_unit` | `'top' \| 'middle' \| 'bottom'` | - | Management unit position | | `boardType` | `'pdu' \| 'rpp' \| 'panel' \| 'busway' \| 'other'` | - | What kind of distribution equipment the strip stands for | | `supply` | `'ac' \| 'dc'` | - | Supply family | | `voltage` | `number` | - | Nominal supply voltage in volts (integer) | | `phases` | `1 \| 3` | - | Supply phase count; outlets attributed round-robin | | `inletConnector` | `string` | - | Free-text inlet connector, e.g. `'IEC 60309 32A'`, `'C20'`, `'hardwired'` | | `inletAmps` | `number` | - | Inlet current rating (the branch-circuit rating) | | `inletGender` | `'male' \| 'female'` | - | Inlet gender (equipment inlets are normally `male`) | | `feedType` | `'primary' \| 'secondary'` | - | A/B designation of the feed landing on the inlet | | `connections` | `string[]` | - | Connected node IDs (if used) | **Handles.** Outlets are bare 1-based ids top-down: `'1'`…`'N'`. The **supply inlet** is a single `target` handle with id `'inlet'` (`PDU_INLET_HANDLE_ID`), mounted at the top of the strip **only when inlet data is present** (`inletConnector` / `inletAmps` / `inletGender`) — a strip without it renders exactly as before and mounts no extra handle. The id is non-numeric by construction and can never collide with an outlet id. **Circuit vs cord.** A distribution-board breaker way terminates a *branch circuit* on the `inlet`; equipment *cords* run from an outlet to a device power inlet. A way landing on a device inlet or a PDU outlet is rejected (`CIRCUIT_TERMINATION_REASON`, and PWR-A13 on an existing graph), and a cord landing on a PDU inlet is rejected (`PDU_INLET_FEED_REASON`). ### SpliceTrayNode Splice tray visualization with holder positioning (12 holders per tray). Used as a React Flow node type; configuration is in **node.data**. ```tsx import { SpliceTrayNode } from '@mp70/react-networks'; ``` #### Node data (SpliceTrayNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | `'Splice Tray'` | Tray label | | `status` | `'active' \| 'inactive' \| 'maintenance'` | - | Optional status | ### SpliceNode Fiber splice component for connecting fiber strands. Used as a React Flow node type; configuration is in **node.data**. ```tsx import { SpliceNode } from '@mp70/react-networks'; ``` #### Node data (SpliceNodeData) | Field | Type | Default | Description | |-------|------|---------|-------------| | `label` | `string` | - | Splice label | | `holderPosition` | `number` | - | Holder position within tray (1-12) | | `lossDb` | `number` | - | Loss value in dB | | `mode` | `'single' \| 'ribbon'` | `'single'` | Splice mode | | `ribbonFiberIds` | `number[]` | - | For ribbon mode: array of fiber IDs (6-12, each 1-24). Required when mode is 'ribbon'. | ### FibreFlowMap Standalone fibre flow diagram: closures, cables, tubes, splices, and optional circuits. Renders a full diagram from FibreFlowClosure[] and optional FibreFlowCircuit[]. ```tsx import { FibreFlowMap } from '@mp70/react-networks'; ``` #### Props (FibreFlowMapProps) | Prop | Type | Default | Description | |------|------|---------|-------------| | `closures` | `FibreFlowClosure[]` | - | Closure definitions (left/right cables + splices) | | `circuits` | `FibreFlowCircuit[]` | - | Optional circuits for highlighting/labels | | `highlightedCircuitId` | `string` | - | Optional circuit ID to highlight | | `layout` | `'horizontal' \| 'vertical'` | - | Layout direction | | `rowHeight` | `number` | - | Row height in pixels | | `tubeGap` | `number` | - | Gap between tubes | | `closureGap` | `number` | - | Gap between closures | | `cableColumnWidth` | `number` | - | Cable column width | | `spliceAreaWidth` | `number` | - | Splice area width | | `showTubeLabels` | `boolean` | - | Show tube labels | | `showFiberNumbers` | `boolean` | - | Show fiber numbers | | `colorMode` | `'dark' \| 'light'` | - | Color mode | | `showCircuitId` | `boolean` | - | Show circuit ID on edges | | `hideUnusedFibres` | `boolean` | `false` | Collapse strands with no splice into per-tube "N of M unused" indicators (read-only viewing; hidden strands mount no handles) | | `className` | `string` | - | CSS class name | | `style` | `React.CSSProperties` | - | Inline styles | | `interaction` | `NetworkDiagramInteractionOptions` | - | Fine-grained interaction controls | ### OtdrTraceStrip Standalone SVG strip for an OTDR trace: measured events ordered along the fibre by distance, each annotated with its splice/insertion loss, with either the measured backscatter waveform (when `trace` is supplied) or a derived cumulative-loss line. Renders measured data only — it never estimates, interpolates missing events, or scales distances to fit. No React Flow dependency. ```tsx import { OtdrTraceStrip } from '@mp70/react-networks'; ``` #### Props (OtdrTraceStripProps) | Prop | Type | Default | Description | |------|------|---------|-------------| | `events` | `OtdrTraceStripEvent[]` | - | Required. Parsed events (e.g. from a Telcordia SR-4731 .sor file) | | `trace` | `OtdrTraceStripTrace \| null` | - | Measured DataPts waveform (`stepM`, `levelsDb`, optional `startM`); replaces the cumulative-loss line. Clipped to the axis end — it never extends the axis — and the strip captions the clip when samples were dropped | | `focusRangeM` | `[number, number] \| null` | - | Fibre-metre span to spotlight, e.g. the stretch visible on a linked map. Rendered exactly as given (clamped to the axis); pass `null` for no focus — the strip applies no "that is basically everything" heuristic of its own | | `lengthM` | `number \| null` | furthest event (or trace end) | Fibre length for the axis. Authoritative against the capture; only an event or `faultDistanceM` beyond it can extend the axis | | `totalLossDb` | `number \| null` | sum of event losses | Measured end-to-end loss | | `wavelengthNm` | `number \| null` | - | Caption wavelength | | `directionLabel` | `string` | - | e.g. `"A → Z"`; caption only, the strip always draws launch-end left | | `faultDistanceM` | `number \| null` | - | Distance to highlight as an actual fault/break. Reserve for real anomalies — ordinary events are not faults | | `selectedEventIndex` | `number \| null` | - | Index into the sorted events to emphasise | | `colorMode` | `'light' \| 'dark'` | `'light'` | Colour mode | | `density` | `'auto' \| 'full' \| 'compact'` | `'auto'` | Label density. `'auto'` drops the legend and unselected dB labels and scales the remaining text up below 640 CSS px of rendered width; `'compact'` forces that treatment at any width; `'full'` keeps the legend and labels however narrow the strip is | | `height` | `number` | `190` | Pixel height (width is fluid) | | `onEventClick` | `(event: OtdrTraceStripEvent, index: number) => void` | - | Click handler; also enables hover affordance rings | #### Companion exports Exported alongside the component so a host's chrome (map popups, event tables) labels and colours the same data identically instead of maintaining a second vocabulary: | Export | Signature | Description | |--------|-----------|-------------| | `formatOtdrDistance` | `(metres: number) => string` | Formats a fibre distance the way the strip's axis and captions do: metres below 1 km, 2 decimals of km, 1 decimal past 10 km | | `otdrEventKindLabel` | `(type: string \| undefined) => string` | The strip's legend wording for an event `type`, e.g. `'reflective'` → `"reflective (connector or mech. splice)"`. Unrecognised types read as `"event"` | | `otdrTraceStripPalette` | `(mode: 'light' \| 'dark') => { text, subtext, axis, fibre, cumulative, eventFill, end, fault, background }` | The strip's colour palette for a given mode | ### FibreCableWithTubesExpanded Renders a single fibre flow cable with tubes expanded (used inside FibreFlowMap or standalone). ```tsx import { FibreCableWithTubesExpanded } from '@mp70/react-networks'; ``` #### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `cable` | `FibreFlowCable` | - | Cable definition | | `side` | `'left' \| 'right'` | - | Which side (left or right of closure) | | `rowHeight` | `number` | `22` | Row height in pixels | | `tubeGap` | `number` | `10` | Gap between tubes | | `columnWidth` | `number` | `150` | Column width in pixels | | `layout` | `FibreFlowCableLayout` | - | Precomputed layout (optional) | | `showTubeLabels` | `boolean` | - | Show tube labels | | `showFiberNumbers` | `boolean` | `true` | Show fiber numbers | | `showCableLabel` | `boolean` | `true` | Show cable label | ## Utilities ### Rack Schema Management #### buildNodesFromRackConfig Convert rack configuration to network nodes. ```tsx import { buildNodesFromRackConfig } from '@mp70/react-networks'; const nodes = buildNodesFromRackConfig(racks: RackConfig[], options?: RackSchemaOptions): NetworkNode[] ``` **Parameters:** - `racks`: Array of rack configurations - `options`: Optional RackSchemaOptions (conflict policy, validation) **Returns:** - Array of network nodes **Example:** ```tsx const rackConfig: RackConfig[] = [ { id: 'rack-1', name: 'Main Rack', position: { x: 100, y: 100 }, units: 42, devices: [ { id: 'server-1', name: 'Web Server', unit: 1, height: 1, type: 'server', width: Width.FULL } ] } ]; const nodes = buildNodesFromRackConfig(rackConfig); ``` #### createRackConfigFromNodes Create rack configuration from existing nodes. ```tsx import { createRackConfigFromNodes } from '@mp70/react-networks'; const rackConfig = createRackConfigFromNodes(nodes: NetworkNode[]): RackConfig[] ``` **Parameters:** - `nodes`: Array of network nodes **Returns:** - Array of rack configurations #### addDeviceToRack Add device to rack configuration. ```tsx import { addDeviceToRack } from '@mp70/react-networks'; const updatedRacks = addDeviceToRack(racks: RackConfig[], rackId: string, device: RackDevice, options?: RackSchemaOptions): RackConfig[] ``` **Parameters:** - `racks`: Array of rack configurations - `rackId`: Rack identifier - `device`: Device configuration - `options`: Optional RackSchemaOptions (conflict handling) **Returns:** - Updated array of rack configurations #### removeDeviceFromRack Remove device from rack configuration. ```tsx import { removeDeviceFromRack } from '@mp70/react-networks'; const updatedRacks = removeDeviceFromRack(racks: RackConfig[], rackId: string, deviceId: string): RackConfig[] ``` **Parameters:** - `racks`: Array of rack configurations - `rackId`: Rack identifier - `deviceId`: Device identifier **Returns:** - Updated array of rack configurations #### validateRackDevicePlacements Validate device placements in rack. ```tsx import { validateRackDevicePlacements } from '@mp70/react-networks'; const validation = validateRackDevicePlacements(rackConfig: RackConfig): DevicePlacementValidation ``` **Parameters:** - `rackConfig`: Rack configuration **Returns:** - Validation result with errors and warnings ### Splice Schema Management #### buildNodesFromSpliceConfig Convert splice tray configuration to network nodes. ```tsx import { buildNodesFromSpliceConfig } from '@mp70/react-networks'; const nodes = buildNodesFromSpliceConfig(trayConfig: SpliceTrayConfig[], options?: SpliceSchemaOptions): NetworkNode[] ``` **Parameters:** - `trayConfig`: Array of splice tray configurations - `options`: Optional configuration for conflict handling **Returns:** - Array of network nodes **Example:** ```tsx const trayConfig: SpliceTrayConfig[] = [ { id: 'tray-1', name: 'Main Tray', position: { x: 100, y: 100 }, splices: [ { id: 'splice-1', name: 'Splice 1', holder: 1, lossDb: 0.02 } ] } ]; const nodes = buildNodesFromSpliceConfig(trayConfig); ``` **Note:** Each tray supports up to 12 holders (HOLDERS_PER_TRAY = 12). Holder positions are numbered 1-12, with each holder accommodating one splice. #### createSpliceConfigFromNodes Create splice tray configuration from existing nodes. ```tsx import { createSpliceConfigFromNodes } from '@mp70/react-networks'; const trayConfig = createSpliceConfigFromNodes(nodes: NetworkNode[]): SpliceTrayConfig[] ``` **Parameters:** - `nodes`: Array of network nodes **Returns:** - Array of splice tray configurations #### addSpliceToTray Add splice to tray configuration. ```tsx import { addSpliceToTray } from '@mp70/react-networks'; const updatedTrays = addSpliceToTray(trays: SpliceTrayConfig[], trayId: string, splice: SpliceConfig, options?: SpliceSchemaOptions): SpliceTrayConfig[] ``` **Parameters:** - `trays`: Array of tray configurations - `trayId`: Tray identifier - `splice`: Splice configuration - `options`: Optional configuration for conflict handling **Returns:** - Updated array of tray configurations #### removeSpliceFromTray Remove splice from tray configuration. ```tsx import { removeSpliceFromTray } from '@mp70/react-networks'; const updatedTrays = removeSpliceFromTray(trays: SpliceTrayConfig[], trayId: string, spliceId: string): SpliceTrayConfig[] ``` **Parameters:** - `trays`: Array of tray configurations - `trayId`: Tray identifier - `spliceId`: Splice identifier **Returns:** - Updated array of tray configurations #### updateSpliceHolderPosition Update splice holder position in tray configuration. ```tsx import { updateSpliceHolderPosition } from '@mp70/react-networks'; const updatedTrays = updateSpliceHolderPosition(trays: SpliceTrayConfig[], trayId: string, spliceId: string, newHolder: number): SpliceTrayConfig[] ``` **Parameters:** - `trays`: Array of tray configurations - `trayId`: Tray identifier - `spliceId`: Splice identifier - `newHolder`: New holder position (1-12) **Returns:** - Updated array of tray configurations #### validateSplicePlacements Validate splice placements in tray. ```tsx import { validateSplicePlacements } from '@mp70/react-networks'; const validation = validateSplicePlacements(tray: SpliceTrayConfig, options?: SpliceSchemaOptions): SplicePlacementValidation ``` **Parameters:** - `tray`: Tray configuration - `options`: Optional configuration **Returns:** - Validation result with conflicts and out-of-bounds errors ### U Positioning #### snapToUPosition Snap device to U position. ```tsx import { snapToUPosition } from '@mp70/react-networks'; const updatedNode = snapToUPosition(rackNode: NetworkNode, uPosition: number): NetworkNode ``` **Parameters:** - `rackNode`: Rack node configuration - `uPosition`: U position to snap to **Returns:** - Updated rack node #### isUPositionAvailable Check if U position is available. ```tsx import { isUPositionAvailable } from '@mp70/react-networks'; const available = isUPositionAvailable(rackNode: NetworkNode, uPosition: number, height: number): boolean ``` **Parameters:** - `rackNode`: Rack node configuration - `uPosition`: U position to check - `height`: Device height in U units **Returns:** - Whether position is available #### findNextAvailableUPosition Find next available U position. ```tsx import { findNextAvailableUPosition } from '@mp70/react-networks'; const position = findNextAvailableUPosition(rackNode: NetworkNode, height: number): number ``` **Parameters:** - `rackNode`: Rack node configuration - `height`: Device height in U units **Returns:** - Next available U position #### calculateDevicePositionFromU Calculate device position from U position. ```tsx import { calculateDevicePositionFromU } from '@mp70/react-networks'; const position = calculateDevicePositionFromU(rackNode: NetworkNode, uPosition: number): { x: number; y: number } ``` **Parameters:** - `rackNode`: Rack node configuration - `uPosition`: U position **Returns:** - Calculated position coordinates #### updateDeviceUPosition Update device U position. ```tsx import { updateDeviceUPosition } from '@mp70/react-networks'; updateDeviceUPosition(nodeId: string, uPosition: number): void ``` **Parameters:** - `nodeId`: Node identifier - `uPosition`: New U position #### validateAndSnapDevice Validate and snap device to U position. ```tsx import { validateAndSnapDevice } from '@mp70/react-networks'; const success = validateAndSnapDevice(nodeId: string, uPosition: number): boolean ``` **Parameters:** - `nodeId`: Node identifier - `uPosition`: U position to snap to **Returns:** - Whether operation was successful ### Splice Positioning #### snapToSplicePosition Snap a splice node to the nearest holder position within a tray. ```tsx import { snapToSplicePosition } from '@mp70/react-networks'; const result = snapToSplicePosition(splice: NetworkNode, tray: NetworkNode, newPosition: { x: number; y: number }, allNodes: NetworkNode[]): { x: number; y: number; holderPosition?: number } ``` **Parameters:** - `splice`: The splice node to snap - `tray`: The parent tray node - `newPosition`: Desired position { x, y } - `allNodes`: All nodes in the diagram (for conflict check) **Returns:** - Snapped position and optional holderPosition (1-12) #### findNextAvailableHolderPosition Find the next available holder position in a tray. ```tsx import { findNextAvailableHolderPosition } from '@mp70/react-networks'; const position = findNextAvailableHolderPosition(tray: NetworkNode, splice: NetworkNode, allNodes: NetworkNode[], startFrom?: number): number ``` **Parameters:** - `tray`: Tray node - `splice`: Splice node (to exclude from conflict check) - `allNodes`: All nodes - `startFrom`: Start searching from this holder (default 1) **Returns:** - Holder position (1-12) #### calculateSplicePositionFromNumber Calculate x,y position from holder number within a tray (single-mode spacing). ```tsx import { calculateSplicePositionFromNumber } from '@mp70/react-networks'; const position = calculateSplicePositionFromNumber(tray: NetworkNode, holderPosition: number): { x: number; y: number } ``` **Parameters:** - `tray`: Tray node - `holderPosition`: Holder number (1-12) **Returns:** - { x, y } in pixels ### Coupler Positioning Constants and functions for patch panel / closure layout (couplers, tubes, trays). **Constants:** ```tsx import { COUPLER_SLOT_HEIGHT_PX, PANEL_HEADER_HEIGHT, COUPLERS_PER_PANEL, COUPLER_WIDTH_PX, PANEL_WIDTH_PX, PANEL_HEIGHT_PX, PANEL_BORDER_WIDTH, PANEL_PADDING, PANEL_TUBE_TRAY_SPACING, } from '@mp70/react-networks'; ``` | Constant | Value | Description | |----------|-------|-------------| | `COUPLER_SLOT_HEIGHT_PX` | 30 | Slot height in pixels | | `PANEL_HEADER_HEIGHT` | 32 | Panel header height | | `COUPLERS_PER_PANEL` | 24 | Default couplers per panel | | `COUPLER_WIDTH_PX` | 80 | Coupler width | | `PANEL_WIDTH_PX` | 400 | Default panel width | | `PANEL_HEIGHT_PX` | 300 | Default panel height | | `PANEL_BORDER_WIDTH` | 2 | Panel border width | | `PANEL_PADDING` | 8 | Panel padding | | `PANEL_TUBE_TRAY_SPACING` | 120 | Spacing between tubes and trays | **Functions:** | Function | Signature | Description | |----------|-----------|-------------| | `getPanelWidth` | `(panelNode: NetworkNode) => number` | Panel width from data or default | | `getPanelHeight` | `(panelNode: NetworkNode) => number` | Panel height from data or default | | `getInnerContentWidth` | `(panelNode: NetworkNode) => number` | Width minus borders/padding | | `calculateCouplerRightX` | `(panelNode: NetworkNode, couplerWidth: number) => number` | Right X of coupler in panel | | `calculateSlotPositionFromY` | `(y: number, slotHeight?: number) => number` | Slot number (1-based) from Y | | `calculateYFromSlotPosition` | `(slot: number, slotHeight?: number) => number` | Y from slot number | | `getCouplerDimensions` | `(node: NetworkNode) => { width: number; height: number }` | Coupler dimensions from node data | | `getTubeDimensions` | `(node: NetworkNode) => { width: number; height: number }` | Tube dimensions from node data | | `getTrayDimensions` | `(node: NetworkNode) => { width: number; height: number }` | Tray dimensions (fixed) | | `snapToCouplerPosition` | `(coupler: NetworkNode, panel: NetworkNode, position: { x: number; y: number }) => { x: number; y: number; slotPosition?: number }` | Snap coupler to nearest slot | | `calculatePanelDimensions` | `(panelNode: NetworkNode, childNodes: NetworkNode[]) => { width: number; height: number }` | Panel size from children | | `calculateCouplerSpacing` | `(numCouplers: number, couplerHeight: number, availableHeight: number) => number` | Spacing between couplers | | `calculateCouplerSlotSpacing` | Same as calculateCouplerSpacing | Alias for slot spacing | ### Power Connectors ```tsx import { getPowerPortStyle, getConnectorConfig, getDeviceConnectorType, getPDUPortType, isHighPowerConnector, POWER_CONNECTORS, } from '@mp70/react-networks'; ``` | Function / Export | Signature / Type | Description | |------------------|------------------|-------------| | `getPowerPortStyle` | `(connectorType: string) => PowerPortStyle` | CSS-style for a connector (C13/C14/C19/C20) | | `getConnectorConfig` | `(connectorType: string) => PowerConnectorConfig` | Config for connector type | | `getDeviceConnectorType` | `(deviceName: string) => string` | C14 or C20 from device name | | `getPDUPortType` | `(portIndex: number) => string` | C13 or C19 by position in cycle (8×C13, 2×C19) | | `isHighPowerConnector` | `(connectorType: string) => boolean` | True for 16A (C19/C20) | | `POWER_CONNECTORS` | `Record` | C13, C14, C19, C20 configs | **PowerConnectorConfig:** `{ type, color, borderColor, amperage, voltage, phases }` **PowerPortStyle:** `{ width, height, borderRadius?, clipPath?, color, borderColor, fontSize }` ### Status Colors ```tsx import { getStatusColor, getStatusColorHex } from '@mp70/react-networks'; getStatusColor(status: DeviceStatus | string): string getStatusColorHex(status: DeviceStatus | string): string ``` `getStatusColor` returns a theme-aware CSS custom-property expression such as `var(--rn-status-ok, #10b981)`. Use it for DOM/CSS styles. CSS custom-property expressions are not valid Canvas paint values and are unsuitable for colour parsers; use `getStatusColorHex` for Canvas, generated SVG, image export, or colour maths. ### Fibre Colors ```tsx import { FIBRE_COLORS_12, baseColorFor, isStriped, fibreSolidOrStriped, getFibreColor, } from '@mp70/react-networks'; import type { FibreColorId } from '@mp70/react-networks'; ``` | Export | Signature / Type | Description | |--------|------------------|-------------| | `FIBRE_COLORS_12` | `readonly string[]` | TIA-598-C base colors (1-12) | | `baseColorFor` | `(id: FibreColorId \| number) => string` | Hex for fibre ID (1-24, 13-24 same as 1-12) | | `isStriped` | `(id: FibreColorId \| number) => boolean` | True for 13-24 (striped) | | `fibreSolidOrStriped` | `(id: FibreColorId \| number) => React.CSSProperties` | Background (and stripe) CSS | | `getFibreColor` | `(id: FibreColorId \| number) => string` | Hex color for ID | ### Rack Geometry ```tsx import { getRackBounds, isPointInRack, findNearestRack } from '@mp70/react-networks'; ``` | Function | Signature | Description | |----------|-----------|-------------| | `getRackBounds` | `(rack: NetworkNode) => { left, right, top, bottom }` | Bounding box of rack in pixels | | `isPointInRack` | `(point: { x: number; y: number }, rack: NetworkNode) => boolean` | Whether point is inside rack | | `findNearestRack` | `(point: { x: number; y: number }, racks: NetworkNode[]) => NetworkNode \| null` | Nearest rack that contains the point | ### Edge Connection ```tsx import { replaceEdge } from '@mp70/react-networks'; import type { ReplaceEdgeOptions } from '@mp70/react-networks'; replaceEdge(options: ReplaceEdgeOptions): Edge[] | null ``` Replaces an existing edge (found by source or target handle) with a new connection. Options: `edges`, `connection`, `edgeType`, `selectEdgeZIndex`, `findBy` ('source' | 'target'), optional `matchOn` ('source' | 'target', defaults to `findBy`—use when connection's target handle is on the edge's source, e.g. replacing switch connection when dragging from server to switch), optional `data`. Uses normalized handle comparison for bidirectional handles. Returns new edges array or null if no matching edge. ### Connection validation (core) Pure connection-validation helpers. There is **no headless store**: React Flow is the diagram state source, and the old store/hook surface was removed in 0.9.0. ```tsx import { validateNetworkConnection, computeEdgeZIndex, createNodeMap, getHandleSide, } from '@mp70/react-networks'; ``` | Export | Signature | Description | |--------|-----------|-------------| | `validateNetworkConnection` | `(params: ValidateConnectionParams, ctx) => ValidationResult` | Whether a proposed connection is legal (fibre policy, power rules, face scoping) | | `computeEdgeZIndex` | `(edge, nodes) => number` | z-index for an edge given its endpoints | | `createNodeMap` | `(nodes: NetworkNode[]) => Map` | Id → node lookup used by the validators | | `getHandleSide` | `(handleId: string) => 'left' \| 'right' \| ...` | Side a handle sits on | Power connections accept any power-source node type — `vertical-pdu` **and** `distribution-board` (`POWER_SOURCE_NODE_TYPES`); the validation entry exposes the same set as `POWER_SOURCE_CAPABLE_NODE_TYPES`. **Power endpoint roles.** `getPowerEndpointRole(node, handle)` (also `EndpointMetadata.powerRole`) classifies a power endpoint as one of: | Role | What it is | Handles | |------|------------|---------| | `circuit-source` | Breaker way on a distribution board | `way-` | | `circuit-sink` | PDU / board supply inlet | `inlet` (`PDU_INLET_HANDLE_ID`), `incomer` (`BOARD_INCOMER_HANDLE_ID`) | | `cord-source` | PDU outlet, or a device `power_outlet` port | `'1'`…`'N'` on a `vertical-pdu` | | `cord-sink` | Device power inlet | `pwr-left-N` / `pwr-right-N`, any `power_*` port on equipment | | `unknown` | Unmodelled — never judged, only skipped | anything else | Rules `defaultValidateConnection` enforces: - `circuit-source` → `circuit-sink` is **valid** (board-to-board included). - `circuit-source` → `cord-sink` or `cord-source` is **invalid**: `CIRCUIT_TERMINATION_REASON` — *"Branch circuits terminate on a PDU inlet — feed devices from PDU outlets"*. - Anything but a way into a `circuit-sink` is **invalid**: `PDU_INLET_FEED_REASON` — strips are not daisy-chained. - `cord-source` → `cord-sink` is **valid** under the IEC 60320 mate table (C13→C14, C19→C20); a mismatch reports the mate reason. `cord-source` → `cord-source` is invalid. - An `unknown` endpoint is never judged here, so graphs authored before the inlet model (bare outlets into `pwr-*` device inlets, no inlets, no ways) validate exactly as they did. **NetworkDiagramOptions:** `{ validateConnection?: (params, ctx) => ValidationResult }` **ValidationResult:** `{ valid: boolean; reason?: string }` **ValidateConnectionParams:** `{ source, target, sourceHandle?, targetHandle? }` (strings or null) ### Inventory Integration #### inventoryRackToNetworkNode Convert inventory rack to network node. ```tsx import { inventoryRackToNetworkNode } from '@mp70/react-networks'; const node = inventoryRackToNetworkNode(rack: any): NetworkNode ``` **Parameters:** - `rack`: inventory rack data **Returns:** - Network node #### inventoryDeviceToNetworkNode Convert inventory device to network node. ```tsx import { inventoryDeviceToNetworkNode } from '@mp70/react-networks'; const node = inventoryDeviceToNetworkNode(device: any): NetworkNode ``` **Parameters:** - `device`: inventory device data **Returns:** - Network node #### inventoryCableToNetworkEdge Convert inventory cable to network edge. ```tsx import { inventoryCableToNetworkEdge } from '@mp70/react-networks'; const edge = inventoryCableToNetworkEdge(cable: any): NetworkEdge ``` **Parameters:** - `cable`: inventory cable data **Returns:** - Network edge #### inventoryToNetworkDiagram Convert inventory data to complete diagram. ```tsx import { inventoryToNetworkDiagram } from '@mp70/react-networks'; const diagram = inventoryToNetworkDiagram(data: { racks: any[]; devices: any[]; cables: any[] }): { nodes: NetworkNode[]; edges: NetworkEdge[] } ``` **Parameters:** - `data`: inventory data object **Returns:** - Diagram with nodes and edges #### generateLayout Generate a simple layout from an array of nodes. ```tsx import { generateLayout } from '@mp70/react-networks'; const laidOutNodes = generateLayout(nodes: NetworkNode[]): NetworkNode[] ``` **Parameters:** - `nodes`: Array of network nodes **Returns:** - Nodes with updated positions ### Power boards and energisation ```tsx import { computeEnergization, applyEnergizationToEdges, buildBreakerWayHandleId, } from '@mp70/react-networks'; import type { DevicePowerState, PowerFlowDirection, EdgeEnergization, EnergizationOptions, EnergizationResult, } from '@mp70/react-networks'; ``` | Export | Signature | Description | |--------|-----------|-------------| | `computeEnergization` | `(nodes: NetworkNode[], edges: NetworkEdge[], options?: EnergizationOptions) => EnergizationResult` | Pure reachability over the existing power edges: from source nodes, out through **closed** breaker ways, onward through PDU/UPS/downstream boards, terminating at consuming devices | | `applyEnergizationToEdges` | `(edges: NetworkEdge[], result: EnergizationResult) => NetworkEdge[]` | Returns a new edge array with `data.energized` / `data.flowDirection` stamped on each power edge | | `buildBreakerWayHandleId` | `(wayId: string) => string` | Handle id for a board way (one power handle per way) | | `isPowerInletHandleId` | `(handleId?: string \| null) => boolean` | Whether a handle names a PDU `inlet` or board `incomer` | | `toOutletPhaseIndex` | `(position: number, phases: number) => number` | 0-based phase of a 1-based PDU outlet: `(position - 1) % phases`, round-robin from the top | | `getPduOutletPhaseLabel` | `(position: number, phases: number) => 'L1' \| 'L2' \| 'L3' \| undefined` | Phase label under the same convention; `undefined` on a single-phase strip | | `getPduOutletConnectorType` | `(portTypes: string[] \| undefined, position: number) => string` | Connector on a 1-based outlet, honouring `data.portTypes` before the 8×C13 + 2×C19 cycle | A PDU/board **inlet** is the node's upstream connection point: whichever end of a power edge lands on an inlet is the one being fed, and power never leaves a strip through its inlet. A de-energised circuit into the inlet leaves every outlet on that strip dead. **EnergizationOptions:** `{ sources?: readonly string[]; defaultWayState?: 'closed' \| 'open' \| 'tripped' }` — with no `sources`, every unfed distribution board, PDU or UPS is a source, including supplies in independent components. A board with an upstream feed never powers itself. Pass `sources: []` to disable all sources; validation similarly honours `sourceNodeIds: []`. An unstated way defaults to `closed`. **EnergizationResult:** `{ edges: Record; devices: Record; energizedNodeIds: string[] }` **DevicePowerState:** `'powered'` (all feeds live) | `'degraded'` (some but not all feeds live — see `expectedFeeds`) | `'unpowered'`. **Invariant:** breaker state never alters the edge set. Opening or tripping a way changes only the overlay; the input `nodes` / `edges` arrays are never mutated and no edge is added or removed. A de-energised edge is still an edge. ```tsx const energization = computeEnergization(nodes, edges); const paintedEdges = applyEnergizationToEdges(edges, energization); ``` ### Power validation (`@mp70/react-networks/validation`) Server-safe checks engine — no React/React Flow at runtime, so it runs in an API route or worker. It ships **zero default rules**: pass your own, or opt into the `standardPowerRules` preset (tree-shaken away when you do not import it). ```tsx import { runPowerValidation, standardPowerRules } from '@mp70/react-networks/validation'; import type { PowerRule, PowerIssue } from '@mp70/react-networks/validation'; const result = runPowerValidation(nodes, edges, { rules: standardPowerRules, config: { energization }, }); result.issues.forEach((issue: PowerIssue) => { console.log(issue.severity, issue.ruleId, issue.message); }); ``` | Export | Description | |--------|-------------| | `runPowerValidation` | `(nodes, edges, { rules, config? }) => PowerValidationResult` — runs the supplied rules and returns `issues`, `warnings`, `evaluatedRuleIds`, `skippedRuleIds` | | `standardPowerRules` | Opt-in preset: Tier A (A01–A07, A09–A13) + Tier B (B01–B05, B08) from `docs/electrical-validation-rules-2026-08-14.md`. Tier C (selectivity, trip curves, arc-flash, earth-loop impedance) is deliberately excluded — not computable from a diagram | | `pwrA13CircuitTermination` | **PWR-A13** (`convention`, `error`) — a breaker way must terminate on a PDU/board inlet, never on a device inlet or a PDU outlet. Unmodelled landings are skipped, not guessed at | | `pwrB08PhaseImbalance` | **PWR-B08** (`convention`, `warning`, option `maxImbalancePct` default 10) — per-phase nameplate load imbalance on a multi-phase PDU. Only judged when every phase carries load; nameplate sums are an upper bound, not measured draw | | `getStandardPowerRule` | Look up one preset rule by catalogue id | | `buildPowerValidationContext` | Build the graph context the rules read (also useful for custom rules) | | `isPowerEdge`, `powerElementKey`, `parsePowerElementKey` | Graph helpers | | `POWER_SOURCE_CAPABLE_NODE_TYPES` | Node types that can source power (`vertical-pdu`, `distribution-board`) | | `getConnectorAmperage`, `getConnectorRole`, `isValidIecMate`, `IEC_60320_*` | IEC 60320 connector tables + mating rules | Types: `PowerRule`, `PowerRuleBasis`, `PowerRuleConfig`, `PowerRuleOptions`, `PowerIssue`, `PowerIssueDraft`, `PowerIssueSeverity`, `PowerValidationConfig`, `PowerValidationContext`, `PowerValidationResult`, `PowerGraphNode`, `PowerGraphEdge`, `PowerLink`, `PowerWayRef`, `PowerElementKey`, `ResolvedPowerConnector`, `RunPowerValidationOptions`. ## Types ### NetworkNode ```tsx interface NetworkNode { id: string; type: NetworkNodeType; position: { x: number; y: number }; parentId?: string; extent?: 'parent' | [[number, number], [number, number]]; data: { label: string; ports?: PortBlock[]; connections?: string[]; status?: DeviceStatus; netboxId?: number; netboxType?: 'device' | 'rack' | 'interface' | 'cable'; uHeight?: number; uPosition?: number; face?: DeviceFace; deviceType?: string; manufacturer?: string; role?: string; width?: Width; rearPorts?: PortBlock[]; powerSide?: PowerSide; frontImageUrl?: string; rearImageUrl?: string; frontImageAttribution?: string; rearImageAttribution?: string; imageFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; imagePosition?: string; imageRepeat?: string; stretchToFit?: boolean; heightCm?: number; powerPorts?: number; // vertical-pdu only (VerticalPduNodeData): portTypes?: Array<'C13' | 'C19'>; boardType?: 'pdu' | 'rpp' | 'panel' | 'busway' | 'other'; supply?: 'ac' | 'dc'; voltage?: number; phases?: 1 | 3; inletConnector?: string; inletAmps?: number; inletGender?: 'male' | 'female'; feedType?: 'primary' | 'secondary'; uNumberingDirection?: UNumberingDirection; customHeaderText?: string; rackWidthPx?: number; holderPosition?: number; lossDb?: number; connector?: 'SC' | 'LC'; couplerFiberCount?: 1 | 2 | 4; portLabelsLeft?: string[]; portLabelsRight?: string[]; slotPosition?: number; panelWidth?: number; panelHeight?: number; rows?: number; cols?: number; header?: string; splitterType?: 'splitter' | 'WDM'; inputCount?: number; outputCount?: number; [key: string]: any; }; } ``` ### NetworkEdge ```tsx interface NetworkEdge { id: string; source: string; target: string; sourceHandle?: string; targetHandle?: string; type?: NetworkEdgeType; data?: { bandwidth?: string; length?: number; color?: string; kind?: 'fiber' | 'power' | 'network'; fiberId?: number; ribbonFiberIds?: number[]; isRibbon?: boolean; highlight?: boolean; highlightColor?: string; highlightWidth?: number; traceTag?: string; connectorType?: string; [key: string]: any; }; } ``` ### NetworkNodeType ```tsx type NetworkNodeType = | 'rack' | 'switch' | 'router' | 'server' | 'fiber' | 'patch-panel' | 'device' | 'vertical-pdu' | 'distribution-board' | 'splice' | 'splice-tray' | 'tube' | 'cable' | 'multi-tube-cable' | 'coupler' | 'closure' | 'fibre-split' | 'fibre-flow-cable' | 'fibre-flow-closure'; ``` ### NetworkEdgeType ```tsx type NetworkEdgeType = | 'fiber' | 'ethernet' | 'power' | 'smoothstep' | 'step' | 'thick-cable' | 'fibre-flow' | 'fibre-flow-link'; ``` ### RackConfig ```tsx interface RackConfig { id: string; name: string; position: { x: number; y: number }; units: number; uNumberingDirection?: UNumberingDirection; customHeaderText?: string; rackWidthPx?: number; devices: RackDevice[]; } ``` ### RackDevice ```tsx interface RackDevice { id: string; name: string; unit: number; height: number; type: RackDeviceType; width?: Width; role?: string; ports?: PortBlock[]; rearPorts?: PortBlock[]; powerSide?: PowerSide; frontImageUrl?: string; rearImageUrl?: string; frontImageAttribution?: string; rearImageAttribution?: string; imageFit?: string; imagePosition?: string; imageRepeat?: string; stretchToFit?: boolean; connections?: string[]; } ``` ### PortBlock and Port ```tsx interface PortBlock { name: string; ports: Port[]; } type PortType = 'ethernet' | 'fiber' | 'console' | 'mgmt' | 'usb' | 'power_ac' | 'power_c13' | 'power_c19'; interface Port { label: string; connected: boolean; type?: PortType; cableType?: 'smf' | 'cat6' | 'om3' | 'om4' | 'om5' | 'cat5e' | 'cat6a' | 'cat7' | 'fiber' | 'ethernet'; } ``` ### Other type definitions ```tsx type DeviceStatus = 'active' | 'inactive' | 'maintenance'; type DeviceFace = 'front' | 'rear'; type PowerSide = 'left' | 'right'; type UNumberingDirection = 'bottom-up' | 'top-down'; type RackDeviceType = 'server' | 'switch' | 'router' | 'patch-panel' | 'ups'; type FiberCableType = 'single-mode' | 'multi-mode'; enum Width { FULL = 'full', HALF = 'half' } ``` ### FiberCable ```tsx interface FiberCable { id: string; name: string; type: FiberCableType; length: number; color: string; connections: { from: string; to: string }; } ``` ### SpliceConfig and SpliceTrayConfig ```tsx interface SpliceConfig { id: string; name: string; holder: number; lossDb?: number; mode?: 'single' | 'ribbon'; ribbonFiberIds?: number[]; } interface SpliceTrayConfig { id: string; name: string; position: { x: number; y: number }; splices: SpliceConfig[]; } ``` ### Fibre flow types ```tsx type FibreFlowCableKind = 'simple' | 'tube'; interface FibreFlowEndpoint { tube?: number; fiber: number; } interface FibreFlowSplice { id: string; from: FibreFlowEndpoint; to: FibreFlowEndpoint; lossDb?: number; circuitId?: string; label?: string; color?: string; } interface FibreFlowCable { id: string; label: string; kind?: FibreFlowCableKind; fiberCount?: number; tubeCount?: number; fibersPerTube?: number; tubeStartIndex?: number; fiberStartIndex?: number; tubeColorMap?: Record; jacketColor?: string; distanceLabel?: string; distanceMeters?: number; } interface FibreFlowClosure { id: string; chamberId?: string; leftCable: FibreFlowCable; rightCable: FibreFlowCable; splices: FibreFlowSplice[]; } interface FibreFlowCircuit { id: string; label: string; color?: string; } ``` ### Rack schema options and validation ```tsx interface RackSchemaOptions { conflictPolicy?: 'strict' | 'nearest'; validatePlacements?: boolean; deviceTypeMapping?: Record; } interface DevicePlacementValidation { isValid: boolean; conflicts: Array<{ deviceId: string; uPosition: number; uHeight: number; conflictWith: string }>; outOfBounds: Array<{ deviceId: string; uPosition: number; uHeight: number; rackUHeight: number }>; } ``` ### Splice schema options and validation ```tsx interface SpliceSchemaOptions { conflictPolicy?: 'strict' | 'nearest'; validatePlacements?: boolean; } interface SplicePlacementValidation { isValid: boolean; conflicts: Array<{ spliceId: string; holder: number; conflictWith: string }>; outOfBounds: Array<{ spliceId: string; holder: number; max: number }>; } ``` ### ReplaceEdgeOptions ```tsx interface ReplaceEdgeOptions { edges: Edge[]; connection: Connection; edgeType: 'power' | 'fiber' | 'step' | 'smoothstep' | 'thick-cable'; selectEdgeZIndex: (edge: NetworkEdge) => number; findBy: 'source' | 'target'; data?: NetworkEdge['data']; } ``` ### FibreColorId ```tsx type FibreColorId = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; ``` ## Error Handling ### DevicePlacementError Thrown when device placement conflicts occur in strict mode (rack schema). ```tsx class DevicePlacementError extends Error { constructor( public rackId: string, public deviceId: string, public uPosition: number, public uHeight: number, public conflictWith?: string ) { super(`Device placement conflict: ${deviceId} at U${uPosition}-${uPosition + uHeight - 1} in rack ${rackId}${conflictWith ? ` conflicts with ${conflictWith}` : ''}`); this.name = 'DevicePlacementError'; } } ``` ### SplicePlacementError Thrown when splice placement conflicts occur in strict mode (splice schema). ```tsx class SplicePlacementError extends Error { constructor( public trayId: string, public spliceId: string, public holder: number, public conflictWith?: string ) { super(`Splice placement conflict: ${spliceId} at holder ${holder} in tray ${trayId}${conflictWith ? ` conflicts with ${conflictWith}` : ''}`); this.name = 'SplicePlacementError'; } } ``` ## Constants Exported from `@mp70/react-networks`: ```tsx const U_HEIGHT_PX = 20; // Height of 1U in pixels const RACK_HEADER_HEIGHT = 60; // Fixed rack header height in pixels const RACK_WIDTH_PX = 300; // Fixed rack width in pixels (standard 19" rack) const HANDLE_EXTENSION_PX = 5; // Handles extend this many pixels outside nodes ``` ## Events These callbacks are supported by **NetworkDiagram** via NetworkDiagramProps. React Flow may expose additional events when using custom `nodeTypes` / `edgeTypes` or the instance from `onInit` / `onFlowMethods`. ### Diagram callbacks (NetworkDiagramProps) | Callback | Signature | Description | |----------|-----------|-------------| | `onNodeClick` | `(node: NetworkNode \| null) => void` | When a node is clicked or selection changes (null when deselected) | | `onEdgeClick` | `(edge: NetworkEdge) => void` | When an edge is clicked | | `onFaceChange` | `(nodeId: string, face: DeviceFace) => void` | When device face (front/rear) changes | | `onRackFaceChange` | `(rackId: string, face: DeviceFace) => void` | When rack face changes | | `onConnect` | `(connection: Connection) => void` | When a new connection is made (optional override) | | `onEdgeUpdate` | `(oldEdge: Edge, newConnection: Connection) => void` | When an edge is updated (optional override) | | `onInit` | `(instance: any) => void` | When React Flow instance is initialized | | `onFlowMethods` | `(methods: { toObject, setViewport, setNodes, setEdges, toImage, getNodes, getEdges, fitView }) => void` | Callback to get save/restore and view methods |