Adds a second bulk workflow alongside the firmware upgrader: changing PON flooding mode (private<->auto) on OLT NNI services across many OLTs. Flooding mode is encoded by the presence of the "PON FLOOD ID" key on each OLT-CFG["NNI Networks"] entry (present = private, absent = auto). private->auto deletes the key; auto->private sets it to 0. The rest of the OLT-CFG is preserved (PUT-as-replace, same as ONU-CFG). - src/mcms-api.js: listAllOltConfigs / getOltConfig / putOltConfig, plus pure helpers floodModeOfNni, summarizeOltNniNetworks, planFloodChange (planFloodChange clones and never mutates its input) - main.js: api:listOlts + api:executeFloodChange IPC handlers (concurrency 3, GET->plan->PUT, streams olt-flood:progress) - preload.js: expose both channels + onFloodProgress - renderer: OLT inspector view (#view-olts), two-step APPLY confirm, auto-saved pon-olt-flood-*.csv result report - Restore README.md (lost to a filesystem error) and document the new feature in README.md and CLAUDE.md (new section 14) Verified: npm run check passes; pure helpers unit-tested for the no-mutation, idempotency, and rollback invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
724 lines
29 KiB
Markdown
724 lines
29 KiB
Markdown
# CLAUDE.md
|
||
|
||
Reference document for future Claude sessions (and humans reading the
|
||
repo cold). Captures what's actually built, the MCMS API quirks
|
||
discovered along the way, and the things that don't work or were
|
||
deliberately skipped.
|
||
|
||
For run instructions and user-facing scope, see `README.md` — this file
|
||
is the engineering counterpart.
|
||
|
||
---
|
||
|
||
## 1. What this app does
|
||
|
||
Electron desktop client for two bulk operations against **Ciena
|
||
MCMS 6.2** PON Manager:
|
||
|
||
1. **Bulk ONU firmware upgrades** (the original purpose).
|
||
2. **Bulk OLT PON flooding-mode changes** (private↔auto on NNI services —
|
||
see §14).
|
||
|
||
Reference deployment: GNXS / Tibit MicroPlug ONUs running Interos Everest
|
||
(EV051xxR / EV052xxR firmware family) behind Tibit OLTs.
|
||
|
||
End-to-end operator workflow:
|
||
|
||
1. Connect with email + password against the MCMS web URL.
|
||
2. Browse the ONU fleet with rich filters (status, down-duration,
|
||
version, equipment ID, PON mode).
|
||
3. Either select ONUs and continue to the upgrade campaign view, **or**
|
||
delete stale ONUs (Deregistered / Dying Gasp / etc.) from MCMS.
|
||
4. In the campaign view: pick a firmware `.bin` already uploaded to
|
||
`/files/onu-firmware/` (or upload one), preview the planned writes,
|
||
and commit. The Preview step also runs a FEC pre-flight health
|
||
check on every selected ONU.
|
||
5. On Execute, the plan is auto-saved as a CSV to `~/Downloads/`.
|
||
6. After the upgrade has had time to roll, open the saved CSV in the
|
||
verify view to compare before/after and auto-save a verification
|
||
CSV alongside.
|
||
|
||
---
|
||
|
||
## 2. Quickstart
|
||
|
||
```bash
|
||
cd pon-fleet-upgrader
|
||
npm install
|
||
npm start # launch Electron
|
||
npm run check # node --check on every JS file (cheap smoke test)
|
||
```
|
||
|
||
Environment:
|
||
|
||
- `PON_FLEET_VERBOSE=0` — silence the per-request log lines in the
|
||
Electron terminal. Default is verbose because the log dumps headers
|
||
and full response bodies on non-2xx, which has been invaluable for
|
||
debugging.
|
||
|
||
DevTools: **Ctrl+Shift+I** (Cmd+Opt+I on macOS). Reload renderer after
|
||
editing `renderer/`: **Ctrl+R**.
|
||
|
||
---
|
||
|
||
## 3. Architecture
|
||
|
||
Standard Electron three-process model with strict isolation:
|
||
|
||
```
|
||
+-----------+ IPC invoke +-----------+ HTTPS +-----------+
|
||
| Renderer | ──────────────► | Main | ────────► | MCMS |
|
||
| (browser) | via preload | (Node) | | (web UI) |
|
||
+-----------+ ◄───────────── +-----------+ ◄──────── +-----------+
|
||
streaming events / responses
|
||
```
|
||
|
||
- **Renderer** (`renderer/app.js`, `index.html`, `app.css`) — vanilla
|
||
DOM, no framework. Calls into `window.api.*` exposed by the preload.
|
||
Has no Node access and never sees raw cookies.
|
||
- **Preload** (`preload.js`) — `contextBridge` whitelist; the only
|
||
bridge from renderer to main. Adding a new IPC channel requires
|
||
touching this file.
|
||
- **Main** (`main.js`) — owns the `McmsClient`, the cookie jar, file
|
||
IO, and the Electron `dialog` / `app.getPath()` APIs.
|
||
- **MCMS client** (`src/mcms-api.js`) — raw `https` + `tough-cookie`.
|
||
No third-party HTTP client; the only npm dep is `tough-cookie`. See
|
||
§5 for the quirks this client works around.
|
||
- **Bank strategy** (`src/bank-strategy.js`) — pure logic for picking
|
||
the inactive bank to write to. See §7.
|
||
|
||
### File layout
|
||
|
||
```
|
||
pon-fleet-upgrader/
|
||
├── package.json # electron, tough-cookie, npm scripts
|
||
├── main.js # Electron main process + IPC handlers
|
||
├── preload.js # contextBridge whitelist
|
||
├── README.md # user-facing run docs
|
||
├── CLAUDE.md # this file
|
||
├── src/
|
||
│ ├── mcms-api.js # HTTPS client, FEC extractor, time helpers
|
||
│ └── bank-strategy.js # inactive-bank + plan computation
|
||
└── renderer/
|
||
├── index.html # login / fleet / campaign / verify views
|
||
├── app.css # dark theme, status pill classes
|
||
└── app.js # UI state, all renderer logic
|
||
```
|
||
|
||
### IPC channel list
|
||
|
||
All channels are `invoke`-style (request/response). A few additionally
|
||
stream progress events back to the renderer.
|
||
|
||
| Channel | Purpose | Streams |
|
||
|---|---|---|
|
||
| `api:login` / `api:logout` | session auth | — |
|
||
| `api:listFleet` | ONU-CFG + ONU-STATE + OLT-STATE merged | — |
|
||
| `api:fetchStates` | per-ONU state fallback if bulk failed | `states:progress` |
|
||
| `api:listOnuConfigs` / `api:getOnuConfig` | direct CFG access | — |
|
||
| `api:listOnuFirmware` / `api:uploadFirmware` | firmware inventory | — |
|
||
| `api:planUpgrade` | dry-run plan for selected ONUs | — |
|
||
| `api:executePerOnu` | Procedure 7 — per-ONU PUTs | `upgrade:progress` |
|
||
| `api:executeBulkTask` | Procedure 8 — single AUTO-TASK-CFG | — |
|
||
| `api:getTaskConfig` / `api:getUpgradeStatus` | poll helpers | — |
|
||
| `api:deleteOnus` | bulk delete (CFG + best-effort STATE) | `delete:progress` |
|
||
| `api:listOlts` | OLT-CFG list + per-OLT NNI flood-mode summary | — |
|
||
| `api:executeFloodChange` | bulk PON flooding-mode flip (GET→plan→PUT) | `olt-flood:progress` |
|
||
| `api:fetchFecHealth` | pre-flight FEC pre/post + optical | `fec:progress` |
|
||
| `api:savePlanCsv` | auto-save plan on Execute | — |
|
||
| `api:saveCsvFile` | generic CSV writer | — |
|
||
| `api:openCsvFile` | open dialog → return text | — |
|
||
| `api:fetchOnuSnapshot` | verify-flow: state + cfg per ONU | `verify:progress` |
|
||
|
||
---
|
||
|
||
## 4. Features actually implemented
|
||
|
||
### Fleet view
|
||
|
||
- Login with email / password, accept-self-signed-TLS toggle (essential
|
||
for lab MCMS installs).
|
||
- Bulk fleet load: `listAllOnuConfigs` + `listAllOnuStates` +
|
||
`listAllOltStates`, all paginated via the `next` cursor at page size
|
||
100 (see §5).
|
||
- Filters: text (name/address/serial), Equipment ID, PON mode,
|
||
active version, exclude active version, model/version family,
|
||
registration status (incl. "Down" aggregate and "Unknown" buckets),
|
||
down-for-at-least-N-days.
|
||
- Columns: serial, name/address, equipment ID, PON mode,
|
||
**status** (colour-coded from OLT-STATE bucket), **last seen**
|
||
(relative time from `ONU-STATE.Time`), active slot, active version,
|
||
inactive version.
|
||
- Bulk select / clear / select-all-matching.
|
||
- **Delete** — `DELETE /v1/onus/configs/<id>/` per ONU at concurrency 5,
|
||
then best-effort `DELETE /v1/onus/states/<id>/`. Two-step confirm
|
||
(alert + typed `DELETE`). Streams progress per ONU.
|
||
|
||
### Campaign view
|
||
|
||
- Firmware picker bound to `/files/onu-firmware/`, inferring version
|
||
from filename (regex `[A-Z]{2}\d{5}[A-Z]?`).
|
||
- Upload firmware via Electron `dialog.showOpenDialog`.
|
||
- Execution mode toggle: **bulk task** (Procedure 8 — recommended) or
|
||
**per-ONU PUT** (Procedure 7 — per-device feedback).
|
||
- Scheduled-start datetime-local picker (converts local → UTC string
|
||
`YYYY-MM-DD HH:MM:SS`).
|
||
- **Dry-run preview** with FEC pre-flight: each selected ONU is
|
||
re-fetched (`getOnuState`) at preview time, and the resulting FEC
|
||
pill + optical levels + raw counters are rendered in a new column.
|
||
- **CSV auto-save on Execute** — writes
|
||
`~/Downloads/pon-upgrade-<version>-<mode>-<count>onus-<stamp>.csv`
|
||
the moment Execute is clicked, *before* any MCMS write. See §10.
|
||
- Soft FEC gate in the Execute confirmation dialog: warns (but doesn't
|
||
block) when any selected ONU is showing post-FEC errors or the
|
||
pre==post buggy signature.
|
||
|
||
### Verify view (after-upgrade comparison)
|
||
|
||
- **Open plan CSV…** — file dialog defaulted to `~/Downloads`. Parser
|
||
is lenient — needs only an `ONU ID` column, everything else is
|
||
optional.
|
||
- For each ONU listed, fetches **both** ONU-STATE (fresh FEC + optical)
|
||
and ONU-CFG (to confirm `FW Bank Ptr` flipped and the active version
|
||
now equals the recorded target). Concurrency 8.
|
||
- Renders a comparison table with target→active, "Applied?" badge,
|
||
before/after FEC pills + counters, before/after optical RX/TX, and
|
||
a one-line verdict per ONU.
|
||
- **Auto-saves** the comparison to
|
||
`~/Downloads/pon-verify-<trailing-of-source>-<stamp>.csv` the moment
|
||
the comparison finishes, so it sorts next to the original.
|
||
- Side-panel summary: counts of applied/not-applied, fixed/improved,
|
||
still-buggy, regressed, still-healthy.
|
||
|
||
### FEC health rules
|
||
|
||
(`extractOnuHealth` in `src/mcms-api.js`)
|
||
|
||
| Flag | Meaning | Trigger |
|
||
|---|---|---|
|
||
| `ok` | green | All four FEC counters present and 0 |
|
||
| `pre` | yellow | Any pre-FEC counter > 0, all post-FEC = 0 |
|
||
| `post` | red | Any post-FEC counter > 0 (uncorrected errors) |
|
||
| `buggy` | blue | ONU pre>0 AND post>0 AND `post/pre >= 0.95` |
|
||
| `nodata` | grey | No FEC counters found in STATS |
|
||
| `error` | grey | Per-ONU fetch threw |
|
||
|
||
**Buggy detection note.** Real-world example from a GNXS Everest ONU:
|
||
`Pre-FEC BER = 22,388,041,291`, `Post-FEC BER = 22,379,111,198`. Those
|
||
differ by 0.04%, which is well into "FEC isn't doing anything" territory.
|
||
Exact-equality detection would miss this; the 0.95 ratio threshold
|
||
catches it. The condition is intentionally one-sided — we treat ONU
|
||
side as the bug source (the OLT side counters are computed by the OLT
|
||
firmware which we trust).
|
||
|
||
### Optical thresholds
|
||
|
||
From the PON Manager User Guide green-LED criteria (323-1961-302 p149):
|
||
|
||
| Field | Green | Yellow | Red |
|
||
|---|---|---|---|
|
||
| ONU RX | ≥ −28 dBm | −28 to −30 dBm | < −30 dBm |
|
||
| ONU TX | ≥ 3 dBm | — | < 3 dBm |
|
||
|
||
Yellow is a margin band we added — the user guide is binary (≥ −30 vs
|
||
< −30), but operators want a warning before the link is actually broken.
|
||
|
||
---
|
||
|
||
## 5. MCMS API quirks
|
||
|
||
These are the gotchas worth knowing. Many were diagnosed empirically
|
||
against MCMS 6.2 + Interos Everest 5.11/5.12. **If something behaves
|
||
oddly, check this section first.**
|
||
|
||
### 5.1 User-Agent header is required
|
||
|
||
Node's `https.request` doesn't send a User-Agent by default. **Some MCMS
|
||
middleware dereferences `HTTP_USER_AGENT` without a guard and 500s when
|
||
it's absent.** Reproducible via `curl -A '' https://mcms/api/v3/...`.
|
||
|
||
→ `src/mcms-api.js` sends `User-Agent: pon-fleet-upgrader/0.1.0` on
|
||
every request. Do not remove.
|
||
|
||
### 5.2 CSRF cookie name is `__Host-csrftoken`
|
||
|
||
MCMS 6.2 uses RFC 6265bis `__Host-` cookie prefix (Secure + Path=/).
|
||
The cookie names are:
|
||
|
||
- `__Host-csrftoken`
|
||
- `__Host-sessionid`
|
||
- `__Host-sessionexpire`
|
||
|
||
Older MCMS builds shipped bare `csrftoken`. The client looks for both:
|
||
|
||
```js
|
||
const csrf = jarCookies.find(
|
||
(c) => c.key === 'csrftoken' || c.key === '__Host-csrftoken',
|
||
);
|
||
```
|
||
|
||
The token value still goes in the `X-CSRFToken` header (no prefix).
|
||
|
||
### 5.3 Pagination: use `next`, not `skip`
|
||
|
||
`/v3/onus/configs/?skip=N` has been observed returning HTTP 500 on this
|
||
MCMS build. Use the `next` cursor: pass the last document's `_id` as
|
||
the `next` query param to get the page that *excludes* all IDs up to
|
||
and including that one.
|
||
|
||
→ See `McmsClient._listPaginatedByNext`.
|
||
|
||
### 5.4 Bulk-list page size 100
|
||
|
||
`/v3/onus/configs/?limit=1000` times out at the Apache front-end on
|
||
~1500-ONU fleets, before Django even logs the request. Full ONU-CFG
|
||
documents are ~30 KB each, so a 1000-row page is ~30 MB and the proxy
|
||
gives up.
|
||
|
||
→ Default page size is 100. Each response is ~3 MB and lands in well
|
||
under the proxy timeout.
|
||
|
||
### 5.5 Path prefix is `/api/v1/...`
|
||
|
||
The dev guide writes "GET `/v1/users/authenticate/`" as shorthand. The
|
||
real HTTP path is `/api/v1/users/authenticate/`. The client normalises
|
||
this internally — pass either form.
|
||
|
||
### 5.6 Request body envelope
|
||
|
||
PUT/POST bodies are wrapped: `{ "data": { ...payload } }`. The login
|
||
endpoint accepts both wrapped and unwrapped on observed builds (the
|
||
client tries wrapped first, falls back on 400). Everything else is
|
||
strictly wrapped.
|
||
|
||
### 5.7 PUT is full-document replace, not patch
|
||
|
||
`PUT /v1/onus/configs/<id>/` replaces the entire `ONU-CFG`. The client
|
||
fetches the doc, mutates the FW fields in place, and PUTs the result.
|
||
**Do not use PATCH** — MCMS PATCH endpoints are not used by this app
|
||
and the dev guide is explicit about PUT-as-replace for CFG updates.
|
||
|
||
### 5.8 Timestamp format
|
||
|
||
MCMS emits and accepts: `"YYYY-MM-DD HH:MM:SS[.ffffff]"` in **UTC**
|
||
with a literal space separator (not ISO-8601 `T`).
|
||
|
||
Used on: `ONU-STATE.Time`, alarm timestamps,
|
||
`AUTO-TASK-CFG.Task.Scheduled Start Time`, query-param timestamps.
|
||
|
||
→ Helpers in `src/mcms-api.js`: `formatMcmsTime(date)` and
|
||
`parseMcmsTime(str)`. The renderer also coerces the space-form to ISO
|
||
before passing to `new Date()` to avoid Node's local-time
|
||
interpretation.
|
||
|
||
### 5.9 Server-side filtering on `query`/`projection` is brittle
|
||
|
||
Mongo-style server filters get rejected (or silently return nothing)
|
||
when the URL-encoded JSON has spaces in field names, certain escape
|
||
patterns, or projection paths that don't exist on every document
|
||
version. **We load the full fleet and filter entirely client-side.**
|
||
|
||
If you need server filtering, the safest pattern is no quotes around
|
||
field names and `%20` for spaces (not `+`). The client's
|
||
`_encodeQuery` uses `encodeURIComponent` for exactly this reason.
|
||
|
||
### 5.10 ONU registration status lives on OLT-STATE, not ONU-STATE
|
||
|
||
The 9 registration buckets (Registered, Deregistered, Dying Gasp,
|
||
Disabled, Disallowed Admin/Error/Reg ID, Unspecified, Unprovisioned)
|
||
appear under `OLT-STATE["ONU States"][bucket] = [onuId, ...]`. See
|
||
323-1961-306 Procedure 2.
|
||
|
||
→ `listAllOltStates` pulls these, and `api:listFleet` builds a
|
||
`onuId → { status, oltMac }` map from the buckets. On duplicates we
|
||
prefer a non-Registered bucket (the interesting signal).
|
||
|
||
### 5.11 FEC counters live on ONU-STATE, not `/onus/stats/`
|
||
|
||
`/v1/onus/stats/<id>/` is the time-series PM collection — `start-time`
|
||
is required, returns historical samples. **The current FEC/optical
|
||
values are already in ONU-STATE:**
|
||
|
||
```
|
||
state_collection.STATS["ONU-PON"]["RX Pre-FEC BER"]
|
||
state_collection.STATS["ONU-PON"]["RX Post-FEC BER"]
|
||
state_collection.STATS["ONU-PON"]["RX Optical Level"] # dBm
|
||
state_collection.STATS["ONU-PON"]["TX Optical Level"] # dBm
|
||
state_collection.STATS["OLT-PON"]["RX Pre-FEC BER"]
|
||
state_collection.STATS["OLT-PON"]["RX Post-FEC BER"]
|
||
```
|
||
|
||
→ The pre-flight fetch (`api:fetchFecHealth`) and the verify fetch
|
||
(`api:fetchOnuSnapshot`) both call `getOnuState(id)` and extract via
|
||
`extractOnuHealth`. The time-series stats endpoint is **not used**
|
||
anywhere — earlier code that hit it has been removed.
|
||
|
||
`extractOnuHealth` accepts both shapes: the bare state doc
|
||
(`{STATS: ...}`) and the UI-helper wrapped form
|
||
(`{state_collection: {STATS: ...}}`), since `/api/onu/summary` returns
|
||
the wrapped shape.
|
||
|
||
### 5.12 Login body shape
|
||
|
||
The PON Manager web app POSTs `{"data": {"email": ..., "password": ...}}`
|
||
to `/v1/users/authenticate/`. Some build variants accept the unwrapped
|
||
form too — the client tries wrapped first.
|
||
|
||
---
|
||
|
||
## 6. ONU-STATE schema cheat-sheet (observed, not specced)
|
||
|
||
The MCMS dev guide doesn't formally document the ONU-STATE shape on this
|
||
build. From observation on GNXS / Interos Everest ONUs:
|
||
|
||
```jsonc
|
||
{
|
||
"_id": "GNXS05057fee", // serial / device ID
|
||
"Time": "2026-05-04 21:51:57.394127", // last update (UTC)
|
||
"ONU": {
|
||
"Equipment ID": "FT-XGS2110",
|
||
"Vendor": "GNXS",
|
||
"PON Mode": "GPON",
|
||
"FW Bank Files": ["...bin", "...bin"],
|
||
"FW Bank Versions": ["EV05110R", "EV05100R"],
|
||
"FW Bank Ptr": 0, // active slot (0 | 1 | 65535)
|
||
"FW Version": "EV05110R",
|
||
"FW Upgrade Status": { /* progress, status, etc */ },
|
||
// ...
|
||
},
|
||
"STATS": {
|
||
"ONU-PON": {
|
||
"RX Pre-FEC BER": 22388041291,
|
||
"RX Post-FEC BER": 22379111198,
|
||
"RX Optical Level": -17.544, // dBm
|
||
"TX Optical Level": 5.532 // dBm
|
||
},
|
||
"OLT-PON": {
|
||
"RX Pre-FEC BER": 0,
|
||
"RX Post-FEC BER": 0,
|
||
"TX Optical Level": 5.6,
|
||
"RX Optical Level": -18.3
|
||
},
|
||
"ONU-EnhancedFecPmHistData-32769": { // NOT used by extractor —
|
||
"uncorrectable_code_words": 1829, // these are corrected/
|
||
"corrected_bytes": 181, // uncorrectable code-word
|
||
"corrected_code_words": 181 // counts, not BER values
|
||
},
|
||
// ... many more sub-objects
|
||
}
|
||
}
|
||
```
|
||
|
||
The renderer's display field `Equipment ID` is sometimes also on the CFG,
|
||
not just the STATE — `equipmentId(cfg)` in `app.js` checks both paths.
|
||
|
||
---
|
||
|
||
## 7. Firmware bank semantics
|
||
|
||
(Full version of the table in `README.md`.)
|
||
|
||
ONU has two flash slots. `FW Bank Ptr` identifies the active one.
|
||
MCMS triggers a firmware download by:
|
||
|
||
1. Writing the new file + version into the target slot's
|
||
`FW Bank Files[slot]` / `FW Bank Versions[slot]`.
|
||
2. Setting `FW Bank Ptr` to that slot.
|
||
|
||
The ONU then downloads, and on completion MCMS flips `FW Bank Ptr` to
|
||
the new slot (so the ONU boots into the new image on its next reboot).
|
||
|
||
The bank-strategy module always writes to the **inactive** slot. If
|
||
`FW Bank Ptr` is unset (65535) it writes to slot 1, leaving slot 0 as
|
||
the factory fallback.
|
||
|
||
For the bulk-task path (Procedure 8), MCMS needs a single bank number
|
||
per task. So if a selection has ONUs with different active banks, we
|
||
bucket by `writeSlot` and create one task per bucket — each task has
|
||
a Procedure-8-shaped `AUTO-TASK-CFG.Task Details.ONU.FW Bank Ptr`.
|
||
|
||
---
|
||
|
||
## 8. CSV file formats
|
||
|
||
Both kinds of CSV the app produces are written with the same
|
||
`writeCsvFile` helper in `main.js`:
|
||
|
||
- Always UTF-8 BOM (Excel auto-detects encoding → Norwegian Æ/Ø/Å
|
||
survive on Windows).
|
||
- Always CRLF line endings.
|
||
- Every cell quoted (defensive against commas in addresses, embedded
|
||
quotes in operator-typed names, embedded newlines from copy/paste).
|
||
- Saved to `app.getPath('downloads')` (`~/Downloads` on macOS/Linux,
|
||
`C:\Users\<you>\Downloads` on Windows).
|
||
|
||
### Plan CSV — `pon-upgrade-<hint>-<stamp>.csv`
|
||
|
||
Auto-saved when Execute is clicked, **before** any MCMS write — so
|
||
even if the API call fails mid-way there's a record of intent.
|
||
|
||
Columns (20): ONU ID, Name, Address, PON Mode, Active slot (before),
|
||
Write slot (target), Slot 0 version (before), Slot 1 version (before),
|
||
Target version, Target file, FEC health, ONU RX Pre-FEC BER,
|
||
ONU RX Post-FEC BER, OLT RX Pre-FEC BER, OLT RX Post-FEC BER,
|
||
ONU RX Optical (dBm), ONU TX Optical (dBm), FEC sample time, Mode, Notes.
|
||
|
||
The hint is derived from `<targetVersion>-<mode>-<count>onus`.
|
||
|
||
### Verify CSV — `pon-verify-<sourcehint>-<stamp>.csv`
|
||
|
||
Auto-saved after the verify comparison finishes. Hint reuses the
|
||
trailing descriptor of the source CSV (strip `pon-upgrade-` prefix and
|
||
`.csv` suffix), so the verify file sorts next to its source in the
|
||
filesystem.
|
||
|
||
Columns (23): identifiers, Target version, Active version (after),
|
||
Upgrade applied?, FEC health (before), FEC health (after), all four
|
||
BER counters before+after (ONU + OLT, pre + post), ONU RX/TX optical
|
||
before+after, After sample time, Verdict, Fetch error.
|
||
|
||
### Verdict strings (in the Verify CSV)
|
||
|
||
| Verdict | Trigger |
|
||
|---|---|
|
||
| `fixed (was buggy)` | `buggy → ok` |
|
||
| `still buggy (firmware bug persists)` | `buggy → buggy` |
|
||
| `improved (no longer mirroring counters)` | `buggy → pre` |
|
||
| `changed: now reporting genuine post-FEC errors` | `buggy → post` |
|
||
| `still healthy` | `ok → ok` |
|
||
| `improved` | `pre|post → ok` |
|
||
| `improved (post→pre)` | post→pre |
|
||
| `regressed (now post-FEC errors)` | `ok → post` |
|
||
| `regressed (now buggy)` | `ok → buggy` |
|
||
| `regressed (now marginal)` | `ok → pre` |
|
||
| `regressed (pre→post)` | pre→post |
|
||
| `still has post-FEC errors` | `post → post` |
|
||
| `still marginal (pre-FEC only)` | `pre → pre` |
|
||
| `upgrade NOT applied (active version unchanged)` | `cfg.activeVersion !== target` |
|
||
| `fetch failed` | per-ONU snapshot threw |
|
||
| `no current FEC data` | flag=nodata |
|
||
|
||
**The raw before/after counter values are misleading on their own**
|
||
because ONU FEC counters typically reset on the upgrade reboot. The
|
||
verdict is computed from the **flag transition**, not the delta.
|
||
|
||
---
|
||
|
||
## 9. Known limitations / not implemented
|
||
|
||
- **No task-progress polling.** Once a bulk task is submitted, MCMS
|
||
owns the rollout and the app doesn't poll `/v3/tasks/states/<id>/`.
|
||
Operators use the verify view (or the PON Manager UI) to confirm.
|
||
- **No post-execute results CSV.** The plan CSV captures intent
|
||
pre-write. Per-ONU success/failure (in Procedure 7 mode) and task
|
||
submission errors (in Procedure 8 mode) are visible on screen but
|
||
not dumped to disk. Add-on candidate.
|
||
- **No offline / cached fleet snapshot.** Every `Load fleet` hits MCMS.
|
||
- **`/v1/onus/<id>/upgrade/status/`** is referenced in some MCMS spec
|
||
documents but returns 404 on this build. Fallback: poll
|
||
`GET /v1/onus/configs/<id>/` and watch `FW Bank Versions` /
|
||
`FW Bank Ptr` flip.
|
||
- **PATCH not used** — MCMS requires full-document PUT for CFG updates
|
||
(dev guide is explicit about this).
|
||
- **Server-side query/projection not used** — see §5.9.
|
||
- **No time-series PM data shown.** We use the live snapshot from
|
||
ONU-STATE (§5.11). `GET /onus/stats/<id>/` exists but is not wired
|
||
up.
|
||
- **No retries on transient HTTP errors** beyond the per-ONU
|
||
best-effort delete-state. Bulk delete and per-ONU PUTs surface
|
||
errors per row but don't auto-retry.
|
||
- **Schedule field is UTC only.** The datetime-local picker converts
|
||
from the browser's local TZ. Operators in different TZs see different
|
||
inputs producing the same `Scheduled Start Time` value in the task —
|
||
intentional, but worth noting.
|
||
|
||
---
|
||
|
||
## 10. Conventions
|
||
|
||
### Adding a new IPC channel
|
||
|
||
Touch three files in order:
|
||
|
||
1. **`src/mcms-api.js`** — add the method on `McmsClient` if it talks
|
||
to MCMS.
|
||
2. **`main.js`** — `ipcMain.handle('api:foo', ...)`. Wrap errors via
|
||
`toWireError` so the renderer never sees stack traces.
|
||
3. **`preload.js`** — add `foo: (opts) => call('api:foo', opts)` to
|
||
the `window.api` contract. **The renderer cannot call channels not
|
||
listed here.**
|
||
4. **`renderer/app.js`** — use `window.api.foo(...)`.
|
||
|
||
For streaming progress events, also add an `onFooProgress(handler)`
|
||
function in the preload (returns an unsubscribe function).
|
||
|
||
### CSS conventions
|
||
|
||
Status colour classes (declared in `app.css`):
|
||
|
||
| Class | Meaning | CSS var |
|
||
|---|---|---|
|
||
| `.status-ok` | success / healthy | `--ok` (green) |
|
||
| `.status-warn` | margin / pre-FEC | `--warn` (yellow) |
|
||
| `.status-err` | failure / post-FEC / regressed | `--danger` (red) |
|
||
| `.status-info` | informational / buggy ONU | `--info` (blue) |
|
||
| `.status-pending` | in-flight / unknown | `--fg-dim` (grey) |
|
||
| `.muted` | secondary text | `--fg-dim` (grey) |
|
||
|
||
`.fec-cell` and `#verify-table tbody td` override
|
||
`white-space: nowrap` to allow stacked content (pill on top, counters
|
||
underneath).
|
||
|
||
### Verbose logging
|
||
|
||
`McmsClient(verbose: true)` (default in this app) logs every request:
|
||
|
||
- Method + path + cookie names (cookie values redacted)
|
||
- All outgoing headers (`X-CSRFToken` redacted)
|
||
- On non-2xx: full response headers + full response body
|
||
|
||
The combination is enough to diff against a working `curl` for
|
||
"why is this 500?" investigations. **Turn off via
|
||
`PON_FLEET_VERBOSE=0`** if logs are leaking secrets to a shared
|
||
terminal.
|
||
|
||
---
|
||
|
||
## 11. Testing
|
||
|
||
There's no formal test runner. Two cheap signals:
|
||
|
||
1. `npm run check` — `node --check` on every JS file. Catches syntax
|
||
errors before launching Electron.
|
||
2. Inline `node -e '...'` fixtures for pure functions (FEC extractor,
|
||
CSV roundtrip, verdict logic). Vendor the function bodies into the
|
||
test snippet rather than importing from `renderer/app.js` — that
|
||
file expects `window` / `document`.
|
||
|
||
When refactoring `extractOnuHealth`, re-test against the GNXS
|
||
real-world fixture (post=22379111198, pre=22388041291): it must flag
|
||
as `buggy`. That ratio (0.9996) is the edge case the 95% threshold
|
||
was designed to catch.
|
||
|
||
---
|
||
|
||
## 12. Reference documents
|
||
|
||
Stored alongside the project in the parent `PON-API` folder:
|
||
|
||
- `323-1961-306_mcms_6_2_rest_api_developer_guide.pdf` — REST API
|
||
reference. Cited as "the dev guide".
|
||
- `323-1961-302_mcms_6_2_pon_manager_user_guide.pdf` — UI guide.
|
||
Procedures 7, 8, 38, 43 referenced from code comments.
|
||
- `323-1963-105_mcms_6_2_onu_planning_guide.pdf` — ONU planning
|
||
context (PON design, link budgets).
|
||
|
||
Key procedure citations:
|
||
|
||
- **Procedure 2** (REST API dev guide) — determining ONU registration
|
||
status via OLT-STATE buckets.
|
||
- **Procedure 7** (REST API dev guide) — per-ONU firmware upgrade via
|
||
full-doc PUT.
|
||
- **Procedure 8** (REST API dev guide) — bulk firmware upgrade via
|
||
`AUTO-TASK-CFG`.
|
||
- **Procedure 38** (PON Manager User Guide) — Resetting bit error
|
||
rate values; identifies the four FEC counters in the UI.
|
||
- **Procedure 43** (PON Manager User Guide) — Deleting an ONU.
|
||
|
||
---
|
||
|
||
## 13. History / non-obvious design choices
|
||
|
||
- **CSV is auto-saved BEFORE the API write, not after.** Intentional:
|
||
capture intent even if the write fails. A "results CSV" with
|
||
per-ONU success/failure is an obvious add-on but not yet wired.
|
||
- **Plan CSV uses 20 columns even for trivial cases.** Wide is fine
|
||
for spreadsheets; narrow forces splits later.
|
||
- **FEC pre-flight only runs on the selection, not the fleet.**
|
||
Originally implemented to query the whole fleet at preview time,
|
||
but operator preference is "right before commit, not for the
|
||
entire fleet" — keeps `/v3/onus/states/<id>/` calls bounded to N
|
||
selected.
|
||
- **Buggy detection uses ratio, not equality.** See §4 — real
|
||
observed buggy pairs aren't bit-identical (timing windows differ),
|
||
so `post == pre` misses the case. `post / pre >= 0.95` catches it.
|
||
- **Optical thresholds add a yellow margin band** beyond the user
|
||
guide's binary green/red. Operators want a warning before a link
|
||
actually fails.
|
||
- **`__Host-` cookie prefix support was added retroactively** —
|
||
initial code only matched `csrftoken`, causing intermittent 500s
|
||
on session refresh.
|
||
- **User-Agent header was the root cause of one prolonged debugging
|
||
session.** Documented in §5.1 so we don't lose that knowledge.
|
||
|
||
---
|
||
|
||
## 14. OLT PON flooding mode (bulk private↔auto)
|
||
|
||
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.
|
||
|
||
### The mutation rule (the whole feature in one fact)
|
||
|
||
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:
|
||
|
||
- **`PON FLOOD ID` key present** (any value, including `0`) → **private**
|
||
- **`PON FLOOD ID` key absent** → **auto**
|
||
|
||
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.
|
||
|
||
### Data flow
|
||
|
||
```
|
||
listOlts ─► McmsClient.listAllOltConfigs ─► summarizeOltNniNetworks (pure)
|
||
executeFloodChange ─► per-OLT: getOltConfig ─► planFloodChange (pure, no-mutate)
|
||
─► 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 }`.
|
||
- **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.
|
||
- **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
|
||
live doc immediately before each PUT to avoid stomping a concurrent
|
||
edit, then writes the whole document back.
|
||
|
||
### UI (OLT inspector view, `#view-olts`)
|
||
|
||
Reached from the fleet view's "Open OLT inspector…" button. Filters by TAG
|
||
pattern + current mode + OLT name; selecting an OLT queues every matching
|
||
service on it. Two-step confirm (summary `confirm` + typed `APPLY`) before
|
||
writing. Auto-saves a result CSV to `~/Downloads/pon-olt-flood-…csv` with
|
||
per-OLT result, change detail, and skip reasons — same `writeCsvFile`
|
||
helper and conventions as the upgrade/verify CSVs (§8).
|
||
|
||
### Testing the pure helpers
|
||
|
||
`floodModeOfNni` / `summarizeOltNniNetworks` / `planFloodChange` are
|
||
exported from `src/mcms-api.js` and have no DOM/Electron deps, so they're
|
||
directly `require`-able in a `node -e` fixture. The invariants worth
|
||
re-checking after any edit: (1) `planFloodChange` does **not** mutate its
|
||
input, (2) re-applying the same change is a no-op, (3) non-matching NNI
|
||
entries pass through untouched.
|
||
|
||
---
|
||
|
||
*Last updated: 2026-06-23.*
|