From 547333be7d6a442c24d0066bd8417bd151bb2dd2 Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:16:58 +0100 Subject: [PATCH] working --- src/nodeClass.js | 21 +++++++++++++++------ src/specificClass.js | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 2b6c648..ff4d9d2 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -1,6 +1,10 @@ const { outputUtils } = require('generalFunctions'); const Specific = require('./specificClass'); +/** + * Node-RED wrapper for dashboard generation requests. + * It listens for `registerChild` messages and emits Grafana upsert requests. + */ class nodeClass { constructor(uiConfig, RED, nodeInstance, nameOfNode) { this.node = nodeInstance; @@ -44,7 +48,7 @@ class nodeClass { this.node.on('input', async (msg, send, done) => { try { if (msg.topic !== 'registerChild') { - done(); + if (typeof done === 'function') done(); return; } @@ -52,9 +56,12 @@ class nodeClass { const childObj = this.RED.nodes.getNode(childId); const childSource = childObj?.source; if (!childSource?.config) { - throw new Error(`Missing child source/config for id=${childId}`); + this.node.warn(`registerChild skipped: missing child source/config for id=${childId}`); + if (typeof done === 'function') done(); + return; } + // Generate one dashboard for the root source and optionally its registered children. const dashboards = this.source.generateDashboardsForGraph(childSource, { includeChildren: Boolean(msg.includeChildren ?? true), }); @@ -69,6 +76,7 @@ class nodeClass { } for (const dash of dashboards) { + // Forward dashboard definitions to an HTTP request node configured for Grafana API. const payload = this.source.buildUpsertRequest({ dashboard: dash.dashboard, folderId: 0, overwrite: true }); send({ topic: 'grafana.dashboard.upsert', @@ -85,19 +93,20 @@ class nodeClass { }); } - done(); + if (typeof done === 'function') done(); } catch (error) { this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' }); this.node.error(error?.message || error, msg); - done(error); + if (typeof done === 'function') done(); } }); } _attachCloseHandler() { - this.node.on('close', (done) => done()); + this.node.on('close', (done) => { + if (typeof done === 'function') done(); + }); } } module.exports = nodeClass; - diff --git a/src/specificClass.js b/src/specificClass.js index 54d392e..858a400 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -45,6 +45,10 @@ function updateTemplatingVar(dashboard, varName, value) { variable.query = value; } +/** + * Dashboard domain service. + * Builds Grafana dashboard payloads from EVOLV node config and child topology. + */ class DashboardApi { constructor(config = {}) { this.config = { @@ -88,11 +92,13 @@ class DashboardApi { if (fs.existsSync(fullPath)) return fullPath; } - throw new Error(`No dashboard template found for softwareType=${st}`); + this.logger.warn(`No dashboard template found for softwareType=${st}`); + return null; } loadTemplate(softwareType) { const templatePath = this._templateFileForSoftwareType(softwareType); + if (!templatePath) return null; const raw = fs.readFileSync(templatePath, 'utf8'); return JSON.parse(raw); } @@ -111,7 +117,12 @@ class DashboardApi { const measurementName = `${softwareType}_${nodeId}`; const title = nodeConfig?.general?.name || String(nodeId); + // Missing templates are treated as non-fatal: we skip only that dashboard. const dashboard = this.loadTemplate(softwareType); + if (!dashboard) { + this.logger.warn(`Skipping dashboard generation: no template for softwareType=${softwareType}`); + return null; + } const uid = stableUid(`${softwareType}:${nodeId}`); dashboard.id = null; @@ -150,11 +161,13 @@ class DashboardApi { generateDashboardsForGraph(rootSource, { includeChildren = true } = {}) { if (!rootSource?.config) { - throw new Error('generateDashboardsForGraph requires a node source with `.config`'); + this.logger.warn('generateDashboardsForGraph skipped: root source missing config'); + return []; } const rootPosition = rootSource?.positionVsParent || rootSource?.config?.functionality?.positionVsParent; const rootDash = this.buildDashboard({ nodeConfig: rootSource.config, positionVsParent: rootPosition }); + if (!rootDash) return []; const results = [rootDash]; @@ -163,7 +176,7 @@ class DashboardApi { const children = this.extractChildren(rootSource); for (const { childSource, positionVsParent } of children) { const childDash = this.buildDashboard({ nodeConfig: childSource.config, positionVsParent }); - results.push(childDash); + if (childDash) results.push(childDash); } // Add links from the root dashboard to children dashboards (when possible)