The web server's dashboardStats now reports how stale its shared cache
snapshot is — data.cache = { enabled, ttlMs, ageMs }, using the age of the
oldest dataset feeding the view (OLT-STATE, plus ONU-STATE under detailed
scan). cache.js gains ageOf(key).
The dashboard renders a live-ticking badge from it (renderCacheBadge):
- within TTL -> "◷ cached Ns ago" (info)
- past TTL -> same, warn-coloured (next load will refetch)
- no cache -> "● live" (Electron app, or PFW_CACHE_TTL_SECONDS=0)
The Dashboard Refresh button now sends fresh:true to bypass the cache and
reset the age; the detailed-stats toggle and auto-loads use the cache.
Shared renderer change, so the Electron app shows "live" (it sends no cache
field). Verified offscreen over HTTP: cached / stale / live states render
with the right text + colour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
823 lines
34 KiB
Markdown
823 lines
34 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 operating a **Ciena MCMS 6.2** PON Manager
|
||
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).
|
||
|
||
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:dashboardStats` | network telemetry aggregates (Dashboard tab) | — |
|
||
| `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.
|
||
|
||
---
|
||
|
||
## 15. Tabbed navigation & Dashboard
|
||
|
||
### Tabs
|
||
|
||
The topbar holds a `<nav id="tabs">` (Dashboard / ONU / OLT), hidden until
|
||
login. Navigation goes through the existing `showView(id)` — it now also
|
||
sets the active tab via the `VIEW_TO_TAB` map, so sub-views keep the right
|
||
tab lit (campaign/verify → ONU). To add a tab: add a `.tab` button with
|
||
`data-view="#view-x"` in `index.html`, a `#view-x` section, and a
|
||
`VIEW_TO_TAB` entry. Login lands on Dashboard; logout hides the tabs and
|
||
clears `state.dashboard`.
|
||
|
||
### Dashboard telemetry
|
||
|
||
Modeled on the PONGo iOS app's `DashboardViewModel`
|
||
(`ssh://…/Svorka/PONGo_ios.git`). One IPC call, `api:dashboardStats`,
|
||
returns aggregates computed in `main.js` — the renderer only formats them.
|
||
|
||
**Light path (default)** — everything from the small OLT-STATE docs, no
|
||
per-ONU fetches:
|
||
|
||
- OLT count; ONU total + registration-state breakdown (the same
|
||
`OLT-STATE["ONU States"]` buckets as `api:listFleet`, Procedure 2).
|
||
- Aggregate traffic — **OLT TX BW = downstream, RX BW = upstream**, summed
|
||
over `STATS["OLT-PON"]["TX|RX BW Ethernet Rate bps"]`.
|
||
- OLTs with laser off — `OLT["Laser Shutdown"] != "Laser ON"`.
|
||
- Controllers count via `listAllControllerConfigs` (`/v3/controllers/
|
||
configs/`) — **best-effort**, wrapped in try/catch (returns `null` if the
|
||
build doesn't expose it; UI shows "—").
|
||
|
||
**Detailed path (`extraStats`, opt-in toggle)** — pulls every ONU-STATE
|
||
(heavy, like a fleet load) to add: abnormal-Rx count (`rx < −28 || rx >
|
||
−10` dBm, the iOS `OpticalThreshold.rxAbnormal` rule) and the FEC-health
|
||
distribution via `extractOnuHealth`.
|
||
|
||
The ONU-state rows drill down: clicking one sets `#filter-status` and jumps
|
||
to the ONU tab (loading the fleet if needed). `fmtBps` in `app.js` is a
|
||
port of the iOS `Format.bps`.
|
||
|
||
### Animated edge glow
|
||
|
||
`#edge-glow` is a fixed, pointer-events-none full-viewport element. A
|
||
`conic-gradient(from var(--beam-angle), …)` with a white-hot core is masked
|
||
to just the 3px border ring (the standard `mask` + `mask-composite:
|
||
exclude` border trick) and animated by spinning the `@property
|
||
--beam-angle` 0→360°, so a neon beam travels around the screen edge.
|
||
Respects `prefers-reduced-motion`. z-index sits below the scanline overlay
|
||
(9999) and modals (10000).
|
||
|
||
---
|
||
|
||
## 16. Web server variant (`web/`)
|
||
|
||
A multi-user, browser-based deployment of the *same* tooling lives in
|
||
`web/` — see `web/README.md`. It is **not a fork**: it reuses `src/*` and
|
||
serves `renderer/app.js` + `renderer/app.css` verbatim, rewriting
|
||
`renderer/index.html` on the fly to swap in a browser `window.api` shim
|
||
(`web/public/api-web.js`) and a responsive overlay (`web/public/web.css`).
|
||
|
||
Consequences for editing:
|
||
|
||
- **The renderer is shared.** Changes to `renderer/*` and `src/*` flow to
|
||
both the Electron app and the web app automatically. Keep `app.js`
|
||
free of Electron/Node globals (it already only touches `window.api` +
|
||
DOM) so the shim stays sufficient.
|
||
- **`window.api` is the contract.** If you add an IPC channel (see §10),
|
||
also add it to `web/server.js` (a route) **and** `web/public/api-web.js`
|
||
(the shim method) or the web app won't have it. Streaming channels use
|
||
NDJSON (progress lines + a `{__final:…}` line) instead of IPC events.
|
||
- **Per-session isolation.** `web/server.js` keeps one `McmsClient` per
|
||
browser session (cookie `pfw_sid`), vs. the Electron app's single global
|
||
`client`. CSV writes become browser downloads; file dialogs become
|
||
`<input type=file>`. Dashboard aggregation is shared via `src/dashboard.js`.
|
||
- **Shared upstream cache** (`web/cache.js`). Heavy bulk *reads* (ONU/OLT
|
||
config+state lists, controllers, firmware) go through a TTL cache with
|
||
single-flight, keyed by MCMS host, so concurrent operators collapse into
|
||
one upstream fetch (`PFW_CACHE_TTL_SECONDS`, default 60). Per-ONU
|
||
read-modify-write fetches and the FEC pre-flight stay **uncached** (must be
|
||
live). Writes call `invalidate(session, [...])` for the affected datasets.
|
||
This cache is web-only; the single-user Electron app doesn't need it.
|
||
`dashboardStats` attaches `data.cache = { enabled, ttlMs, ageMs }` (age of
|
||
the oldest dataset feeding the view); the dashboard renders a live-ticking
|
||
age badge from it (`renderCacheBadge`), and the Dashboard **Refresh** button
|
||
sends `fresh:true` to bypass the cache. The Electron app sends no cache
|
||
field, so the badge shows "live".
|
||
- **Deploy** lives in `web/deploy/` (systemd unit + env template +
|
||
`update.sh`); Debian-13-LXC clone-to-run steps and the Nginx Proxy Manager
|
||
setup are in `web/README.md`. Note streams send `X-Accel-Buffering: no` so
|
||
nginx/NPM don't buffer the NDJSON progress.
|
||
|
||
The web app must stay a subdirectory of this repo — it `require`s `../src/*`
|
||
and borrows `../node_modules` (`tough-cookie`); no separate install. Deploy
|
||
uses `npm install --omit=dev` (not `npm ci` — `package-lock.json` is
|
||
gitignored) which pulls `tough-cookie` and skips the Electron devDep.
|
||
|
||
---
|
||
|
||
*Last updated: 2026-06-23.*
|