olt: add bulk DHCPv4/DHCPv6 filter edit alongside flood mode

Generalises the OLT bulk tool from flood-mode-only to arbitrary NNI-service
edits. DHCP modes live nested under each NNI entry's Filter object
(Filter.DHCPv4 / Filter.DHCPv6, e.g. "pass" <-> "umt"), confirmed from a
real OLT-CFG paste-back.

- src/mcms-api.js: planFloodChange -> planNniEdit(oltCfg, {tagPattern, flood,
  dhcpv4, dhcpv6}); each concern optional. DHCP is value-replacement,
  replace-only-when-present (never invents the key; leaves EAPOL/PPPoE/NDP).
  Still non-mutating (clones the entry AND the Filter). Returns changes with
  per-entry edits[]. summarizeOltNniNetworks surfaces dhcpv4/dhcpv6; new
  dhcpModeOfNni helper.
- main.js + web/server.js: executeFloodChange handler passes flood/dhcpv4/
  dhcpv6 ops through (channel name kept for wire compat).
- renderer: OLT inspector now has three action dropdowns (Flooding / DHCPv4 /
  DHCPv6, defaults flood private->auto + DHCP pass->umt); table shows DHCP
  chips + per-service "will change (flood, DHCPv4, …)"; flood-mode filter
  defaults to Any so DHCP-on-pass auto-flood OLTs still show; CSV report
  lists every per-NNI edit (pon-olt-nni-*.csv).

Verified: node --check; planNniEdit unit-tested against the real fixture +
a pass variant (13/13: no-mutation, EAPOL/PPPoE/NDP preserved,
replace-when-present, flood/DHCP independence); OLT view rendered offscreen
showing correct per-service "will change" markers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-23 14:57:37 +02:00
parent de037c8795
commit 5e3d9271a8
7 changed files with 232 additions and 118 deletions

View file

@ -18,7 +18,8 @@ fleet. Top-level **tabs** (Dashboard / ONU / OLT) switch between:
- **Dashboard** — read-only network telemetry (see §15).
- **ONU** — the fleet table + firmware-upgrade campaign + verify flows
(the original purpose: bulk ONU firmware upgrades).
- **OLT** — bulk OLT PON flooding-mode changes (private↔auto, see §14).
- **OLT** — bulk OLT NNI-service edits: PON flooding mode (private↔auto) and
DHCPv4/v6 filter (pass↔umt), see §14.
Reference deployment: GNXS / Tibit MicroPlug ONUs running Interos Everest
(EV051xxR / EV052xxR firmware family) behind Tibit OLTs.
@ -655,48 +656,54 @@ Key procedure citations:
---
## 14. OLT PON flooding mode (bulk private↔auto)
## 14. OLT NNI-service edits (bulk flood mode + DHCP filter)
Second bulk feature, added after the firmware upgrader. Lets an operator
flip the PON flooding mode of OLT NNI services across many OLTs at once.
edit OLT NNI services across many OLTs at once. Two concerns, applied
together in one GET→edit→PUT per OLT:
### The mutation rule (the whole feature in one fact)
### The two mutation rules
On Tibit OLTs, a service's flooding mode is encoded by the **presence or
absence of one key** on its `OLT-CFG["NNI Networks"][i]` entry:
Both live on each `OLT-CFG["NNI Networks"][i]` entry, both confirmed
empirically from operator paste-backs of `s0.c76.c0`:
- **`PON FLOOD ID` key present** (any value, including `0`) → **private**
- **`PON FLOOD ID` key absent** → **auto**
1. **Flooding mode** — the **presence/absence of the `PON FLOOD ID` key**:
present (any value, incl. `0`) → **private**; absent → **auto**. So
private→auto is `delete entry['PON FLOOD ID']`; auto→private is
`entry['PON FLOOD ID'] = 0`.
2. **DHCP filter mode** — a **value** under the nested `Filter` object:
`entry.Filter.DHCPv4` / `entry.Filter.DHCPv6`, e.g. `"pass"``"umt"`
(siblings `EAPOL`/`PPPoE`/`NDP` are left alone). Value replacement, not
key presence. **Replace-only-when-present**: an entry with no `Filter`
(or no such key) is skipped, never invented.
So private→auto is `delete entry['PON FLOOD ID']`; auto→private is
`entry['PON FLOOD ID'] = 0`. Everything else on the entry (`TAG MATCH`,
`Learning Limit`, …) and the rest of the OLT-CFG is preserved. This was
confirmed empirically by diffing two paste-backs of the same OLT
(`24:65:e1:cc:af:1e`, NNI `s0.c76.c0`) in private vs auto — the *only*
difference was that key.
The default action ships as: TAG MATCH = `s0.c76.c0`, mode `private`
`auto`. Both directions are wired so a change can be rolled back from the
same UI.
Everything else on the entry and the rest of the OLT-CFG is preserved.
Default actions: TAG MATCH `s0.c76.c0`, flood `private→auto`, DHCPv4/v6
`pass→umt`. All directions are wired, so changes roll back from the same UI.
### Data flow
```
listOlts ─► McmsClient.listAllOltConfigs ─► summarizeOltNniNetworks (pure)
executeFloodChange ─► per-OLT: getOltConfig ─► planFloodChange (pure, no-mutate)
─► putOltConfig (full-doc PUT)
executeFloodChange ─► per-OLT: getOltConfig ─► planNniEdit (pure, no-mutate)
(channel name kept for wire compat) ─► putOltConfig (full-doc PUT)
```
- **Pure helpers** live at the bottom of `src/mcms-api.js`:
`floodModeOfNni`, `summarizeOltNniNetworks`, `tagPatternToRegex`,
`planFloodChange`. `planFloodChange` clones the `NNI Networks` array and
never mutates its input — it returns `{ newDoc, changes, unchanged }`.
- **Pure helpers** at the bottom of `src/mcms-api.js`: `floodModeOfNni`,
`dhcpModeOfNni`, `summarizeOltNniNetworks`, `tagPatternToRegex`,
`planNniEdit`. `planNniEdit(oltCfg, { tagPattern, flood, dhcpv4, dhcpv6 })`
clones the `NNI Networks` array (and any edited `Filter`) and never mutates
its input. Returns `{ newDoc, changes, unchanged }`, where a change is
`{ index, tagMatch, edits: [{ type, from, to }] }``type` is `'flood'`,
`'DHCPv4'`, or `'DHCPv6'`, so one entry can carry several edits.
- Each concern is optional (`flood`/`dhcpv4`/`dhcpv6` may be `null`); the UI
has three independent action dropdowns.
- **TAG MATCH pattern** supports exact strings, `*`/`""` (match all), and
`*` globs (e.g. `s0.c76.*`). `tagPatternToRegex` escapes regex metachars
then turns `*` into `.*`.
- **`fromMode` guard**: only entries currently in `fromMode` are flipped;
entries already in the target mode are recorded in `unchanged` (the
per-OLT `noop` case), so re-running is idempotent.
- **`from` guards**: only entries currently on the `from` mode/value are
flipped; entries already at target are left in `unchanged` (the per-OLT
`noop` case), so re-running is idempotent.
- **Concurrency 3** on the write path (vs 5 for ONU delete, 8 for state
fetch) — OLT writes carry far more blast radius.
- **PUT-as-replace**, same gotcha as ONU-CFG (§5.7): the handler GETs the