P9.3: wiki/Home.md following 14-section visual-first template + wiki:* scripts

Auto-generated topic-contract + data-model sections via shared wikiGen
script. Hand-written Mermaid diagrams for position-in-platform, code
map, child registration, lifecycle, configuration, state chart (where
applicable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 15:17:42 +02:00
parent 2a6a0bc34b
commit 2a82b7d7dc
2 changed files with 315 additions and 1 deletions

View File

@@ -4,7 +4,10 @@
"description": "Control module Monsternamekast",
"main": "monster.js",
"scripts": {
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js",
"wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md",
"wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md",
"wiki:all": "npm run wiki:contract && npm run wiki:datamodel"
},
"repository": {
"type": "git",

311
wiki/Home.md Normal file
View File

@@ -0,0 +1,311 @@
# monster
> **Reflects code as of `2a6a0bc` · regenerated `2026-05-11` via `npm run wiki:all`**
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
## 1. What this node is
**monster** is an S88 Unit that runs an AQUON-scheduled flow-proportional sampling program. It aggregates measured + manual flow, blends a rain-scaled prediction, and emits a `pulse` whenever the integrated volume crosses `m³ per pulse`. Drives the physical "monsternamekast" (composite sampling cabinet) on a wastewater treatment plant.
## 2. Position in the platform
```mermaid
flowchart LR
parent[plant parent<br/>Process Cell]:::pc
monster[monster<br/>Unit]:::unit
flow_up[measurement<br/>type=flow<br/>position=upstream]:::ctrl
flow_at[measurement<br/>type=flow<br/>position=atequipment]:::ctrl
op[(operator / AQUON)]
weather[(Open-Meteo)]
flow_up -->|flow.measured.upstream| monster
flow_at -->|flow.measured.atequipment| monster
op -->|set.schedule / data.flow / cmd.start| monster
weather -->|set.rain| monster
monster -->|child.register| parent
monster -.evt.output.-> parent
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
classDef ctrl fill:#a9daee,color:#000
```
S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| Flow-proportional sampling pulse | ✅ | Triggered when integrated `temp_pulse` ≥ 1. |
| Time-bounded sampling run | ✅ | Run length governed by `constraints.samplingtime` (hours). |
| AQUON schedule auto-start | ✅ | `set.schedule` parses AQUON rows; `nextDate` arms the next run. |
| Rain-scaled flow prediction | ✅ | `set.rain` aggregates Open-Meteo hourly precipitation into `sumRain` / `avgRain`. |
| Measured + manual flow blend | ✅ | `flowTracker.getEffectiveFlow()` picks measured if recent, else manual. |
| Minimum sample-interval cooldown | ✅ | Skips pulses inside `minSampleIntervalSec` window. |
| Bucket / weight tracking | ✅ | `bucketVol`, `bucketWeight` track the composite container. |
| Sub-sample volume override | ❌ | `subSampleVolume` fixed at 50 mL per schema. |
| Stateful FSM | ❌ | Monster is run/idle only — no formal state machine. |
## 4. Code map
```mermaid
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass = Monster<br/>static commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Monster.configure()<br/>wires concerns, ChildRouter rules<br/>tick() → flowCalc → samplingProgram"]
end
subgraph concerns["src/ concern modules"]
parameters["parameters/<br/>bounds + targets + rain index"]
flow["flow/<br/>FlowTracker (measured + manual)"]
rain["rain/<br/>RainAggregator (sum + avg)"]
schedule["schedule/<br/>AQUON next-date parser"]
sampling["sampling/<br/>samplingProgram + flowCalc"]
io["io/<br/>output + statusBadge"]
commands["commands/<br/>topic registry + handlers"]
end
nc --> sc
sc --> parameters
sc --> flow
sc --> rain
sc --> schedule
sc --> sampling
sc --> io
nc --> commands
```
| Module | Owns | Read first if you're changing… |
|---|---|---|
| `parameters/` | Bounds (`minPuls`, `absMaxPuls`, `targetPuls`), rain-index lookup, predicted-flow rate. | Sampling bounds, rain scaling math. |
| `flow/` | `FlowTracker` — measured-child latch + manual flow + effective-flow pick. | Flow source priority, dead-band logic. |
| `rain/` | `RainAggregator` — fold hourly Open-Meteo rows into `sumRain` / `avgRain`. | Rain inputs, forecast horizon. |
| `schedule/` | AQUON schedule parsing, `nextDate` + `daysPerYear` derivation. | Schedule arming, sample-name filtering. |
| `sampling/` | `_beginRun` / `_endRun` / `_maybeEmitPulse` — pulse integrator + cooldown guard. | Pulse timing, cooldown behaviour. |
| `io/` | `buildOutput`, `buildStatusBadge`. | Port-0 payload shape, status badge text. |
| `commands/` | Topic registry + payload validation + unit conversion. | New input topics, alias deprecation. |
## 5. Topic contract
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
<!-- BEGIN AUTOGEN: topic-contract -->
| Canonical topic | Aliases | Payload | Effect |
|---|---|---|---|
| `cmd.start` | `i_start` | `any` | Triggers an action / sequence — not idempotent. |
| `set.schedule` | `monsternametijden` | `any` | Replaces the named state value with the supplied payload. |
| `set.rain` | `rain_data` | `any` | Replaces the named state value with the supplied payload. |
| `data.flow` | `input_q` | `object` | Pushes a value into the node's measurement stream. |
| `set.mode` | `setMode` | `any` | Replaces the named state value with the supplied payload. |
| `set.model-prediction` | `model_prediction` | `any` | Replaces the named state value with the supplied payload. |
<!-- END AUTOGEN: topic-contract -->
## 6. Child registration
Mirrors the `ChildRouter` declaration in `specificClass.js → configure()`. Monster only accepts `measurement` children whose `asset.type` is `flow` (or unset).
```mermaid
flowchart LR
subgraph kids["accepted children (softwareType)"]
m_up["measurement<br/>asset.type=flow<br/>position=upstream"]:::ctrl
m_at["measurement<br/>asset.type=flow<br/>position=atequipment"]:::ctrl
m_dn["measurement<br/>asset.type=flow<br/>position=downstream"]:::ctrl
end
m_up -->|flow.measured.upstream| ft[FlowTracker.handleMeasuredFlow]
m_at -->|flow.measured.atequipment| ft
m_dn -->|flow.measured.downstream| ft
ft --> eff[getEffectiveFlow]
classDef ctrl fill:#a9daee,color:#000
```
| softwareType | filter | wired to | side-effect |
|---|---|---|---|
| `measurement` | `asset.type=flow` (or missing) | `flowTracker.handleMeasuredFlow` | Latches latest measured flow for `getEffectiveFlow`. |
| `measurement` | `asset.type` anything else | _ignored_ | Logged once at register. |
## 7. Lifecycle — the sampling-program loop
```mermaid
sequenceDiagram
participant child as measurement child
participant ops as operator / AQUON
participant monster as monster
participant ft as flowTracker
participant sp as samplingProgram
participant out as Port-0 output
child->>monster: flow.measured.<position>
ops->>monster: set.schedule / cmd.start / data.flow
Note over monster: every 1000 ms tick
monster->>ft: getEffectiveFlow()
monster->>monster: flowCalc → m3PerTick
monster->>sp: samplingProgram
alt cmd.start OR Date.now() ≥ nextDate
sp->>sp: validateFlowBounds → _beginRun
end
alt running AND stop_time > now
sp->>sp: integrate temp_pulse += m3PerTick / m3PerPuls
sp->>sp: _maybeEmitPulse (cooldown-guarded)
else stop_time elapsed
sp->>sp: _endRun
end
monster->>out: msg{topic, payload (delta-compressed)}
```
One pulse per integrated `m³ per pulse`. The cooldown guard suppresses pulses inside `minSampleIntervalSec` and increments `missedSamples`.
## 8. Data model — `getOutput()`
What lands on Port 0. Built in `buildOutput()` from `m.measurements.getFlattenedOutput()` plus the sampling-run snapshot.
<!-- BEGIN AUTOGEN: data-model -->
| Key | Type | Unit | Sample |
|---|---|---|---|
| `avgRain` | number | — | `0` |
| `bucketVol` | number | — | `0` |
| `bucketWeight` | number | — | `0` |
| `daysPerYear` | number | — | `0` |
| `flowMax` | undefined | — | `null` |
| `flowToNextPulseM3` | number | — | `0` |
| `invalidFlowBounds` | boolean | — | `false` |
| `m3PerPuls` | number | — | `0` |
| `m3PerPulse` | number | — | `0` |
| `m3Total` | number | — | `0` |
| `maxVolume` | number | — | `20` |
| `minSampleIntervalSec` | number | — | `60` |
| `minVolume` | number | — | `5` |
| `missedSamples` | number | — | `0` |
| `nextDate` | undefined | — | `null` |
| `nominalFlowMin` | undefined | — | `null` |
| `predFlow` | number | — | `0` |
| `predM3PerSec` | number | — | `0` |
| `predictedRateM3h` | number | — | `0` |
| `pulse` | boolean | — | `false` |
| `pulseFraction` | number | — | `0` |
| `pulsesRemaining` | number | — | `200` |
| `q` | number | — | `0` |
| `running` | boolean | — | `false` |
| `sampleCooldownMs` | number | — | `0` |
| `sumPuls` | number | — | `0` |
| `sumRain` | number | — | `0` |
| `targetDeltaL` | number | — | `-10` |
| `targetDeltaM3` | number | — | `-0.01` |
| `targetProgressPct` | number | — | `0` |
| `targetVolumeM3` | number | — | `0.01` |
| `timeLeft` | number | — | `0` |
| `timePassed` | number | — | `0` |
| `timeToNextPulseSec` | number | — | `0` |
<!-- END AUTOGEN: data-model -->
**Concrete sample** (mid-run snapshot):
```json
{
"pulse": false,
"running": true,
"bucketVol": 1.25,
"sumPuls": 25,
"predFlow": 240.0,
"m3PerPuls": 4,
"q": 215.4,
"timeLeft": 12340,
"targetVolumeM3": 0.005,
"targetProgressPct": 41.6,
"sumRain": 3.2,
"avgRain": 0.13,
"nextDate": 1746940800000
}
```
Concrete samples must come from a known-good test run. Regenerate when concern modules change shape.
## 9. Configuration — editor form ↔ config keys
```mermaid
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Sampling time hr]
f2[Sampling period hr]
f3[Min volume L]
f4[Max weight kg]
f5[Empty bucket weight kg]
f6[Flowmeter on/off]
f7[Min sample interval s]
end
subgraph config["Domain config slice"]
c1[constraints.samplingtime]
c2[constraints.samplingperiod]
c3[constraints.minVolume]
c4[constraints.maxWeight]
c5[asset.emptyWeightBucket]
c6[constraints.flowmeter]
c7[constraints.minSampleIntervalSec]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
f7 --> c7
```
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Sampling time (hr) | `constraints.samplingtime` | `0` | ≥ 0 | run length, `predFlow` |
| Sampling period (hr) | `constraints.samplingperiod` | `24` | ≥ 1 | schedule arming |
| Min volume (L) | `constraints.minVolume` | `5` | ≥ 5 | bounds + invalid-flow guard |
| Max weight (kg) | `constraints.maxWeight` | `23` | ≤ 23 | bucket overload |
| Empty bucket weight (kg) | `asset.emptyWeightBucket` | `3` | ≥ 0 | `bucketWeight` |
| Sub-sample volume (mL) | `constraints.subSampleVolume` | `50` | fixed | per-pulse volume |
| Flowmeter present | `constraints.flowmeter` | `true` | bool | proportional vs time mode |
| Min sample interval (s) | `constraints.minSampleIntervalSec` | `60` | ≥ 0 | cooldown guard |
| Intake speed (m/s) | `constraints.intakeSpeed` | `0.3` | ≥ 0 | informational |
| Intake diameter (mm) | `constraints.intakeDiameter` | `12` | ≥ 0 | informational |
## 10. State chart
Skipped — monster has no formal state machine. The `running` boolean toggles when `_beginRun` / `_endRun` fire. See section 7 for the sampling-program sequence, which is the closest analogue.
## 11. Examples
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | `examples/basic.flow.json` | Inject + manual flow + dashboard, no parent | ✅ in repo |
| Integration | `examples/integration.flow.json` | monster + measurement child + AQUON schedule | ✅ in repo |
| Dashboard | `examples/monster-dashboard.flow.json` | Live FlowFuse charts (pulse, bucket, predFlow) | ✅ in repo |
| Edge | `examples/edge.flow.json` | Cooldown guard + invalid-flow bounds | ✅ in repo |
| API | `examples/monster-api-dashboard.flow.json` | dashboardAPI consumer wired in | ✅ in repo |
One screenshot per tier where helpful. PNG ≤ 200 KB under `wiki/_partial-screenshots/monster/`.
## 12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| `pulse` never fires | Is `running` true? Check `validateFlowBounds` log — invalid bounds short-circuits the run. | `parameters/parameters.js` |
| Pulses arrive too fast / skipped | Cooldown guard active. Inspect `missedSamples` + `minSampleIntervalSec`. | `sampling/samplingProgram.js → _maybeEmitPulse` |
| `q` always zero | Measured-flow child not registered, manual flow not pushed. Watch Port 2 + `data.flow` history. | `flow/flowTracker.js` |
| `nextDate` not arming | `set.schedule` payload didn't include matching `aquonSampleName` row. | `schedule/schedule.js → regNextDate` |
| `sumRain` zero with rain input | `rainAggregator.update` ran while `running=true` (skipped). | `specificClass.js → updateRainData` |
| Bucket overfilled before `stop_time` | `m3PerPuls` rounded down; check `predFlow` vs effective `q`. | `sampling/samplingProgram.js → _beginRun` |
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging.
## 13. When you would NOT use this node
- Use monster for **AQUON-scheduled composite sampling** on a wastewater plant. For a single grab sample on demand, fire `cmd.start` once and tear it down — monster is overkill for ad-hoc work.
- Don't use monster as a generic flow totaliser. Use a `measurement` child with the right type/variant for raw flow integration.
- Skip monster if you don't need pulse-proportional dosing — the time-mode (`flowmeter=false`) is currently informational only.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | Edge test `sampling-guards.edge.test.js` cooldown-guard case is a pre-existing failure — the cooldown skip increments `missedSamples` but the assertion expects a different timing. | `test/edge/sampling-guards.edge.test.js` |
| 2 | `set.mode` and `set.model-prediction` are reserved — handlers delegate to optional methods that don't exist yet. | `commands/handlers.js` |
| 3 | Time-only mode (`flowmeter=false`) is not exercised — the sampling program assumes a flow source. | `sampling/samplingProgram.js` |
| 4 | Sub-sample volume hard-coded at 50 mL (schema enforces `min=max=50`). | `generalFunctions/src/configs/monster.json` |