`,
templateScope: "local",
className: "",
x: 510, y: 960,
wires: [[]]
});
// =============================================
// 3c. KPI gauges on overview
// =============================================
// Total Influent Flow gauge
flow.push({
id: "demo_gauge_overview_flow",
type: "ui-gauge",
z: "demo_tab_dashboard",
group: "demo_ui_grp_overview_kpi",
name: "Total Influent Flow",
gtype: "gauge-34",
gstyle: "Rounded",
title: "Influent Flow",
units: "m\u00b3/h",
prefix: "",
suffix: "m\u00b3/h",
min: 0,
max: 500,
segments: [
{ color: "#2196f3", from: 0 },
{ color: "#4caf50", from: 50 },
{ color: "#ff9800", from: 350 },
{ color: "#f44336", from: 450 }
],
width: 3,
height: 4,
order: 1,
className: "",
x: 510, y: 1020,
wires: []
});
// Reactor DO gauge
flow.push({
id: "demo_gauge_overview_do",
type: "ui-gauge",
z: "demo_tab_dashboard",
group: "demo_ui_grp_overview_kpi",
name: "Reactor DO",
gtype: "gauge-34",
gstyle: "Rounded",
title: "Reactor DO",
units: "mg/L",
prefix: "",
suffix: "mg/L",
min: 0,
max: 10,
segments: [
{ color: "#f44336", from: 0 },
{ color: "#ff9800", from: 1 },
{ color: "#4caf50", from: 2 },
{ color: "#ff9800", from: 6 },
{ color: "#f44336", from: 8 }
],
width: 3,
height: 4,
order: 2,
className: "",
x: 510, y: 1060,
wires: []
});
// Effluent TSS gauge
flow.push({
id: "demo_gauge_overview_tss",
type: "ui-gauge",
z: "demo_tab_dashboard",
group: "demo_ui_grp_overview_kpi",
name: "Effluent TSS",
gtype: "gauge-34",
gstyle: "Rounded",
title: "Effluent TSS",
units: "mg/L",
prefix: "",
suffix: "mg/L",
min: 0,
max: 50,
segments: [
{ color: "#4caf50", from: 0 },
{ color: "#ff9800", from: 25 },
{ color: "#f44336", from: 40 }
],
width: 3,
height: 4,
order: 3,
className: "",
x: 510, y: 1100,
wires: []
});
// Effluent NH4 gauge
flow.push({
id: "demo_gauge_overview_nh4",
type: "ui-gauge",
z: "demo_tab_dashboard",
group: "demo_ui_grp_overview_kpi",
name: "Effluent NH4",
gtype: "gauge-34",
gstyle: "Rounded",
title: "Effluent NH4",
units: "mg/L",
prefix: "",
suffix: "mg/L",
min: 0,
max: 20,
segments: [
{ color: "#4caf50", from: 0 },
{ color: "#ff9800", from: 5 },
{ color: "#f44336", from: 10 }
],
width: 3,
height: 4,
order: 4,
className: "",
x: 510, y: 1140,
wires: []
});
// =============================================
// 3d. Reorder all page navigation
// =============================================
const pageOrders = {
"demo_ui_page_overview": 0,
"demo_ui_page_influent": 1,
"demo_ui_page_treatment": 5,
"demo_ui_page_telemetry": 6,
};
for (const [pageId, order] of Object.entries(pageOrders)) {
const page = byId(pageId);
if (page) page.order = order;
}
// =============================================
// Feed chain vis and KPIs from merge + reactor + effluent
// We need to also wire the overview_template to receive reactor/eff data
// The parse functions already wire to the template and gauges separately
// But the template needs ALL data sources - let's connect reactor and eff parsers to it too
// =============================================
// Actually, the template needs multiple inputs. Let's connect reactor and eff parse outputs too.
// Modify overview reactor parse to also send to template
const reactorParse = byId("demo_fn_overview_reactor_parse");
// Currently wires to demo_gauge_overview_do. Add template as well.
reactorParse.func = `const p = msg.payload || {};
if (!p.C || !Array.isArray(p.C)) return null;
flow.set('overview_reactor', p);
// Output 1: DO gauge, Output 2: to chain template
const doVal = Math.round(p.C[0]*100)/100;
return [
{ topic: 'Reactor DO', payload: doVal },
{ topic: 'Reactor DO', payload: doVal }
];`;
reactorParse.outputs = 2;
reactorParse.wires = [["demo_gauge_overview_do"], ["demo_overview_template"]];
// Same for effluent parse - add template output
const effParse = byId("demo_fn_overview_eff_parse");
effParse.func = `const p = msg.payload || {};
const topic = msg.topic || '';
const val = Number(p.mAbs);
if (!Number.isFinite(val)) return null;
const rounded = Math.round(val*100)/100;
// Route to appropriate gauge + template based on measurement type
if (topic.includes('TSS') || topic.includes('tss')) {
return [{ topic: 'Effluent TSS', payload: rounded }, null, { topic: 'Effluent TSS', payload: rounded }];
}
if (topic.includes('NH4') || topic.includes('ammonium')) {
return [null, { topic: 'Effluent NH4', payload: rounded }, { topic: 'Effluent NH4', payload: rounded }];
}
return [null, null, null];`;
effParse.outputs = 3;
effParse.wires = [["demo_gauge_overview_tss"], ["demo_gauge_overview_nh4"], ["demo_overview_template"]];
// =============================================
// Validate
// =============================================
const allIds = new Set(flow.map(n => n.id));
let issues = 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}`);
issues++;
}
}
}
if (n.type === 'link out' && n.links) {
for (const lt of n.links) {
if (!allIds.has(lt)) { console.warn(`BROKEN LINK OUT: ${n.id} → ${lt}`); issues++; }
}
}
if (n.type === 'link in' && n.links) {
for (const ls of n.links) {
if (!allIds.has(ls)) { console.warn(`BROKEN LINK IN: ${n.id} ← ${ls}`); issues++; }
}
}
}
if (issues === 0) console.log('All references valid ✓');
console.log('Total nodes:', flow.length);
// Write
fs.writeFileSync(FLOW_PATH, JSON.stringify(flow, null, 2) + '\n');
console.log(`Wrote ${FLOW_PATH}`);