# Per-node module split Where each concern lives **after** the refactor. All paths are relative to `nodes//src/`. ## Generic node template (any node post-refactor) ``` nodes// .js # Node-RED entry: registerType + admin endpoints (≤ 50 lines) .html # Form template + thin oneditprepare/oneditsave (≤ 250 lines) CONTRACT.md # Generated from commands/ + hand-written events examples/ 01-basic.json 02-integration.json 03-dashboard.json # optional src/ nodeClass.js # extends BaseNodeAdapter; ~25 lines specificClass.js # extends BaseDomain; orchestrator only; ~150 lines editor.js # client-side JS for HTML, served via admin endpoint (only if non-trivial UI) commands/ index.js # the command registry array handlers.js # the handler functions / # one folder per domain concern (see per-node sections below) ... test/ basic/ integration/ edge/ ``` ## pumpingStation (Process Cell — L5, `#0c99d9`) ``` src/ nodeClass.js # ~25 lines, extends BaseNodeAdapter specificClass.js # ~150 lines, orchestrator editor.js # extracted SVG/redraw logic from the .html (~260 lines) commands/ index.js # set.mode | set.demand | set.inflow | calibrate.* | child.register handlers.js basin/ BasinGeometry.js # initBasinProperties + level<->volume conversions thresholdValidator.js # _validateThresholdOrdering — pure function measurement/ flowAggregator.js # _selectBestNetFlow + _updatePredictedVolume + _computeRemainingTime + _levelRate + _deriveDirection measurementRouter.js # _handleMeasurement + _onLevelMeasurement + _onPressureMeasurement calibration.js # calibratePredictedVolume + calibratePredictedLevel + setManualInflow control/ levelBased.js # _controlLevelBased + _scaleLevelToFlowPercent + _applyMachineGroupLevelControl flowBased.js # placeholder for the flow mode; clearly stubbed manual.js # forwardDemandToChildren index.js # { 'levelbased': ..., 'flowbased': ..., 'manual': ... } safety/ safetyController.js # evaluate() — split internally into dryRunRule + overfillRule io/ statusBadge.js # getStatusBadge composition (was nodeClass._updateNodeStatus) output.js # getOutput, mostly a pass-through to measurements + basin snapshot configBuilder.js # extracted _loadConfig mapping examples/ standalone-demo.js # extracted from the bottom of specificClass.js ``` ## measurement (Control Module — L2, `#a9daee`) The good news: `Channel.js` already exists and is pure. Most of the analog mode in `specificClass.js` is duplication that vanishes when the analog path also goes through `Channel`. ``` src/ nodeClass.js # extends BaseNodeAdapter specificClass.js # ~150 lines, orchestrator over modes channel/ Channel.js # KEEP — already clean, the model for everything else modes/ analogMode.js # one Channel built from flat config; routes msg.payload number digitalMode.js # N channels from config.channels[]; routes msg.payload object index.js # { analog, digital } simulation/ simulator.js # simulateInput — random walk over the configured range calibration/ calibrator.js # calibrate + isStable + standardDeviation helpers (drop duplicates of the static helpers in Channel) commands/ index.js # set.simulator | set.outlierDetection | cmd.calibrate | data.measurement handlers.js ``` `statistics/` (mean/stdDev/median/etc.) — promote to `generalFunctions/src/stats/`. Both `Channel.static helpers` and the calibrator use them. ## machineGroupControl (Unit — L4, `#50a8d9`) ``` src/ nodeClass.js # extends BaseNodeAdapter specificClass.js # ~200 lines orchestrator; tick/handlePressureChange/handleInput groupOps/ groupOperatingPoint.js # _equalizeOperatingPoint, _readChildMeasurement, _writeMeasurement groupCurves.js # _groupFlow, _groupPower, _groupNCog, _groupCalcPower totals/ totalsCalculator.js # calcDynamicTotals, calcAbsoluteTotals, activeTotals combinatorics/ pumpCombinations.js # validPumpCombinations + checkSpecialCases optimizer/ bestCombination.js # calcBestCombination (CoG-based) bepGravitation.js # calcBestCombinationBEPGravitation + redistributeFlowBySlope + estimateSlopesAtBEP index.js # picks the optimizer by config efficiency/ groupEfficiency.js # calcGroupEfficiency + calcDistanceBEP + helpers dispatch/ demandDispatcher.js # uses LatestWinsGate; handleInput + per-machine fanout registration/ # auto via ChildRouter — file may be tiny commands/ index.js # set.mode | set.scaling | set.demand | child.register handlers.js ``` ## rotatingMachine (Equipment Module — L3, `#86bbdd`) The biggest specificClass (1760 lines). The split mirrors the natural boundaries the existing comments suggest. ``` src/ nodeClass.js # extends BaseNodeAdapter specificClass.js # ~250 lines orchestrator curves/ curveLoader.js # loadCurve wrapper + model resolution curveNormalizer.js # _normalizeMachineCurve + _normalizeCurveSection (unit conversion + anomaly detection) reverseCurve.js # the existing reverseCurve helper prediction/ predictors.js # owns predictFlow / predictPower / predictCtrl (delegates to generalFunctions/predict) groupPredictors.js # group-scope predictors used when an MGC parent calls setGroupOperatingPoint operatingPoint.js # current operating point: pressure source, derived flow & power drift/ driftAssessor.js # _updateMetricDrift + assessDrift + _applyDriftPenalty predictionHealth.js # composes flow/power/pressure drift into a HealthStatus pressure/ virtualChildren.js # _initVirtualPressureChildren + dashboard-sim children pressureInitialization.js # getPressureInitializationStatus + tracking real children pressureRouter.js # updateMeasuredPressure + per-position handling state/ # adapter to generalFunctions/state — thin glue, lifecycle hooks stateBindings.js # the position/state event handlers that fire _updateState etc. measurement/ measurementHandlers.js # updateMeasured{Flow,Power,Temperature} + _callMeasurementHandler flow/ flowController.js # handleInput dispatch by source/action/parameter — feeds state machine display/ workingCurves.js # showWorkingCurves + showCoG (admin endpoints) commands/ index.js # set.mode | cmd.startup | cmd.shutdown | cmd.estop | cmd.setpoint | cmd.flow-setpoint | data.simulate-measurement | query.curves | query.cog handlers.js ``` ## remaining nodes (skeleton — they get the platform refactor only) | Node | Notes | |---|---| | `valve` | Equipment Module. Smaller than rotatingMachine — concern split likely just `state/`, `commands/`, `position/`. | | `valveGroupControl` | Unit. Similar to MGC but no flow-power optimization — straightforward `position-aggregator` + `commands/`. | | `reactor` | Unit. Domain is biological kinetics (ASM); will need a `kinetics/` folder. Big — second-tier candidate for deeper split. | | `settler` | Unit. Has the recently-fixed `_connectReactor` integration; keep that wired through `ChildRouter`. | | `monster` | Unit. Multi-parameter monitoring; the parameter set itself is config-driven. | | `diffuser` | Equipment Module. Aeration controller. Likely small. | | `dashboardAPI` | Utility. InfluxDB endpoints. Likely no `BaseDomain` — it's a passive HTTP server. | The "skeleton" refactor for these is just: - Convert `nodeClass.js` to extend `BaseNodeAdapter`. - Convert `specificClass.js` to extend `BaseDomain`. - Move the input switch to `commands/`. - Add `getStatusBadge()` if not present. - Use `ChildRouter` for registration. - File splits driven by file size — if `specificClass` < 300 lines, leave it alone for now. ## generalFunctions itself ``` src/ configs/ # unchanged — JSON schemas per node helper/ # eventually split into infra/ + domain/, but not in this refactor measurements/ # MeasurementContainer — unchanged nodered/ # NEW — node-RED-side infra BaseNodeAdapter.js commandRegistry.js statusBadge.js # composition helpers statusUpdater.js # the 1 Hz status-loop wrapper index.js domain/ # NEW — domain-side infra BaseDomain.js UnitPolicy.js ChildRouter.js LatestWinsGate.js HealthStatus.js index.js stats/ # NEW — promoted from measurement (mean, std, median, mad, lerp) index.js ``` Existing exports (`logger`, `configManager`, `outputUtils`, `MeasurementContainer`, `predict`, `interpolation`, `state`, …) stay exactly where they are. Imports keep working unchanged. `generalFunctions/index.js` adds new exports alongside existing ones. Nothing is removed in this refactor.