Canvas frame logical width: 480 → 400 px (was leaving ~104 px of empty
space on the right inside the card). Panel grid width: 8 → 6 cols so the
card pixel width matches the frame logical width and content fills it
without horizontal padding, instead of letterboxing in the centre.
Bottom readouts repositioned to fit within 400 px (level/volume/fill all
inside the new frame width) and per-field decimal overrides added so unit
formatting doesn't truncate ('100.00 mm' fits in the value label width).
Freed grid cols flow to the Level + Volume timeseries on the right
(w:12 → 14 each, x:12 → 10) so the right half consumes the rest of the
row without a gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Basin row grows from h:10 to h:20. Bar gauge, Canvas, and Level/Volume
timeseries all scale proportionally. Canvas internal frame doubled (480x600)
and tank rectangle stretched (height 240→520) so the canvas content fills
the panel instead of letterboxing in the top half. Bottom readouts moved
from y=280 to y=562 to stay just below the taller tank floor.
Flow row + its panels shifted down by 10 grid rows to make room.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the configuration row's Heights + Volume Limits stat panels and
the radial Fill % gauge with an integrated basin visual that conveys tank
geometry and live water level at a glance.
Configuration row → Basin row:
- Vertical bar gauge bound to level (m) with min=0/max=basinHeight and
thresholds at outflow/dryRun/inflow/highSafety/overflow safety levels.
- Canvas panel with tank outline, zone tints (dead/operating/highSafety/
spill), threshold lines + named labels, and live numeric readouts for
each threshold value plus current level/volume/fill at the bottom.
- Level + Volume timeseries moved next to the basin visual so the row
reads as basin → trends left-to-right.
Other layout polish:
- Status row Fill % gauge removed; remaining 4 stats widen to w:6 each.
- Old "Basin" row header dropped (its panels migrated into the new row).
- Configuration row renamed to "Basin".
Mechanics:
- dashboardAPI substitutes mustache {{var}} placeholders in templates at
JSON.parse time. Per-softwareType var sets live in _templateVarsForNode;
pumpingStation gets basin geometry + derived safety levels + canvas
pixel y-positions + min-gap-enforced label positions.
- Mustache braces stay distinct from Grafana's ${var} dashboard variables.
- Canvas Flux query pivots heights + predicted level/volume/percent into
one row with normalized field names so metric-value elements can bind.
No node-side telemetry change: dryRunLevel + highVolumeSafetyLevel already
reach Influx via getOutput() (specificClass.js:248,250) and outputUtils
iterates every key with no filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the "Scaling" stat panel: it queried field 'scaling' that machineGroup
never emits, so it always rendered "No data".
- The "Mode" and "Rel Dist Peak" panels were stripped by the #39 no-duplication
rule because child pumps emit fields of the same name ('mode', 'relDistFromPeak').
But those are the GROUP's own measurement, never a true duplicate of per-pump
series. Mark them emittedFields:[] (the existing "always keep" convention used by
the injected pump panels) so the group-level status/metric renders.
Verified live: MGC dashboard now shows Mode "optimalControl", Abs/Rel Dist Peak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three display defects surfaced when rendering the live PS/pump/MGC dashboards:
1. Doubled values everywhere. EVOLV telemetry historically carried stray tags
(tagcode="undefined"/uuid="null") that newer writes dropped, so InfluxDB holds
two series per field and last() returned two of everything (e.g. Time Left,
Runtime, Heights). Add |> group(columns:["_field"]) before last()/aggregateWindow
in every template query so each field collapses to one value/line regardless of
tag-set history.
2. fields:"/.*/" also rendered the _time column as a stat value. For single-field
string panels (Direction, Flow Source, Mode, State, Prediction Quality) append
|> keep(columns:["_value"]); for mixed string+numeric panels (valve/vgc/monster)
drop _time/_start/_stop instead.
3. Level/Heights showed "0.12 min" — Grafana unit id "m" means minutes, not meters.
Change to lengthm; normalize m³->m3, m³/h->m3/h on pumpingStation.
Verified live via headless screenshots: PS, pump, and measurement dashboards now
show single clean values with correct units.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grafana Stat/Gauge panels default the Fields option to "Numeric Fields"
(reduceOptions.fields == ""), so string-valued fields (mode, state,
movementState, direction, flowSource, predictionQuality, running) are excluded
and the panel renders "No data" even though the Flux query (last()) returns the
string correctly.
Set reduceOptions.fields = "/.*/" ("All fields") on every stat panel bound to a
string field across machine, machineGroup, pumpingStation, valve,
valveGroupControl, and monster templates. lastNotNull calc was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate dashboards for an entire parent-child subtree from a single root
registration (pre-order, cycle/diamond-safe), so wiring only the subtree root
(e.g. pumpingStation) to dashboardAPI yields dashboards for every descendant.
Fix two contract drifts that left generated panels blank against live telemetry:
- _measurement var now mirrors outputUtils.formatMsg (general.name ||
<softwareType>_<id>); previously it always used the fallback form, so any
named node's dashboard queried a non-existent series.
- pumpingStation template field keys realigned to emitted telemetry
(flow.*.{upstream,out,overflow}, netFlowRate.measured, inflowLevel/
outflowLevel/overflowLevel, maxVolAtOverflow/minVolAt{Inflow,Outflow}).
Adds template alias resolution (softwareType -> shared template file) and
locks parity with slice44/45/46 tests + output manifest. 67/67 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- config/machineGroup.json: every non-row panel now annotated with
meta.emittedFields (mode, scaling, abs/relDistFromPeak, flow.total/group,
power.total/group). Per-pump fields (ctrl, state, runtime, pressure,
temperature) deliberately absent — those live on rotatingMachine children
per #39's no-data-duplication contract.
- Timeseries panels gain byRegexp dashed-bounds overrides for .min$/.max$
(same pattern as #38).
- test/basic/slice40-mgc-template.basic.test.js: 4 cases — no per-pump
fields leak in, every non-row annotated, dashed overrides present on TS,
composer dedup applies when a child claims an MGC-level field.
Closes#40
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
Applies the byRegexp(\\.min$ | \\.max$) → custom.lineStyle dashed pattern to
all 4 timeseries panels in config/machine.json — pattern confirmed via S2
spike (#33). Forward-compatible: nodes that don't yet emit .min/.max fields
see no change in rendering (regex won't match).
- config/machine.json: 4 timeseries panels gain byRegexp overrides for both
.min$ and .max$, dashed [10,10], orange (min) / red (max).
- test/basic/slice38-dashed-bounds.basic.test.js: 2 cases (presence per ts
panel, anchor-to-end forward compatibility).
Companion-field emission helper (generalFunctions.outputUtils — produces
<field>, <field>.min, <field>.max from a bounds-aware source) is a
generalFunctions submodule change and lands in a follow-up PR — out of
scope for this dashboardAPI-only slice.
Closes#38
Adds per-panel `meta.emittedFields` to machine.json (rotatingMachine) and
machineGroup.json (MGC) templates. Each non-row panel declares the Influx
field paths it visualizes, so a parent template's composer can filter out
panels already covered by its children (#39 no-data-duplication rule).
- config/machine.json: 13 non-row panels annotated.
- config/machineGroup.json: panels annotated.
- src/specificClass.js: collectEmittedFields(dashboard) helper.
- test/basic/slice37-emitted-fields.basic.test.js: 4 cases (template loads
with annotations, aggregation, missing-meta graceful, null input).
PRD F-6 panel set audit: machine.json already covers all the PRD-required
panels (State/Mode/Ctrl%/Runtime/NCog%/Flow/Efficiency/Pressure/Temperature/
Diagnostics) — substantially more than asked. No new panels added.
PRD F-7 predicted-vs-measured side-by-side: deferred. Current architecture
is "1 dashboard per node" (each child gets its own dashboard, cross-linked
from the parent), not "1 dashboard with N composed panels." Side-by-side
rendering of predicted (rotatingMachine dashboard) + measured (measurement
child dashboard) lives naturally as drill-down navigation today. Refactor
to a single-dashboard composition model would be substantial — flagged in
the issue comment for v2 if the drill-down UX proves insufficient.
Closes#37