Files
EVOLV/scripts/transform-flow-step1.js
znetsixe 6a6c04d34b Migrate to new Gitea instance (gitea.wbd-rd.nl)
- Update all submodule URLs from gitea.centraal.wbd-rd.nl to gitea.wbd-rd.nl
- Add settler as proper submodule in .gitmodules
- Add agent skills, function anchors, decisions, and improvements
- Add Docker configuration and scripts
- Add manuals and third_party docs
- Update .gitignore with secrets and build artifacts
- Remove stale .tgz build artifact

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:07:04 +01:00

381 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* Step 1: Tab Restructure + Per-tab link-outs
* - Creates 4 new tabs (PS West, PS North, PS South, Treatment)
* - Renames WWTP tab to "Telemetry / InfluxDB"
* - Moves nodes to their new tabs
* - Creates per-tab link-out nodes for influx + process
* - Rewires nodes to use local link-outs
* - Recalculates coordinates for clean layout
*/
const fs = require('fs');
const path = require('path');
const FLOW_PATH = path.join(__dirname, '..', 'docker', 'demo-flow.json');
const flow = JSON.parse(fs.readFileSync(FLOW_PATH, 'utf8'));
const byId = (id) => flow.find(n => n.id === id);
// =============================================
// 1a. Create 4 new tabs
// =============================================
flow.push(
{ id: "demo_tab_ps_west", type: "tab", label: "PS West", disabled: false, info: "Pumping Station West (Urban Catchment - 2 pumps, Level-based)" },
{ id: "demo_tab_ps_north", type: "tab", label: "PS North", disabled: false, info: "Pumping Station North (Industrial - 1 pump, Flow-based)" },
{ id: "demo_tab_ps_south", type: "tab", label: "PS South", disabled: false, info: "Pumping Station South (Residential - 1 pump, Manual)" },
{ id: "demo_tab_treatment", type: "tab", label: "Biological Treatment", disabled: false, info: "Merge point, Reactor, Settler, Effluent Measurements" }
);
// =============================================
// 1b. Rename existing WWTP tab
// =============================================
const wwtpTab = byId("demo_tab_wwtp");
wwtpTab.label = "Telemetry / InfluxDB";
wwtpTab.info = "InfluxDB write chain, process debug, Grafana dashboard API, shared infrastructure";
// =============================================
// 1c. Move nodes to new tabs
// =============================================
const moveMap = {
// PS West tab
"demo_comment_ps": "demo_tab_ps_west",
"demo_ps_west": "demo_tab_ps_west",
"demo_pump_w1": "demo_tab_ps_west",
"demo_pump_w2": "demo_tab_ps_west",
"demo_mgc_west": "demo_tab_ps_west",
"demo_inj_west_mode": "demo_tab_ps_west",
"demo_inj_west_flow": "demo_tab_ps_west",
"demo_fn_west_flow_sim": "demo_tab_ps_west",
"demo_inj_w1_mode": "demo_tab_ps_west",
"demo_inj_w2_mode": "demo_tab_ps_west",
"demo_inj_calib_west": "demo_tab_ps_west",
"demo_fn_level_to_pressure_w": "demo_tab_ps_west",
"demo_meas_pt_w_up": "demo_tab_ps_west",
"demo_meas_pt_w_down": "demo_tab_ps_west",
"demo_mon_west": "demo_tab_ps_west",
"demo_link_ps_west_dash": "demo_tab_ps_west",
// PS North tab
"demo_comment_ps_north": "demo_tab_ps_north",
"demo_ps_north": "demo_tab_ps_north",
"demo_pump_n1": "demo_tab_ps_north",
"demo_inj_north_mode": "demo_tab_ps_north",
"demo_inj_north_flow": "demo_tab_ps_north",
"demo_fn_north_flow_sim": "demo_tab_ps_north",
"demo_inj_n1_mode": "demo_tab_ps_north",
"demo_inj_calib_north": "demo_tab_ps_north",
"demo_comment_north_outflow": "demo_tab_ps_north",
"demo_meas_ft_n1": "demo_tab_ps_north",
"demo_fn_level_to_pressure_n": "demo_tab_ps_north",
"demo_meas_pt_n_up": "demo_tab_ps_north",
"demo_meas_pt_n_down": "demo_tab_ps_north",
"demo_mon_north": "demo_tab_ps_north",
"demo_link_ps_north_dash": "demo_tab_ps_north",
// PS South tab
"demo_comment_ps_south": "demo_tab_ps_south",
"demo_ps_south": "demo_tab_ps_south",
"demo_pump_s1": "demo_tab_ps_south",
"demo_inj_south_mode": "demo_tab_ps_south",
"demo_inj_south_flow": "demo_tab_ps_south",
"demo_fn_south_flow_sim": "demo_tab_ps_south",
"demo_inj_s1_mode": "demo_tab_ps_south",
"demo_inj_calib_south": "demo_tab_ps_south",
"demo_fn_level_to_pressure_s": "demo_tab_ps_south",
"demo_meas_pt_s_up": "demo_tab_ps_south",
"demo_meas_pt_s_down": "demo_tab_ps_south",
"demo_mon_south": "demo_tab_ps_south",
"demo_link_ps_south_dash": "demo_tab_ps_south",
// Treatment tab
"demo_comment_treatment": "demo_tab_treatment",
"demo_meas_flow": "demo_tab_treatment",
"demo_meas_do": "demo_tab_treatment",
"demo_meas_nh4": "demo_tab_treatment",
"demo_reactor": "demo_tab_treatment",
"demo_inj_reactor_tick": "demo_tab_treatment",
"demo_settler": "demo_tab_treatment",
"demo_monster": "demo_tab_treatment",
"demo_inj_monster_flow": "demo_tab_treatment",
"demo_fn_monster_flow": "demo_tab_treatment",
"demo_comment_effluent_meas": "demo_tab_treatment",
"demo_meas_eff_flow": "demo_tab_treatment",
"demo_meas_eff_do": "demo_tab_treatment",
"demo_meas_eff_nh4": "demo_tab_treatment",
"demo_meas_eff_no3": "demo_tab_treatment",
"demo_meas_eff_tss": "demo_tab_treatment",
"demo_comment_pressure": "demo_tab_treatment",
"demo_link_reactor_dash": "demo_tab_treatment",
"demo_link_meas_dash": "demo_tab_treatment",
"demo_link_eff_meas_dash": "demo_tab_treatment"
};
for (const [nodeId, tabId] of Object.entries(moveMap)) {
const node = byId(nodeId);
if (node) {
node.z = tabId;
} else {
console.warn(`WARNING: Node ${nodeId} not found for move`);
}
}
// =============================================
// 1c-coords. Recalculate coordinates per tab
// =============================================
// PS West layout (2 pumps + MGC)
const psWestCoords = {
"demo_comment_ps": { x: 340, y: 40 },
"demo_inj_calib_west": { x: 120, y: 80 },
"demo_inj_w1_mode": { x: 120, y: 120 },
"demo_inj_west_mode": { x: 120, y: 200 },
"demo_inj_west_flow": { x: 120, y: 240 },
"demo_inj_w2_mode": { x: 120, y: 320 },
"demo_fn_west_flow_sim": { x: 360, y: 240 },
"demo_pump_w1": { x: 600, y: 120 },
"demo_pump_w2": { x: 600, y: 320 },
"demo_mgc_west": { x: 600, y: 220 },
"demo_ps_west": { x: 860, y: 220 },
"demo_fn_level_to_pressure_w": { x: 360, y: 420 },
"demo_meas_pt_w_up": { x: 560, y: 420 },
"demo_meas_pt_w_down": { x: 560, y: 480 },
"demo_mon_west": { x: 1080, y: 160 },
"demo_link_ps_west_dash": { x: 1080, y: 220 },
};
// PS North layout (1 pump, no MGC)
const psNorthCoords = {
"demo_comment_ps_north": { x: 340, y: 40 },
"demo_inj_calib_north": { x: 120, y: 80 },
"demo_inj_n1_mode": { x: 120, y: 120 },
"demo_inj_north_mode": { x: 120, y: 200 },
"demo_inj_north_flow": { x: 120, y: 240 },
"demo_fn_north_flow_sim": { x: 360, y: 240 },
"demo_pump_n1": { x: 600, y: 120 },
"demo_ps_north": { x: 860, y: 200 },
"demo_comment_north_outflow":{ x: 200, y: 320 },
"demo_meas_ft_n1": { x: 560, y: 340 },
"demo_fn_level_to_pressure_n":{ x: 360, y: 420 },
"demo_meas_pt_n_up": { x: 560, y: 420 },
"demo_meas_pt_n_down": { x: 560, y: 480 },
"demo_mon_north": { x: 1080, y: 140 },
"demo_link_ps_north_dash": { x: 1080, y: 200 },
};
// PS South layout (1 pump, no MGC)
const psSouthCoords = {
"demo_comment_ps_south": { x: 340, y: 40 },
"demo_inj_calib_south": { x: 120, y: 80 },
"demo_inj_s1_mode": { x: 120, y: 120 },
"demo_inj_south_mode": { x: 120, y: 200 },
"demo_inj_south_flow": { x: 120, y: 240 },
"demo_fn_south_flow_sim": { x: 360, y: 240 },
"demo_pump_s1": { x: 600, y: 120 },
"demo_ps_south": { x: 860, y: 200 },
"demo_fn_level_to_pressure_s":{ x: 360, y: 380 },
"demo_meas_pt_s_up": { x: 560, y: 380 },
"demo_meas_pt_s_down": { x: 560, y: 440 },
"demo_mon_south": { x: 1080, y: 140 },
"demo_link_ps_south_dash": { x: 1080, y: 200 },
};
// Treatment layout
const treatmentCoords = {
"demo_comment_treatment": { x: 200, y: 40 },
"demo_meas_flow": { x: 400, y: 120 },
"demo_meas_do": { x: 400, y: 180 },
"demo_meas_nh4": { x: 400, y: 240 },
"demo_inj_reactor_tick": { x: 600, y: 80 },
"demo_reactor": { x: 800, y: 180 },
"demo_settler": { x: 800, y: 320 },
"demo_monster": { x: 800, y: 420 },
"demo_inj_monster_flow": { x: 560, y: 420 },
"demo_fn_monster_flow": { x: 660, y: 460 },
"demo_comment_effluent_meas":{ x: 200, y: 520 },
"demo_meas_eff_flow": { x: 400, y: 560 },
"demo_meas_eff_do": { x: 400, y: 620 },
"demo_meas_eff_nh4": { x: 400, y: 680 },
"demo_meas_eff_no3": { x: 400, y: 740 },
"demo_meas_eff_tss": { x: 400, y: 800 },
"demo_comment_pressure": { x: 200, y: 860 },
"demo_link_reactor_dash": { x: 1020, y: 180 },
"demo_link_meas_dash": { x: 620, y: 180 },
"demo_link_eff_meas_dash": { x: 620, y: 620 },
};
// Apply coordinates
for (const [nodeId, coords] of Object.entries({...psWestCoords, ...psNorthCoords, ...psSouthCoords, ...treatmentCoords})) {
const node = byId(nodeId);
if (node) {
node.x = coords.x;
node.y = coords.y;
}
}
// =============================================
// 1d. Create per-tab link-out nodes
// =============================================
// Determine which tab each moved node belongs to
const tabForNode = {};
for (const n of flow) {
if (n.z) tabForNode[n.id] = n.z;
}
// Map from tab → influx link-out ID
const influxLinkOutMap = {
"demo_tab_ps_west": "demo_link_influx_out_west",
"demo_tab_ps_north": "demo_link_influx_out_north",
"demo_tab_ps_south": "demo_link_influx_out_south",
"demo_tab_treatment": "demo_link_influx_out_treatment",
};
// Map from tab → process link-out ID
const processLinkOutMap = {
"demo_tab_ps_west": "demo_link_process_out_west",
"demo_tab_ps_north": "demo_link_process_out_north",
"demo_tab_ps_south": "demo_link_process_out_south",
"demo_tab_treatment": "demo_link_process_out_treatment",
};
// Link-out node positions per tab
const linkOutPositions = {
"demo_tab_ps_west": { influx: { x: 1080, y: 280 }, process: { x: 1080, y: 320 } },
"demo_tab_ps_north": { influx: { x: 1080, y: 260 }, process: { x: 1080, y: 300 } },
"demo_tab_ps_south": { influx: { x: 1080, y: 260 }, process: { x: 1080, y: 300 } },
"demo_tab_treatment": { influx: { x: 1020, y: 280 }, process: { x: 1020, y: 320 } },
};
// Create influx link-out nodes
for (const [tabId, nodeId] of Object.entries(influxLinkOutMap)) {
const pos = linkOutPositions[tabId].influx;
flow.push({
id: nodeId,
type: "link out",
z: tabId,
name: "→ InfluxDB",
mode: "link",
links: ["demo_link_influx_in"],
x: pos.x,
y: pos.y
});
}
// Create process link-out nodes
for (const [tabId, nodeId] of Object.entries(processLinkOutMap)) {
const pos = linkOutPositions[tabId].process;
flow.push({
id: nodeId,
type: "link out",
z: tabId,
name: "→ Process debug",
mode: "link",
links: ["demo_link_process_in"],
x: pos.x,
y: pos.y
});
}
// =============================================
// 1d-rewire. Rewire nodes to use local link-outs
// =============================================
// For every node that references "demo_link_influx_out" or "demo_link_process_out"
// in its wires, replace with the per-tab version
for (const node of flow) {
if (!node.wires || !node.z) continue;
const tab = node.z;
const localInflux = influxLinkOutMap[tab];
const localProcess = processLinkOutMap[tab];
for (let portIdx = 0; portIdx < node.wires.length; portIdx++) {
for (let wireIdx = 0; wireIdx < node.wires[portIdx].length; wireIdx++) {
if (node.wires[portIdx][wireIdx] === "demo_link_influx_out" && localInflux) {
node.wires[portIdx][wireIdx] = localInflux;
}
if (node.wires[portIdx][wireIdx] === "demo_link_process_out" && localProcess) {
node.wires[portIdx][wireIdx] = localProcess;
}
}
}
}
// Update the link-in nodes to reference all new link-out IDs
const influxIn = byId("demo_link_influx_in");
influxIn.links = Object.values(influxLinkOutMap);
// Also keep the old one if any nodes on the telemetry tab still reference it
// (the dashapi, telemetry nodes that stayed on demo_tab_wwtp)
influxIn.links.push("demo_link_influx_out");
const processIn = byId("demo_link_process_in");
processIn.links = Object.values(processLinkOutMap);
processIn.links.push("demo_link_process_out");
// Keep old link-out nodes on telemetry tab (they may still be needed
// by nodes that remain there, like dashapi)
// Update their links arrays too
const oldInfluxOut = byId("demo_link_influx_out");
if (oldInfluxOut) {
oldInfluxOut.links = ["demo_link_influx_in"];
// Move to bottom of telemetry tab
oldInfluxOut.x = 1135;
oldInfluxOut.y = 500;
}
const oldProcessOut = byId("demo_link_process_out");
if (oldProcessOut) {
oldProcessOut.links = ["demo_link_process_in"];
oldProcessOut.x = 1135;
oldProcessOut.y = 540;
}
// =============================================
// Validate
// =============================================
const tabCounts = {};
for (const n of flow) {
if (n.z) {
tabCounts[n.z] = (tabCounts[n.z] || 0) + 1;
}
}
console.log('Nodes per tab:', JSON.stringify(tabCounts, null, 2));
console.log('Total nodes:', flow.length);
// Check for broken wire references
const allIds = new Set(flow.map(n => n.id));
let brokenWires = 0;
for (const n of flow) {
if (!n.wires) continue;
for (const port of n.wires) {
for (const target of port) {
if (!allIds.has(target)) {
console.warn(`BROKEN WIRE: ${n.id}${target}`);
brokenWires++;
}
}
}
}
if (brokenWires === 0) console.log('All wire references valid ✓');
// Check link-in/link-out pairing
for (const n of flow) {
if (n.type === 'link out' && n.links) {
for (const linkTarget of n.links) {
if (!allIds.has(linkTarget)) {
console.warn(`BROKEN LINK: ${n.id} links to missing ${linkTarget}`);
}
}
}
if (n.type === 'link in' && n.links) {
for (const linkSource of n.links) {
if (!allIds.has(linkSource)) {
console.warn(`BROKEN LINK: ${n.id} expects link from missing ${linkSource}`);
}
}
}
}
// Write
fs.writeFileSync(FLOW_PATH, JSON.stringify(flow, null, 2) + '\n');
console.log(`\nWrote ${FLOW_PATH} (${flow.length} nodes)`);