'use strict'; // Resolve a child's source object from a registration payload. // Payload may be: a string (node id) | { source: {...} } | { config: {...} }. function resolveChildSource(payload, ctx) { if (payload?.source?.config) return payload.source; if (payload?.config) return { config: payload.config }; if (typeof payload === 'string') { const childNode = resolveChildNode(payload, ctx); return childNode?.source || null; } return null; } function resolveChildNode(childId, ctx) { const runtimeNode = ctx.RED?.nodes?.getNode?.(childId); if (runtimeNode?.source?.config) return runtimeNode; const flowNode = ctx.node?._flow?.getNode?.(childId); if (flowNode?.source?.config) return flowNode; return runtimeNode || flowNode || null; } // On child.register: build the dashboard graph (root + direct children) and // emit one Grafana upsert HTTP request per dashboard on Port 0. // // Diff-skip behavior (PRD F-1, S1 spike #32): if the latest flows:started // payload's `diff` indicates that NEITHER the dashboardAPI itself NOR this // child NOR its grandchildren changed, skip composition and log no-diff. The // first call after startup (no cached diff yet) regenerates unconditionally. function registerChild(source, msg, ctx) { const childSource = resolveChildSource(msg.payload, ctx); if (!childSource?.config) { throw new Error('Missing or invalid child node'); } const subtreeIds = source.subtreeIdsFor(ctx.node?.id, childSource); const changed = source.subtreeChanged(source.lastFlowsStartedDiff, subtreeIds); if (!changed) { if (source.logger?.info) { source.logger.info({ event: 'regen-skipped', outcome: 'no-diff', trigger: 'child.register', dashboardApiId: ctx.node?.id, childId: childSource?.config?.general?.id, subtreeSize: subtreeIds.size, }); } return; } const dashboards = source.generateDashboardsForGraph(childSource, { includeChildren: Boolean(msg.includeChildren ?? true), }); const url = source.grafanaUpsertUrl(); const headers = { Accept: 'application/json', 'Content-Type': 'application/json' }; const token = source.config?.grafanaConnector?.bearerToken; if (token) headers.Authorization = `Bearer ${token}`; for (const dash of dashboards) { ctx.send({ ...msg, topic: 'create', url, method: 'POST', headers, payload: source.buildUpsertRequest({ dashboard: dash.dashboard, folderUid: source.config?.grafanaConnector?.folderUid || undefined, overwrite: true, }), meta: { nodeId: dash.nodeId, softwareType: dash.softwareType, uid: dash.uid, title: dash.title, }, }); } } module.exports = { registerChild };