Add top tabs + telemetry Dashboard + animated edge glow
Restructure the UI behind top-level tabs (Dashboard / ONU / OLT) so more features have room to land. showView() now drives the active tab via a VIEW_TO_TAB map, so sub-views (campaign/verify) keep the ONU tab lit. Tabs appear on login (landing on Dashboard) and hide on logout. Dashboard: a read-only network telemetry view modeled on the PONGo iOS app's DashboardViewModel. New api:dashboardStats aggregates, all in main.js (renderer just formats): - counts: ONUs, OLTs, controllers (controllers best-effort via /v3/controllers/configs/) - aggregate traffic (OLT TX BW = downstream, RX BW = upstream) - health: OLTs with laser off; abnormal-Rx count (rx < -28 || > -10) - ONU registration-state breakdown (clicking a state deep-links to the ONU tab pre-filtered) - opt-in detailed scan: per-ONU optical + FEC-health distribution fmtBps is a port of the iOS Format.bps. Edge glow: #edge-glow draws a neon conic-gradient beam masked to a 3px border ring and spins an @property --beam-angle 0->360deg, so a glow travels around the screen edge. Honors prefers-reduced-motion. mcms-api: add listAllControllerConfigs. Docs: CLAUDE.md section 15 + README + IPC table. Verified: npm run check passes; rendered the Dashboard + tabs + edge beam offscreen (beam frozen on the right edge to confirm it follows the border). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7cc9649f03
commit
9106e5a071
8 changed files with 533 additions and 12 deletions
113
main.js
113
main.js
|
|
@ -706,3 +706,116 @@ ipcMain.handle('api:executeFloodChange', async (event, opts) => {
|
|||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- 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.
|
||||
*/
|
||||
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).
|
||||
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(),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue