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
110
src/dashboard.js
Normal file
110
src/dashboard.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Shared dashboard telemetry aggregation. Pure — no Electron, no HTTP — so
|
||||
// both the Electron main process (api:dashboardStats) and the web server
|
||||
// (/api/dashboardStats) compute identical numbers. Modeled on the PONGo
|
||||
// iOS app's DashboardViewModel.
|
||||
|
||||
const { extractOnuHealth } = require('./mcms-api');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// PONGo OpticalThreshold (dBm): Rx in-spec is -28..-10; Tx floor is 3.
|
||||
const RX_LOW = -28, RX_HIGH = -10, TX_LOW = 3;
|
||||
|
||||
/**
|
||||
* @param {object} args
|
||||
* @param {object[]} args.olts OLT-STATE docs (listAllOltStates).
|
||||
* @param {object[]|null} [args.onuStates] ONU-STATE docs for the heavy
|
||||
* optical/FEC scan, or null/undefined to skip it (the light default).
|
||||
* @param {number|null} [args.controllerCount]
|
||||
*/
|
||||
function summarizeDashboard({ olts = [], onuStates = null, controllerCount = null } = {}) {
|
||||
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.
|
||||
// OLT TX BW = downstream (OLT→ONU), RX BW = upstream (ONU→OLT).
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// Opt-in heavy scan: every ONU-STATE for optical + FEC health. Keeps the
|
||||
// actual offender lists so the dashboard can drill down without re-fetch.
|
||||
let optical = {
|
||||
available: false,
|
||||
abnormalRx: [], abnormalRxCount: 0,
|
||||
abnormalTx: [], abnormalTxCount: 0,
|
||||
};
|
||||
let fecCounts = null;
|
||||
if (Array.isArray(onuStates)) {
|
||||
const abnormalRx = [], abnormalTx = [];
|
||||
let available = false;
|
||||
fecCounts = { ok: 0, pre: 0, post: 0, buggy: 0, nodata: 0, error: 0 };
|
||||
for (const st of onuStates) {
|
||||
const health = extractOnuHealth(st);
|
||||
if (fecCounts[health.flag] !== undefined) fecCounts[health.flag] += 1;
|
||||
const o = health.optical || {};
|
||||
const rx = typeof o.rx === 'number' ? o.rx : null;
|
||||
const tx = typeof o.tx === 'number' ? o.tx : null;
|
||||
const name = st?.ONU?.Name || '';
|
||||
if (rx !== null) {
|
||||
available = true;
|
||||
if (rx < RX_LOW || rx > RX_HIGH) abnormalRx.push({ onuId: st._id, rx, name });
|
||||
}
|
||||
if (tx !== null) {
|
||||
available = true;
|
||||
if (tx < TX_LOW) abnormalTx.push({ onuId: st._id, tx, name });
|
||||
}
|
||||
}
|
||||
optical = {
|
||||
available,
|
||||
abnormalRx, abnormalRxCount: abnormalRx.length,
|
||||
abnormalTx, abnormalTxCount: abnormalTx.length,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
onuTotal, oltCount, controllerCount,
|
||||
onuCounts,
|
||||
traffic: trafficSeen ? { downBps, upBps } : null,
|
||||
lasersOff,
|
||||
optical,
|
||||
fecCounts,
|
||||
extraStats: Array.isArray(onuStates),
|
||||
sampledAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { summarizeDashboard };
|
||||
Loading…
Add table
Add a link
Reference in a new issue