Compare commits

..

2 Commits

Author SHA1 Message Date
znetsixe
998b2002e9 docs: add CLAUDE.md with S88 classification and superproject rule reference
References the flow-layout rule set in the EVOLV superproject
(.claude/rules/node-red-flow-layout.md) so Claude Code sessions working
in this repo know the S88 level, colour, and placement lane for this node.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:47:22 +02:00
znetsixe
fb8d5c03e6 fix(editor): asset/logger/position menus broken by TDZ ReferenceError in oneditprepare
The previous oneditprepare ran applyMode(initialMode) early in the
function, which called validateChannelsJson(), which referenced const
declarations (channelsArea, channelsHint) that were declared later in
the same function. JavaScript hoists const into the Temporal Dead Zone,
so accessing them before the declaration line throws a ReferenceError.
That uncaught throw aborted the rest of oneditprepare — including the
waitForMenuData() call that initialises the asset / logger / position
menu placeholders. Symptom for the user: opening a measurement node in
the editor showed Mode + analog fields but the asset menu was empty.

Fixes:

1. Move waitForMenuData() to the very top of oneditprepare so the
   shared menu init is independent of any later mode-block work. Even
   if the mode logic ever throws again, the asset / logger / position
   menus still render.

2. Resolve every DOM reference (modeSelect, analogBlock, digitalBlock,
   modeHint, channelsArea, channelsHint) at the top of the function
   before any helper that touches them is invoked. validateChannelsJson
   and applyMode now read closed-over names that are guaranteed to be
   initialised.

3. Guard applyMode(initialMode) with try/catch as defense in depth and
   add null-checks on every DOM reference. A future template change
   that drops one of the IDs will only no-op rather than break the
   editor.

No runtime change. 71/71 tests still green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:15:06 +02:00
2 changed files with 73 additions and 37 deletions

23
CLAUDE.md Normal file
View File

@@ -0,0 +1,23 @@
# measurement — Claude Code context
Sensor signal conditioning and data quality.
Part of the [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV) wastewater-automation platform.
## S88 classification
| Level | Colour | Placement lane |
|---|---|---|
| **Control Module** | `#a9daee` | L2 |
## Flow layout rules
When wiring this node into a multi-node demo or production flow, follow the
placement rule set in the **EVOLV superproject**:
> `.claude/rules/node-red-flow-layout.md` (in the EVOLV repo root)
Key points for this node:
- Place on lane **L2** (x-position per the lane table in the rule).
- Stack same-level siblings vertically.
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
- Wrap in a Node-RED group box coloured `#a9daee` (Control Module).

View File

@@ -74,44 +74,50 @@
oneditprepare: function() {
const node = this;
// === Mode selector — TOP-LEVEL hierarchy ===
// The Input Mode drives whether analog-pipeline fields or the
// digital channels editor are shown. Initialize the <select> from
// the saved node value, fall back to 'analog' for legacy nodes
// that were saved before the mode field existed.
const modeSelect = document.getElementById('node-input-mode');
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
modeSelect.value = initialMode;
// === Asset / logger / position placeholders (dynamic menus) ===
// Kick these off FIRST so that any error in the downstream mode
// logic can never block the shared menus. Historical regression:
// a ReferenceError in the mode block aborted oneditprepare and
// stopped the asset menu from rendering at all.
const waitForMenuData = () => {
if (window.EVOLV?.nodes?.measurement?.initEditor) {
window.EVOLV.nodes.measurement.initEditor(node);
} else {
setTimeout(waitForMenuData, 50);
}
};
waitForMenuData();
// IMPORTANT: all DOM references are resolved up front so helper
// functions called during initial applyMode() don't trip over the
// Temporal Dead Zone on later `const` declarations.
const modeSelect = document.getElementById('node-input-mode');
const analogBlock = document.getElementById('analog-only-fields');
const digitalBlock = document.getElementById('digital-only-fields');
const modeHint = document.getElementById('mode-hint');
function applyMode(mode) {
const isDigital = mode === 'digital';
analogBlock.style.display = isDigital ? 'none' : 'block';
digitalBlock.style.display = isDigital ? 'block' : 'none';
if (modeHint) {
modeHint.textContent = isDigital
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
}
validateChannelsJson();
}
modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
applyMode(initialMode);
// === Channels JSON live validation (digital only) ===
const channelsArea = document.getElementById('node-input-channels');
const channelsHint = document.getElementById('channels-validation');
// Initialise the mode <select> from the saved node.mode. Legacy
// nodes (saved before the mode field existed) fall back to
// 'analog' so they keep behaving exactly like before.
const initialMode = (node.mode === 'digital' || node.mode === 'analog') ? node.mode : 'analog';
if (modeSelect) modeSelect.value = initialMode;
// Populate the channels textarea from the saved node.channels
// (stored as a raw JSON string; parsing happens server-side).
if (channelsArea && typeof node.channels === 'string') {
channelsArea.value = node.channels;
}
function validateChannelsJson() {
if (!channelsHint) return;
if (modeSelect.value !== 'digital') { channelsHint.textContent = ''; return; }
const raw = (channelsArea.value || '').trim();
if (!modeSelect || modeSelect.value !== 'digital') {
channelsHint.textContent = '';
return;
}
const raw = (channelsArea && channelsArea.value || '').trim();
if (!raw || raw === '[]') {
channelsHint.innerHTML = '<span style="color:#b45309;">Digital mode with no channels — no measurements will be emitted.</span>';
return;
@@ -120,7 +126,7 @@
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) throw new Error('must be an array');
const missing = parsed
.map((c, i) => (c && c.key && c.type ? null : `entry ${i}: missing key or type`))
.map((c, i) => (c && c.key && c.type ? null : 'entry ' + i + ': missing key or type'))
.filter(Boolean);
if (missing.length) {
channelsHint.innerHTML = '<span style="color:#b45309;">' + missing.join('; ') + '</span>';
@@ -131,17 +137,24 @@
channelsHint.innerHTML = '<span style="color:#b91c1c;">Invalid JSON: ' + e.message + '</span>';
}
}
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
// === Asset / logger / position placeholders (dynamic menus) ===
const waitForMenuData = () => {
if (window.EVOLV?.nodes?.measurement?.initEditor) {
window.EVOLV.nodes.measurement.initEditor(node);
} else {
setTimeout(waitForMenuData, 50);
function applyMode(mode) {
const isDigital = mode === 'digital';
if (analogBlock) analogBlock.style.display = isDigital ? 'none' : 'block';
if (digitalBlock) digitalBlock.style.display = isDigital ? 'block' : 'none';
if (modeHint) {
modeHint.textContent = isDigital
? 'msg.payload must be an OBJECT, e.g. {"temperature": 22.5, "humidity": 45}. Define each key below.'
: 'msg.payload must be a NUMBER (or numeric string). Configure scaling/smoothing below.';
}
validateChannelsJson();
}
if (modeSelect) modeSelect.addEventListener('change', (e) => applyMode(e.target.value));
if (channelsArea) channelsArea.addEventListener('input', validateChannelsJson);
try { applyMode(initialMode); } catch (e) {
console.error('measurement: applyMode failed', e);
}
};
waitForMenuData();
// === Smoothing method dropdown (analog only) ===
const smoothMethodSelect = document.getElementById('node-input-smooth_method');