diff --git a/package.json b/package.json
index c1a1952..6ae1f9d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/wiki/Home.md b/wiki/Home.md
new file mode 100644
index 0000000..6d88ba5
--- /dev/null
+++ b/wiki/Home.md
@@ -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
Process Cell]:::pc
+ monster[monster
Unit]:::unit
+ flow_up[measurement
type=flow
position=upstream]:::ctrl
+ flow_at[measurement
type=flow
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()
static DomainClass = Monster
static commands"]
+ end
+ subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
+ sc["Monster.configure()
wires concerns, ChildRouter rules
tick() → flowCalc → samplingProgram"]
+ end
+ subgraph concerns["src/ concern modules"]
+ parameters["parameters/
bounds + targets + rain index"]
+ flow["flow/
FlowTracker (measured + manual)"]
+ rain["rain/
RainAggregator (sum + avg)"]
+ schedule["schedule/
AQUON next-date parser"]
+ sampling["sampling/
samplingProgram + flowCalc"]
+ io["io/
output + statusBadge"]
+ commands["commands/
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`.
+
+
+
+| 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. |
+
+
+
+## 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
asset.type=flow
position=upstream"]:::ctrl
+ m_at["measurement
asset.type=flow
position=atequipment"]:::ctrl
+ m_dn["measurement
asset.type=flow
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.
+ 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.
+
+
+
+| 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` |
+
+
+
+**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` |