feat(dashboardapi): manual regen via msg.topic == regenerate-dashboard (#41)
Adds an explicit topic for operators (and the dashboardAPI v2 manual escape hatch from PRD F-12). On `regenerate-dashboard`, dashboardAPI iterates every child source cached by prior `child.register` messages and re-emits Grafana upsert messages — bypassing the diff-skip predicate from #36. - src/specificClass.js: light state cache (recordChild / cachedChildSources). - src/commands/handlers.js: refactor shared emit path; emitDashboardsFor() used by both child.register and regenerateDashboard; meta.trigger distinguishes the two for downstream filtering. - src/commands/index.js: register 'regenerate-dashboard' (alias 'regen'). - CONTRACT.md: document the new topic. - test/basic/slice41-manual-regen.basic.test.js: 5 cases covering cache semantics, no-op for empty cache, bypass-predicate, trigger stamp on both paths, registry exposure. Closes #41
This commit is contained in:
@@ -22,35 +22,9 @@ function resolveChildNode(childId, ctx) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Shared emit path used by both child.register (auto, deploy-driven) and
|
||||
// regenerate-dashboard (manual). `trigger` distinguishes the two for logs.
|
||||
function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
const dashboards = source.generateDashboardsForGraph(childSource, {
|
||||
includeChildren: Boolean(msg.includeChildren ?? true),
|
||||
});
|
||||
@@ -77,9 +51,73 @@ function registerChild(source, msg, ctx) {
|
||||
softwareType: dash.softwareType,
|
||||
uid: dash.uid,
|
||||
title: dash.title,
|
||||
trigger,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (source.logger?.info) {
|
||||
source.logger.info({
|
||||
event: 'regen-emitted',
|
||||
trigger,
|
||||
dashboardApiId: ctx.node?.id,
|
||||
childId: childSource?.config?.general?.id,
|
||||
dashboardCount: dashboards.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { registerChild };
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Cache the child source for later manual regen (#41).
|
||||
source.recordChild?.(childSource);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
emitDashboardsFor(source, childSource, ctx, msg, 'child.register');
|
||||
}
|
||||
|
||||
// On regenerate-dashboard: re-emit dashboards for every cached child source,
|
||||
// bypassing the diff predicate. Useful as an operator escape hatch when
|
||||
// auto-regen missed an edge case or when the operator just wants to refresh.
|
||||
function regenerateDashboard(source, msg, ctx) {
|
||||
const cached = source.cachedChildSources?.() || [];
|
||||
if (source.logger?.info) {
|
||||
source.logger.info({
|
||||
event: 'manual-regen-requested',
|
||||
trigger: 'manual',
|
||||
dashboardApiId: ctx.node?.id,
|
||||
cachedChildCount: cached.length,
|
||||
});
|
||||
}
|
||||
for (const childSource of cached) {
|
||||
emitDashboardsFor(source, childSource, ctx, msg, 'manual');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { registerChild, regenerateDashboard };
|
||||
|
||||
Reference in New Issue
Block a user