- 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>
381 lines
14 KiB
JavaScript
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)`);
|