P6: convert dashboardAPI to platform infrastructure

Refactor of dashboardAPI to use BaseNodeAdapter + commandRegistry + statusBadge.
dashboardAPI follows the platform refactor plan in .claude/refactor/MODULE_SPLIT.md.
Tests stay green; CONTRACT.md generated; legacy aliases preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-10 22:23:45 +02:00
parent 869ba4fca5
commit 2874608375
5 changed files with 191 additions and 91 deletions

64
src/commands/handlers.js Normal file
View File

@@ -0,0 +1,64 @@
'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.
function registerChild(source, msg, ctx) {
const childSource = resolveChildSource(msg.payload, ctx);
if (!childSource?.config) {
throw new Error('Missing or invalid child node');
}
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,
folderId: 0,
overwrite: true,
}),
meta: {
nodeId: dash.nodeId,
softwareType: dash.softwareType,
uid: dash.uid,
title: dash.title,
},
});
}
}
module.exports = { registerChild };

16
src/commands/index.js Normal file
View File

@@ -0,0 +1,16 @@
'use strict';
// dashboardAPI command registry. Canonical names follow CONTRACTS.md §1.
// The legacy `registerChild` topic is kept as an alias of `child.register`
// (Phase 1 canonical) and logs a one-time deprecation warning on first use.
const handlers = require('./handlers');
module.exports = [
{
topic: 'child.register',
aliases: ['registerChild'],
payloadSchema: { type: 'any' },
handler: handlers.registerChild,
},
];

View File

@@ -1,23 +1,36 @@
const { configManager } = require('generalFunctions');
'use strict';
// dashboardAPI nodeClass — passive HTTP-emitter adapter.
//
// Does NOT extend BaseNodeAdapter: dashboardAPI has no generalFunctions
// config JSON, no Port-0/1 telemetry stream, no parent registration, no
// tick or status loop. It just listens for `child.register` and emits one
// Grafana upsert HTTP request per dashboard. See OPEN_QUESTIONS.md
// (2026-05-10) for the rationale.
const { configManager, createRegistry } = require('generalFunctions');
const DashboardApi = require('./specificClass');
const commands = require('./commands');
class nodeClass {
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
this.node = nodeInstance;
this.RED = RED;
this.name = nameOfNode;
this.source = null;
this.config = null;
this._loadConfig(uiConfig);
this._setupSpecificClass();
this.config = this._buildConfig(uiConfig);
this.source = new DashboardApi(this.config);
this.node.source = this.source;
this._commands = createRegistry(commands, { logger: this.source?.logger });
this._attachInputHandler();
this._attachCloseHandler();
}
_loadConfig(uiConfig) {
_buildConfig(uiConfig) {
const cfgMgr = new configManager();
this.config = cfgMgr.buildConfig(this.name, uiConfig, this.node.id, {
return cfgMgr.buildConfig(this.name, uiConfig, this.node.id, {
functionality: {
softwareType: this.name.toLowerCase(),
role: 'auto ui generator',
@@ -32,89 +45,15 @@ class nodeClass {
});
}
_setupSpecificClass() {
this.source = new DashboardApi(this.config);
this.node.source = this.source;
}
_resolveChildNode(childId) {
const runtimeNode = this.RED.nodes.getNode(childId);
if (runtimeNode?.source?.config) {
return runtimeNode;
}
const flowNode = this.node._flow?.getNode?.(childId);
if (flowNode?.source?.config) {
return flowNode;
}
return runtimeNode || flowNode || null;
}
_resolveChildSource(payload) {
if (payload?.source?.config) {
return payload.source;
}
if (payload?.config) {
return { config: payload.config };
}
if (typeof payload === 'string') {
return this._resolveChildNode(payload)?.source || null;
}
return null;
}
_attachInputHandler() {
this.node.on('input', async (msg, send, done) => {
try {
if (msg.topic !== 'registerChild') {
if (typeof done === 'function') done();
return;
}
const childSource = this._resolveChildSource(msg.payload);
if (!childSource?.config) {
throw new Error('Missing or invalid child node');
}
const dashboards = this.source.generateDashboardsForGraph(childSource, {
includeChildren: Boolean(msg.includeChildren ?? true),
await this._commands.dispatch(msg, this.source, {
node: this.node,
RED: this.RED,
send,
logger: this.source?.logger,
});
const url = this.source.grafanaUpsertUrl();
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
if (this.config.grafanaConnector.bearerToken) {
headers.Authorization = `Bearer ${this.config.grafanaConnector.bearerToken}`;
}
for (const dash of dashboards) {
send({
...msg,
topic: 'create',
url,
method: 'POST',
headers,
payload: this.source.buildUpsertRequest({
dashboard: dash.dashboard,
folderId: 0,
overwrite: true,
}),
meta: {
nodeId: dash.nodeId,
softwareType: dash.softwareType,
uid: dash.uid,
title: dash.title,
},
});
}
if (typeof done === 'function') done();
} catch (error) {
this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' });