ponfw/CLAUDE.md
Jon Vanvik 96b0264179 Add multi-user web server variant (browser UI behind a reverse proxy)
New web/ subfolder: a Node http server that serves the SAME tooling as the
Electron app to any browser, with per-session MCMS credentials so several
operators can use it at once behind a TLS reverse proxy. Mobile-responsive.

Not a fork — it reuses the core and the UI:
- serves renderer/app.js + app.css verbatim and rewrites index.html on the
  fly (mobile viewport + browser window.api shim + responsive overlay)
- web/public/api-web.js: a drop-in window.api over fetch + NDJSON streaming;
  Electron file dialogs -> <input type=file>, CSV-to-Downloads -> Blob
- web/server.js: per-session McmsClient (cookie pfw_sid), idle expiry,
  routes mirroring the IPC handlers, NDJSON for the 6 streaming ops
- pure Node http, no new deps (borrows ../node_modules tough-cookie)

Shared improvements (benefit both apps):
- src/dashboard.js: extracted, pure dashboard aggregation (main.js now uses
  it); adds abnormal-Tx detection alongside abnormal-Rx
- renderer: dashboard health stats are now clickable — abnormal Rx / Tx /
  lasers-off pop a list of the offending devices (like the iOS app), via a
  new showListModal

Docs: web/README.md (run + reverse-proxy + security), CLAUDE.md §16.

Verified: node --check all; started the server and confirmed transformed
index, shared-asset serving, 401 auth gate, login (per-session client), and
NDJSON wiring; rendered login + dashboard drilldown + mobile over HTTP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:07:23 +02:00

32 KiB
Raw Blame History

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

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.
  • DeleteDELETE /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.

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:

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:

{
  "_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
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.jsipcMain.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 checknode --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 absentauto

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 privateauto. 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.

The web app must stay a subdirectory of this repo — it requires ../src/* and borrows ../node_modules (tough-cookie); no separate install.


Last updated: 2026-06-23.