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>
This commit is contained in:
parent
9106e5a071
commit
96b0264179
10 changed files with 1096 additions and 109 deletions
106
main.js
106
main.js
|
|
@ -12,6 +12,7 @@ const {
|
|||
McmsClient, McmsApiError, extractOnuHealth,
|
||||
summarizeOltNniNetworks, planFloodChange,
|
||||
} = require('./src/mcms-api');
|
||||
const { summarizeDashboard } = require('./src/dashboard');
|
||||
const { planUpgrade, activeBankPtr, inactiveBank } = require('./src/bank-strategy');
|
||||
|
||||
/** @type {McmsClient | null} */
|
||||
|
|
@ -709,112 +710,21 @@ ipcMain.handle('api:executeFloodChange', async (event, opts) => {
|
|||
|
||||
// ---- Dashboard telemetry --------------------------------------------
|
||||
|
||||
function toNum(v) {
|
||||
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
|
||||
if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v))) return Number(v);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Network-wide telemetry for the Dashboard tab, modeled on the PONGo iOS
|
||||
* app's DashboardViewModel. The "light" path is cheap — everything below
|
||||
* comes from the small OLT-STATE docs:
|
||||
* - OLT count
|
||||
* - ONU total + registration-state breakdown (OLT-STATE "ONU States"
|
||||
* buckets, dev guide Procedure 2)
|
||||
* - aggregate traffic: OLT TX BW = downstream, RX BW = upstream
|
||||
* (STATS["OLT-PON"]["TX|RX BW Ethernet Rate bps"])
|
||||
* - OLTs with the laser shut down (OLT["Laser Shutdown"] != "Laser ON")
|
||||
* Controllers count is best-effort (not on every build).
|
||||
*
|
||||
* The heavy per-ONU optical/FEC scan (pull every ONU-STATE) runs only
|
||||
* when `extraStats` is set — same opt-in as the iOS "Rx scan" toggle.
|
||||
* Network-wide telemetry for the Dashboard tab. Aggregation lives in the
|
||||
* shared, pure src/dashboard.js (so the web server computes identically);
|
||||
* this handler just fetches the inputs. Light by default (small OLT-STATE
|
||||
* docs); the per-ONU optical/FEC scan runs only when `extraStats` is set.
|
||||
*/
|
||||
ipcMain.handle('api:dashboardStats', async (_ev, opts) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const extraStats = !!(opts && opts.extraStats);
|
||||
|
||||
const olts = await c.listAllOltStates();
|
||||
const oltCount = olts.length;
|
||||
|
||||
// Registration map: onuId -> bucket. Prefer a non-Registered bucket on
|
||||
// duplicates (the interesting signal), matching api:listFleet.
|
||||
const stateByOnu = new Map();
|
||||
for (const olt of olts) {
|
||||
const buckets = olt?.['ONU States'] || {};
|
||||
for (const [bucket, ids] of Object.entries(buckets)) {
|
||||
if (!Array.isArray(ids)) continue;
|
||||
for (const id of ids) {
|
||||
const existing = stateByOnu.get(id);
|
||||
if (!existing || existing === 'Registered') stateByOnu.set(id, bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
const onuTotal = stateByOnu.size;
|
||||
const stateCounts = {};
|
||||
for (const s of stateByOnu.values()) stateCounts[s] = (stateCounts[s] || 0) + 1;
|
||||
const onuCounts = Object.entries(stateCounts)
|
||||
.map(([state, count]) => ({ state, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// Aggregate traffic + laser state from OLT-STATE.
|
||||
let downBps = 0, upBps = 0, trafficSeen = false;
|
||||
const lasersOff = [];
|
||||
for (const olt of olts) {
|
||||
const pon = olt?.STATS?.['OLT-PON'] || {};
|
||||
const tx = toNum(pon['TX BW Ethernet Rate bps']);
|
||||
const rx = toNum(pon['RX BW Ethernet Rate bps']);
|
||||
if (tx !== null) { downBps += tx; trafficSeen = true; }
|
||||
if (rx !== null) { upBps += rx; trafficSeen = true; }
|
||||
const laser = olt?.OLT?.['Laser Shutdown'];
|
||||
if (typeof laser === 'string' && laser.toLowerCase() !== 'laser on') {
|
||||
lasersOff.push({ oltId: olt._id, name: olt?.OLT?.Name || '', laser });
|
||||
}
|
||||
}
|
||||
|
||||
// Controllers — best-effort (some builds don't expose this).
|
||||
const onuStates = extraStats ? await c.listAllOnuStates() : null;
|
||||
let controllerCount = null;
|
||||
try {
|
||||
controllerCount = (await c.listAllControllerConfigs()).length;
|
||||
} catch (_) { controllerCount = null; }
|
||||
|
||||
// Opt-in heavy scan: every ONU-STATE for optical + FEC health.
|
||||
let optical = { available: false, abnormalRx: [], abnormalRxCount: 0 };
|
||||
let fecCounts = null;
|
||||
if (extraStats) {
|
||||
const states = await c.listAllOnuStates();
|
||||
const RX_LOW = -28, RX_HIGH = -10; // PONGo OpticalThreshold.rxAbnormal
|
||||
const abnormalRx = [];
|
||||
let available = false;
|
||||
fecCounts = { ok: 0, pre: 0, post: 0, buggy: 0, nodata: 0, error: 0 };
|
||||
for (const st of states) {
|
||||
const health = extractOnuHealth(st);
|
||||
if (fecCounts[health.flag] !== undefined) fecCounts[health.flag] += 1;
|
||||
const rx = health.optical && typeof health.optical.rx === 'number' ? health.optical.rx : null;
|
||||
if (rx !== null) {
|
||||
available = true;
|
||||
if (rx < RX_LOW || rx > RX_HIGH) {
|
||||
abnormalRx.push({ onuId: st._id, rx, name: st?.ONU?.Name || '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
optical = { available, abnormalRx, abnormalRxCount: abnormalRx.length };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
onuTotal, oltCount, controllerCount,
|
||||
onuCounts,
|
||||
traffic: trafficSeen ? { downBps, upBps } : null,
|
||||
lasersOff,
|
||||
optical,
|
||||
fecCounts,
|
||||
extraStats,
|
||||
sampledAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
try { controllerCount = (await c.listAllControllerConfigs()).length; } catch (_) { /* best-effort */ }
|
||||
return { ok: true, data: summarizeDashboard({ olts, onuStates, controllerCount }) };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue