onu detail: parent OLT + LLDP switch + traffic history + alarms + UNI; single-ONU upgrade

De-compacts the ONU info page into a responsive card grid and enriches it
with one new endpoint (api:getOnuDetail) summarized by the pure, shared
src/onudetail.js:
- Parent OLT (name/model/FW/PON) — follows the OLT MAC from the ONU
  alarm-history doc to its OLT-CFG.
- Upstream switch (LLDP neighbour of the OLT NNI) from the alarm-history
  Switch block: system name, port, IPv4, chassis.
- Traffic (last ~hour) with current + peak down/up and inline SVG
  sparklines, from /onus/stats/ (down = OLT TX rate, up = OLT RX rate).
- UNI-ETH ports (speed/duplex/enable) + last Ethernet LOS (LAN-LOS alarm).
- Alarms: active-first, severity-sorted with raised counts + last raised.
- CPE card folded into the same single fetch.

New client methods getOnuAlarms + getOnuStatsSeries; api:getOnuDetail in
main.js + web/server.js, exposed in both bridges. Not TTL-cached (live).

Single-ONU upgrade: the Firmware card gets a firmware picker + "Upgrade this
ONU" button reusing the bulk per-ONU path (executePerOnu with one id,
Procedure 7 inactive-bank write), behind a typed UPGRADE confirm; refreshes
the detail on success.

Docs: CLAUDE.md §17 + §5.11 (stats endpoint now used for the sparkline) +
IPC table.

Verified: node --check; onudetail.js unit-tested 11/11 against the real
alarm-history / cfg / stats / OLT-cfg fixtures (firmware banks, UNI, alarm
sort, LAN-LOS, OLT active-bank FW, LLDP switch, traffic skip-nontraffic +
down/up mapping + current/peak); card grid rendered offscreen (9 cards, 2
sparklines, upgrade button).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-23 15:47:50 +02:00
parent 55e95ed467
commit 276c3e9faa
9 changed files with 474 additions and 46 deletions

View file

@ -533,8 +533,21 @@ button.danger:hover:not(:disabled) {
.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 { overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding-right: 6px; }
.onu-detail h2 { margin: 0; font-size: 17px; color: var(--neon); text-shadow: var(--glow-neon); }
/* De-compacted: cards flow into as many columns as fit the width. */
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
gap: 14px;
align-items: start;
}
svg.spark { vertical-align: middle; opacity: 0.9; }
.alarm-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
.alarm-row:last-child { border-bottom: none; }
.alarm-row .alarm-text { flex: 1; }
.alarm-row .alarm-meta { color: var(--fg-dim); font-size: 11px; white-space: nowrap; }
.fw-upg { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border); padding-top: 12px; }
.detail-card {
background: var(--bg-elev);
border: 1px solid var(--border);