feat(dashboardapi): no-data-duplication rule for parent dashboards (#39)

When generateDashboardsForGraph builds a root dashboard for a parent (e.g.
pumpingStation) and a set of child dashboards (e.g. measurements), it now
removes any non-row panel from the root whose meta.emittedFields are fully
covered by panels declared in any child dashboard. Result: the parent
shows only metrics its children don't already plot, eliminating redundant
rendering of the same series in two dashboards.

- config/pumpingStation.json: 11 non-row panels annotated with
  meta.emittedFields (Direction, Time Left, Flow Source, Fill %, Level (x2),
  Volume, Net Flow Rate, Inflow+Outflow, Heights, Volume Limits).
- src/specificClass.js: generateDashboardsForGraph runs the parent-panel
  filter after composing children; row panels always kept; panels without
  emittedFields declaration always kept (no silent removal).
- test/basic/slice39-no-duplication.basic.test.js: 4 cases — annotation
  presence, child-covered removal, no-overlap preservation, row preservation.

Closes #39
This commit is contained in:
2026-05-26 18:01:58 +02:00
parent e5099de986
commit a76f22281e
3 changed files with 735 additions and 77 deletions

View File

@@ -228,6 +228,32 @@ class DashboardApi {
if (childDash) results.push(childDash);
}
// No-data-duplication rule (PRD F-5, #39): remove root panels whose
// emittedFields are fully covered by panels on child dashboards. The
// parent then shows only metrics its children don't already plot,
// avoiding redundant rendering of the same series in two places.
if (children.length > 0 && rootDash.dashboard) {
const childCoveredFields = new Set();
for (const dash of results.slice(1)) {
for (const f of this.collectEmittedFields(dash.dashboard)) childCoveredFields.add(f);
}
const before = rootDash.dashboard.panels.length;
rootDash.dashboard.panels = rootDash.dashboard.panels.filter((p) => {
if (p.type === 'row') return true; // never drop rows
const fields = p?.meta?.emittedFields;
if (!Array.isArray(fields) || fields.length === 0) return true; // no declaration, keep
return !fields.every((f) => childCoveredFields.has(f));
});
if (this.logger?.debug && before !== rootDash.dashboard.panels.length) {
this.logger.debug({
event: 'parent-panels-deduped',
before,
after: rootDash.dashboard.panels.length,
rootTitle: rootDash.title,
});
}
}
// Add links from the root dashboard to children dashboards (when possible)
if (children.length > 0) {
rootDash.dashboard.links = Array.isArray(rootDash.dashboard.links) ? rootDash.dashboard.links : [];