onu: info/stats sub-tab with CPE lease health + OUI vendor
Adds an ONU info page and splits the ONU tab into two sub-tabs (Info / stats, Fleet & upgrade). The ONU tab now lands on Info; campaign/verify keep the Fleet sub-tab lit. ONU info page (#view-onu-info): master-detail over the already-loaded fleet (search + status filter, no new bulk fetch). Per-ONU detail shows identity, link/optical + FEC (read from the merged ONU-STATE), firmware, and a CPE card. CPE (customer router): - McmsClient.getCpe — GET /api/cpe/onu/<id>/ (unversioned, [status,doc] tuple). - src/cpe.js (pure, shared): summarizeCpe + leaseHealth. Renewal rule — <50% of the lease remaining => "renewal overdue" (amber), past end => expired. v4 expiry = DHCP "Remove Time"; v6 spans Last Success -> Preferred Lifetime (T1 midpoint), with Prefix Delegation surfaced. - api:getOnuCpe (main.js + web/server.js, exposed in both bridges). OUI vendor (src/oui.js, pure, shared): lazy fetch of the IEEE oui.csv, prefix->org map, cached in memory ~30 days + best-effort to disk (PFW_CACHE_DIR / temp; PrivateTmp makes /tmp writable). Resolved server-side in getOnuCpe; every failure degrades to "unknown OUI". CPE itself is not TTL-cached (lease must be live); only the OUI list is. Dashboard abnormal Rx/Tx rows are now clickable -> deep-link to that ONU's info page (showListModal gained per-item onClick). Docs: CLAUDE.md §17 + IPC table + intro; README. Verified: node --check; cpe.js/oui.js unit-tested (15/15: lease ok/warn/ expired/unknown + 50% boundary, v6 prefix parse, OUI quoted-CSV parse + lookup); ONU info page + CPE card rendered offscreen over HTTP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5e3d9271a8
commit
55e95ed467
12 changed files with 677 additions and 7 deletions
|
|
@ -480,6 +480,94 @@ button.danger:hover:not(:disabled) {
|
|||
box-shadow: 0 0 12px rgba(33, 200, 255, 0.45), inset 0 0 14px rgba(33, 200, 255, 0.12);
|
||||
}
|
||||
|
||||
/* -------- Sub-tabs (within the ONU tab) -------- */
|
||||
.subtabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin: -4px 0 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.subtabs .subtab {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg-dim);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 700;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.subtabs .subtab:hover:not(.active) { color: var(--neon); border-color: transparent; box-shadow: none; }
|
||||
.subtabs .subtab.active {
|
||||
color: var(--neon);
|
||||
border-color: var(--neon);
|
||||
background: rgba(33, 200, 255, 0.1);
|
||||
text-shadow: var(--glow-neon);
|
||||
box-shadow: inset 0 0 12px rgba(33, 200, 255, 0.12);
|
||||
}
|
||||
|
||||
/* -------- ONU info / stats page -------- */
|
||||
.onu-info-list {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
flex: 1;
|
||||
min-height: 120px;
|
||||
}
|
||||
.onu-info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
}
|
||||
.onu-info-row:last-child { border-bottom: none; }
|
||||
.onu-info-row:hover { background: rgba(33, 200, 255, 0.06); }
|
||||
.onu-info-row.selected { background: rgba(33, 200, 255, 0.14); box-shadow: inset 3px 0 0 var(--neon); }
|
||||
.onu-info-row .oir-serial { font-weight: 600; font-size: 12px; }
|
||||
.onu-info-row .oir-sub { font-size: 11px; color: var(--fg-dim); display: flex; gap: 8px; align-items: center; }
|
||||
|
||||
.onu-detail { overflow-y: auto; display: flex; flex-direction: column; gap: 18px; padding-right: 6px; }
|
||||
.onu-detail h2 { margin: 0; font-size: 17px; color: var(--neon); text-shadow: var(--glow-neon); }
|
||||
.detail-card {
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: inset 0 0 22px rgba(33, 200, 255, 0.04);
|
||||
}
|
||||
.detail-card > h3 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: var(--neon);
|
||||
text-shadow: var(--glow-neon);
|
||||
}
|
||||
.kv {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 6px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.kv dt { color: var(--fg-dim); }
|
||||
.kv dd { margin: 0; font-variant-numeric: tabular-nums; word-break: break-all; }
|
||||
.lease-pill {
|
||||
display: inline-block;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid currentColor;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* -------- Dashboard -------- */
|
||||
.dash-wrap { overflow: hidden; }
|
||||
.dash-scroll {
|
||||
|
|
@ -592,6 +680,7 @@ button.danger:hover:not(:disabled) {
|
|||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.list-modal-item:last-child { border-bottom: none; }
|
||||
.list-modal-item.tappable { cursor: pointer; }
|
||||
.list-modal-item:hover { background: rgba(33, 200, 255, 0.06); }
|
||||
.lm-primary { font-weight: 600; }
|
||||
.lm-secondary { color: var(--fg-dim); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue