onu detail polish + clickable OLT detail modal
- Fix alarm card overflow: alarm rows stack (severity+text on one line that
wraps, meta below) instead of squeezing the text to ~0 width next to the
nowrap meta — which had made long alarm text render one char per line in
narrow columns. Also min-width:0 on cards/kv so long IPv6/descr strings
don't blow out the grid track.
- Combine Parent OLT + Upstream switch into one "Parent OLT & uplink" card.
- ONU list/search de-compacted: add equipment/version + PON-mode filters;
two-line rows (serial+status, then equipment · version · last-seen).
- UNI card labels the speed as configured ("cfg Auto/Auto"); negotiated
speed isn't in the payloads — link up/down still comes from LAN-LOS.
Clickable parent OLT -> OLT detail modal (showOltModal):
- new src/oltdetail.js (pure, shared; reuses summarizeAlarms) + getOltState
/ getOltAlarms client methods + api:getOltDetail (main + web + bridges).
- shows identity, traffic + util, ASIC temperature, ONU counts, laser,
alarms, upstream switch; plus a connected-ONU list with per-ONU
FEC/optical built from the loaded fleet (filtered by _status.oltMac),
each row clickable back into that ONU's detail.
Docs: CLAUDE.md §17 + IPC table.
Verified: node --check; oltdetail.js unit-tested 9/9 (fw active-bank,
traffic down=TX/up=RX, util, ASIC temp, ONU counts, laser, LLDP, alarms);
rendered the OLT modal + richer list offscreen and confirmed the alarm-text
wrap fix (alarm head ~1-2 lines, not vertical).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
276c3e9faa
commit
2046fad0f8
11 changed files with 321 additions and 59 deletions
24
CLAUDE.md
24
CLAUDE.md
|
|
@ -121,6 +121,7 @@ stream progress events back to the renderer.
|
||||||
| `api:listOnuConfigs` / `api:getOnuConfig` | direct CFG access | — |
|
| `api:listOnuConfigs` / `api:getOnuConfig` | direct CFG access | — |
|
||||||
| `api:getOnuCpe` | CPE (router) DHCP lease health + OUI vendor | — |
|
| `api:getOnuCpe` | CPE (router) DHCP lease health + OUI vendor | — |
|
||||||
| `api:getOnuDetail` | rich ONU detail: firmware/UNI/alarms/OLT/switch/traffic/CPE | — |
|
| `api:getOnuDetail` | rich ONU detail: firmware/UNI/alarms/OLT/switch/traffic/CPE | — |
|
||||||
|
| `api:getOltDetail` | OLT detail: traffic/temp/ONU-counts/laser/alarms/switch | — |
|
||||||
| `api:listOnuFirmware` / `api:uploadFirmware` | firmware inventory | — |
|
| `api:listOnuFirmware` / `api:uploadFirmware` | firmware inventory | — |
|
||||||
| `api:planUpgrade` | dry-run plan for selected ONUs | — |
|
| `api:planUpgrade` | dry-run plan for selected ONUs | — |
|
||||||
| `api:executePerOnu` | Procedure 7 — per-ONU PUTs | `upgrade:progress` |
|
| `api:executePerOnu` | Procedure 7 — per-ONU PUTs | `upgrade:progress` |
|
||||||
|
|
@ -870,6 +871,11 @@ from `state.fleet` (`onuClientStats`); the rest streams from one
|
||||||
**not** TTL-cached (live detail). Dashboard abnormal-Rx/Tx drill-downs (§15)
|
**not** TTL-cached (live detail). Dashboard abnormal-Rx/Tx drill-downs (§15)
|
||||||
deep-link here via `openOnuInfo`.
|
deep-link here via `openOnuInfo`.
|
||||||
|
|
||||||
|
The left list/search supports name/address/serial + equipment/version +
|
||||||
|
PON-mode + status filters; rows are two-line (serial+status, then
|
||||||
|
equipment · version · last-seen). Parent OLT and upstream switch are one
|
||||||
|
combined card.
|
||||||
|
|
||||||
### Single-ONU upgrade
|
### Single-ONU upgrade
|
||||||
|
|
||||||
The Firmware card has a firmware picker + "Upgrade this ONU" button that
|
The Firmware card has a firmware picker + "Upgrade this ONU" button that
|
||||||
|
|
@ -877,6 +883,24 @@ reuses the bulk per-ONU path — `executePerOnu({ onuIds: [id], … })`
|
||||||
(Procedure 7, inactive-bank write via `bank-strategy`). Two-step `UPGRADE`
|
(Procedure 7, inactive-bank write via `bank-strategy`). Two-step `UPGRADE`
|
||||||
typed confirm; streams `upgrade:progress`; refreshes the detail on success.
|
typed confirm; streams `upgrade:progress`; refreshes the detail on success.
|
||||||
|
|
||||||
|
### Clickable parent OLT → OLT detail modal
|
||||||
|
|
||||||
|
The OLT name (and "View OLT details") opens `showOltModal` →
|
||||||
|
`api:getOltDetail` → `src/oltdetail.js` (pure, shared; reuses
|
||||||
|
`summarizeAlarms`): identity, traffic (OLT TX=down / RX=up) + util,
|
||||||
|
**ASIC temperature**, ONU counts (total/online/offline), laser, alarms, and
|
||||||
|
the upstream switch. The **connected-ONU list** (per-ONU FEC/optical, each
|
||||||
|
clickable back into the ONU detail) is built **client-side from the loaded
|
||||||
|
`state.fleet`** filtered by `_status.oltMac` — no extra per-ONU fetch; it
|
||||||
|
prompts to load the fleet if empty. `getOltDetail` fans out `getOltConfig` +
|
||||||
|
`getOltState` + `getOltAlarms`.
|
||||||
|
|
||||||
|
**Caveat — UNI link speed is the *configured* value** (`UNI-ETH n`.Speed,
|
||||||
|
usually `Auto`), shown as "cfg Auto/Auto". A negotiated link speed isn't in
|
||||||
|
the ONU-CFG / alarm / stats payloads inspected so far; link up/down comes
|
||||||
|
from the `LAN-LOS` alarm instead. Wire negotiated speed if a future
|
||||||
|
ONU-STATE sample exposes a per-UNI operational field.
|
||||||
|
|
||||||
### CPE (customer router)
|
### CPE (customer router)
|
||||||
|
|
||||||
`api:getOnuCpe` → `McmsClient.getCpe` (`GET /api/cpe/onu/<id>/` — unversioned,
|
`api:getOnuCpe` → `McmsClient.getCpe` (`GET /api/cpe/onu/<id>/` — unversioned,
|
||||||
|
|
|
||||||
19
main.js
19
main.js
|
|
@ -16,6 +16,7 @@ const { summarizeDashboard } = require('./src/dashboard');
|
||||||
const { summarizeCpe } = require('./src/cpe');
|
const { summarizeCpe } = require('./src/cpe');
|
||||||
const { lookupOui } = require('./src/oui');
|
const { lookupOui } = require('./src/oui');
|
||||||
const { summarizeOnuDetail } = require('./src/onudetail');
|
const { summarizeOnuDetail } = require('./src/onudetail');
|
||||||
|
const { summarizeOltDetail } = require('./src/oltdetail');
|
||||||
const { planUpgrade, activeBankPtr, inactiveBank } = require('./src/bank-strategy');
|
const { planUpgrade, activeBankPtr, inactiveBank } = require('./src/bank-strategy');
|
||||||
|
|
||||||
/** @type {McmsClient | null} */
|
/** @type {McmsClient | null} */
|
||||||
|
|
@ -265,6 +266,24 @@ ipcMain.handle('api:getOnuDetail', async (_ev, { onuId, windowMin = 60 }) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich OLT detail (clicked from an ONU's Parent OLT card): identity +
|
||||||
|
* traffic + temperature + ONU counts + laser + alarms + upstream switch.
|
||||||
|
*/
|
||||||
|
ipcMain.handle('api:getOltDetail', async (_ev, { oltMac }) => {
|
||||||
|
try {
|
||||||
|
const c = requireClient();
|
||||||
|
const [oltCfg, oltState, alarmsDoc] = await Promise.all([
|
||||||
|
c.getOltConfig(oltMac).catch(() => null),
|
||||||
|
c.getOltState(oltMac).catch(() => null),
|
||||||
|
c.getOltAlarms(oltMac).catch(() => null),
|
||||||
|
]);
|
||||||
|
return { ok: true, data: summarizeOltDetail({ oltCfg, oltState, alarmsDoc }) };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: toWireError(err) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('api:listOnuFirmware', async () => {
|
ipcMain.handle('api:listOnuFirmware', async () => {
|
||||||
try {
|
try {
|
||||||
const c = requireClient();
|
const c = requireClient();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||||
getOnuConfig: (opts) => call('api:getOnuConfig', opts),
|
getOnuConfig: (opts) => call('api:getOnuConfig', opts),
|
||||||
getOnuCpe: (opts) => call('api:getOnuCpe', opts),
|
getOnuCpe: (opts) => call('api:getOnuCpe', opts),
|
||||||
getOnuDetail: (opts) => call('api:getOnuDetail', opts),
|
getOnuDetail: (opts) => call('api:getOnuDetail', opts),
|
||||||
|
getOltDetail: (opts) => call('api:getOltDetail', opts),
|
||||||
listOnuFirmware: () => call('api:listOnuFirmware'),
|
listOnuFirmware: () => call('api:listOnuFirmware'),
|
||||||
uploadFirmware: () => call('api:uploadFirmware'),
|
uploadFirmware: () => call('api:uploadFirmware'),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -530,8 +530,10 @@ button.danger:hover:not(:disabled) {
|
||||||
.onu-info-row:last-child { border-bottom: none; }
|
.onu-info-row:last-child { border-bottom: none; }
|
||||||
.onu-info-row:hover { background: rgba(33, 200, 255, 0.06); }
|
.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.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-serial { font-weight: 600; font-size: 12px; display: flex; gap: 8px; align-items: center; justify-content: space-between; }
|
||||||
.onu-info-row .oir-sub { font-size: 11px; color: var(--fg-dim); display: flex; gap: 8px; align-items: center; }
|
.onu-info-row .oir-sub { font-size: 11px; color: var(--fg-dim); display: flex; gap: 8px; align-items: center; }
|
||||||
|
.onu-info-row .oir-meta { font-size: 11px; color: var(--fg-dim); overflow-wrap: anywhere; }
|
||||||
|
.onu-info-row .oir-meta .sep { opacity: 0.5; }
|
||||||
|
|
||||||
.onu-detail { overflow-y: auto; display: flex; flex-direction: column; gap: 14px; 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); }
|
.onu-detail h2 { margin: 0; font-size: 17px; color: var(--neon); text-shadow: var(--glow-neon); }
|
||||||
|
|
@ -542,12 +544,24 @@ button.danger:hover:not(:disabled) {
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
/* min-width:0 stops a long unbreakable string (system descriptions, IPv6,
|
||||||
|
alarm text) from blowing the grid track out past the card. */
|
||||||
|
.detail-card { min-width: 0; }
|
||||||
|
.kv dd { min-width: 0; overflow-wrap: anywhere; }
|
||||||
svg.spark { vertical-align: middle; opacity: 0.9; }
|
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 { display: flex; flex-direction: column; gap: 2px; padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
|
||||||
.alarm-row:last-child { border-bottom: none; }
|
.alarm-row:last-child { border-bottom: none; }
|
||||||
.alarm-row .alarm-text { flex: 1; }
|
.alarm-row .alarm-head { display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
|
||||||
.alarm-row .alarm-meta { color: var(--fg-dim); font-size: 11px; white-space: nowrap; }
|
.alarm-row .alarm-text { overflow-wrap: anywhere; }
|
||||||
|
.alarm-row .alarm-meta { color: var(--fg-dim); font-size: 11px; overflow-wrap: anywhere; }
|
||||||
.fw-upg { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border); padding-top: 12px; }
|
.fw-upg { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border); padding-top: 12px; }
|
||||||
|
.olt-link { color: var(--neon); text-decoration: none; border-bottom: 1px dotted currentColor; cursor: pointer; }
|
||||||
|
.olt-link:hover { text-shadow: var(--glow-neon); }
|
||||||
|
|
||||||
|
/* OLT detail modal */
|
||||||
|
.olt-modal { width: min(960px, 95vw); }
|
||||||
|
.olt-modal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
|
||||||
|
.olt-onu-list { max-height: 42vh; margin-top: 6px; }
|
||||||
.detail-card {
|
.detail-card {
|
||||||
background: var(--bg-elev);
|
background: var(--bg-elev);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|
|
||||||
141
renderer/app.js
141
renderer/app.js
|
|
@ -1868,6 +1868,8 @@ $('#onu-info-load').addEventListener('click', async () => {
|
||||||
renderOnuInfoList();
|
renderOnuInfoList();
|
||||||
});
|
});
|
||||||
$('#onu-info-search').addEventListener('input', renderOnuInfoList);
|
$('#onu-info-search').addEventListener('input', renderOnuInfoList);
|
||||||
|
$('#onu-info-eq').addEventListener('input', renderOnuInfoList);
|
||||||
|
$('#onu-info-pon').addEventListener('change', renderOnuInfoList);
|
||||||
$('#onu-info-status').addEventListener('change', renderOnuInfoList);
|
$('#onu-info-status').addEventListener('change', renderOnuInfoList);
|
||||||
|
|
||||||
// Jump straight to one ONU's info page (used by dashboard drill-downs).
|
// Jump straight to one ONU's info page (used by dashboard drill-downs).
|
||||||
|
|
@ -1880,14 +1882,24 @@ async function openOnuInfo(onuId) {
|
||||||
|
|
||||||
function onuInfoMatches() {
|
function onuInfoMatches() {
|
||||||
const q = $('#onu-info-search').value.trim().toLowerCase();
|
const q = $('#onu-info-search').value.trim().toLowerCase();
|
||||||
|
const eq = $('#onu-info-eq').value.trim().toLowerCase();
|
||||||
|
const pon = $('#onu-info-pon').value;
|
||||||
const status = $('#onu-info-status').value;
|
const status = $('#onu-info-status').value;
|
||||||
return state.fleet.filter((cfg) => {
|
return state.fleet.filter((cfg) => {
|
||||||
if (status === '__down__') { if (!DOWN_STATUSES.has(onuStatus(cfg))) return false; }
|
if (status === '__down__') { if (!DOWN_STATUSES.has(onuStatus(cfg))) return false; }
|
||||||
else if (status === '__unknown__') { if (onuStatus(cfg) !== null) return false; }
|
else if (status === '__unknown__') { if (onuStatus(cfg) !== null) return false; }
|
||||||
else if (status) { if (onuStatus(cfg) !== status) return false; }
|
else if (status) { if (onuStatus(cfg) !== status) return false; }
|
||||||
if (!q) return true;
|
if (pon && (cfg?.ONU?.['PON Mode'] || '') !== pon) return false;
|
||||||
|
if (q) {
|
||||||
const hay = `${cfg._id} ${cfg?.ONU?.Name || ''} ${cfg?.ONU?.Address || ''}`.toLowerCase();
|
const hay = `${cfg._id} ${cfg?.ONU?.Name || ''} ${cfg?.ONU?.Address || ''}`.toLowerCase();
|
||||||
return hay.includes(q);
|
if (!hay.includes(q)) return false;
|
||||||
|
}
|
||||||
|
if (eq) {
|
||||||
|
const versions = (cfg?.ONU?.['FW Bank Versions'] || []).join(' ');
|
||||||
|
const hay = `${equipmentId(cfg)} ${versions}`.toLowerCase();
|
||||||
|
if (!hay.includes(eq)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1903,9 +1915,16 @@ function renderOnuInfoList() {
|
||||||
const st = onuStatus(cfg);
|
const st = onuStatus(cfg);
|
||||||
const cls = st === 'Registered' ? 'status-ok' : (st && DOWN_STATUSES.has(st)) ? 'status-err' : 'status-pending';
|
const cls = st === 'Registered' ? 'status-ok' : (st && DOWN_STATUSES.has(st)) ? 'status-err' : 'status-pending';
|
||||||
const sel = cfg._id === state.onuInfoSelected ? ' selected' : '';
|
const sel = cfg._id === state.onuInfoSelected ? ' selected' : '';
|
||||||
|
const name = cfg?.ONU?.Name || cfg?.ONU?.Address || '';
|
||||||
|
const eqid = equipmentId(cfg);
|
||||||
|
const ver = activeVersion(cfg);
|
||||||
|
const seen = formatRelative(daysSince(lastSeenDate(cfg)));
|
||||||
|
const meta = [eqid, ver && ver !== '(unset)' ? ver : '', seen]
|
||||||
|
.filter(Boolean).map(escapeHtml).join(' <span class="sep">·</span> ');
|
||||||
return `<div class="onu-info-row${sel}" data-id="${escapeHtml(cfg._id)}">
|
return `<div class="onu-info-row${sel}" data-id="${escapeHtml(cfg._id)}">
|
||||||
<span class="oir-serial">${escapeHtml(cfg._id)}</span>
|
<span class="oir-serial">${escapeHtml(cfg._id)} <span class="${cls}">●</span></span>
|
||||||
<span class="oir-sub"><span class="${cls}">${escapeHtml(st || 'unknown')}</span><span>${escapeHtml(cfg?.ONU?.Name || cfg?.ONU?.Address || '')}</span></span>
|
${name ? `<span class="oir-sub">${escapeHtml(name)}</span>` : ''}
|
||||||
|
${meta ? `<span class="oir-meta">${meta}</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('') || '<div class="muted small" style="padding:10px">No ONUs match.</div>';
|
}).join('') || '<div class="muted small" style="padding:10px">No ONUs match.</div>';
|
||||||
const total = onuInfoMatches().length;
|
const total = onuInfoMatches().length;
|
||||||
|
|
@ -1999,12 +2018,15 @@ async function renderOnuDetail(onuId) {
|
||||||
${detailLinkCard(cfg)}
|
${detailLinkCard(cfg)}
|
||||||
${detailTrafficCard(d)}
|
${detailTrafficCard(d)}
|
||||||
${detailUniCard(d)}
|
${detailUniCard(d)}
|
||||||
${detailOltCard(d)}
|
${detailOltUplinkCard(d)}
|
||||||
${detailSwitchCard(d)}
|
|
||||||
${detailAlarmsCard(d)}
|
${detailAlarmsCard(d)}
|
||||||
<div class="detail-card">${renderCpeCard(d.cpe)}</div>
|
<div class="detail-card">${renderCpeCard(d.cpe)}</div>
|
||||||
${detailFirmwareCard(d)}
|
${detailFirmwareCard(d)}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
// Clickable parent OLT -> OLT detail modal.
|
||||||
|
for (const a of el.querySelectorAll('.olt-link, .olt-detail-btn')) {
|
||||||
|
a.addEventListener('click', (e) => { e.preventDefault(); showOltModal(a.dataset.olt); });
|
||||||
|
}
|
||||||
wireFirmwareUpgrade(onuId);
|
wireFirmwareUpgrade(onuId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2074,7 +2096,7 @@ function detailUniCard(d) {
|
||||||
const uni = d.uni || [];
|
const uni = d.uni || [];
|
||||||
const rows = uni.map((u) => kvRow(
|
const rows = uni.map((u) => kvRow(
|
||||||
escapeHtml(u.name),
|
escapeHtml(u.name),
|
||||||
`${u.enable ? '<span class="status-ok">enabled</span>' : '<span class="muted">disabled</span>'} · ${escapeHtml(u.speed || '?')}/${escapeHtml(u.duplex || '?')}`,
|
`${u.enable ? '<span class="status-ok">enabled</span>' : '<span class="muted">disabled</span>'} · <span class="muted">cfg</span> ${escapeHtml(u.speed || '?')}/${escapeHtml(u.duplex || '?')}`,
|
||||||
)).join('');
|
)).join('');
|
||||||
let losRows = '';
|
let losRows = '';
|
||||||
if (d.ethLos) {
|
if (d.ethLos) {
|
||||||
|
|
@ -2086,30 +2108,92 @@ function detailUniCard(d) {
|
||||||
return `<div class="detail-card"><h3>UNI ports</h3><dl class="kv">${rows || kvRow('—', 'no UNI-ETH ports')}${losRows}</dl></div>`;
|
return `<div class="detail-card"><h3>UNI ports</h3><dl class="kv">${rows || kvRow('—', 'no UNI-ETH ports')}${losRows}</dl></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detailOltCard(d) {
|
// Combined parent-OLT + upstream-switch (LLDP) card. The OLT name links to a
|
||||||
|
// full OLT detail modal (showOltModal).
|
||||||
|
function detailOltUplinkCard(d) {
|
||||||
const o = d.olt;
|
const o = d.olt;
|
||||||
if (!o) return '<div class="detail-card"><h3>Parent OLT</h3><p class="muted small">Unknown.</p></div>';
|
const s = d.sw;
|
||||||
return `<div class="detail-card"><h3>Parent OLT</h3><dl class="kv">
|
if (!o && !s) return '<div class="detail-card"><h3>Parent OLT & uplink</h3><p class="muted small">Unknown.</p></div>';
|
||||||
${kvRow('Name', escapeHtml(o.name || '—'))}
|
const oltRows = o ? `
|
||||||
|
${kvRow('OLT', `<a class="olt-link" data-olt="${escapeHtml(o.mac)}">${escapeHtml(o.name || o.mac)}</a>`)}
|
||||||
${kvRow('MAC', escapeHtml(o.mac || '—'))}
|
${kvRow('MAC', escapeHtml(o.mac || '—'))}
|
||||||
${o.model ? kvRow('Model', escapeHtml(o.model)) : ''}
|
${o.model ? kvRow('Model', escapeHtml(o.model)) : ''}
|
||||||
${kvRow('FW', escapeHtml(o.fwVersion || '—'))}
|
${kvRow('FW / PON', `${escapeHtml(o.fwVersion || '—')} · ${escapeHtml(o.ponMode || '—')}`)}
|
||||||
${kvRow('PON mode', escapeHtml(o.ponMode || '—'))}
|
${o.location ? kvRow('Location', escapeHtml(o.location)) : ''}` : '';
|
||||||
${o.location ? kvRow('Location', escapeHtml(o.location)) : ''}
|
const swRows = s ? `
|
||||||
</dl></div>`;
|
${kvRow('Uplink sw', escapeHtml(s.systemName || '—'))}
|
||||||
|
${kvRow('Sw port', escapeHtml(s.portDesc || s.portId || '—'))}
|
||||||
|
${s.ipv4 ? kvRow('Sw IPv4', escapeHtml(s.ipv4)) : ''}
|
||||||
|
${s.systemDesc ? kvRow('Sw descr', `<span class="small muted">${escapeHtml(s.systemDesc)}</span>`) : ''}` : '';
|
||||||
|
return `<div class="detail-card"><h3>Parent OLT & uplink</h3><dl class="kv">${oltRows}${swRows}</dl>
|
||||||
|
${o ? `<div class="row" style="margin-top:10px"><button class="ghost olt-detail-btn" data-olt="${escapeHtml(o.mac)}">View OLT details →</button></div>` : ''}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detailSwitchCard(d) {
|
// Full OLT detail in a modal: identity, traffic/temp/ONU-counts, alarms, and
|
||||||
const s = d.sw;
|
// (from the loaded fleet, no extra fetch) the per-ONU FEC/optical list.
|
||||||
if (!s) return '';
|
async function showOltModal(oltMac) {
|
||||||
return `<div class="detail-card"><h3>Upstream switch (LLDP)</h3><dl class="kv">
|
const overlay = document.createElement('div');
|
||||||
${kvRow('System', escapeHtml(s.systemName || '—'))}
|
overlay.className = 'modal-overlay';
|
||||||
${kvRow('Port', escapeHtml(s.portDesc || s.portId || '—'))}
|
overlay.innerHTML = `<div class="modal olt-modal"><h3>OLT ${escapeHtml(oltMac)}</h3><p class="muted small">Loading…</p><div class="modal-actions"><button class="primary modal-ok" type="button">Close</button></div></div>`;
|
||||||
${s.ipv4 ? kvRow('IPv4', escapeHtml(s.ipv4)) : ''}
|
document.body.appendChild(overlay);
|
||||||
${kvRow('Chassis', escapeHtml(s.chassisId || '—'))}
|
const close = () => { document.removeEventListener('keydown', onKey, true); overlay.remove(); };
|
||||||
${kvRow('Port ID', escapeHtml(s.portId || '—'))}
|
const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } };
|
||||||
${s.systemDesc ? kvRow('Descr', `<span class="small muted">${escapeHtml(s.systemDesc)}</span>`) : ''}
|
overlay.addEventListener('mousedown', (e) => { if (e.target === overlay) close(); });
|
||||||
</dl></div>`;
|
overlay.querySelector('.modal-ok').addEventListener('click', close);
|
||||||
|
document.addEventListener('keydown', onKey, true);
|
||||||
|
|
||||||
|
const res = await window.api.getOltDetail({ oltMac });
|
||||||
|
if (!document.body.contains(overlay)) return;
|
||||||
|
const box = overlay.querySelector('.olt-modal');
|
||||||
|
if (!res.ok) {
|
||||||
|
box.innerHTML = `<h3>OLT ${escapeHtml(oltMac)}</h3><p class="status-err">Error: ${escapeHtml(res.error?.message || 'failed')}</p><div class="modal-actions"><button class="primary modal-ok" type="button">Close</button></div>`;
|
||||||
|
box.querySelector('.modal-ok').addEventListener('click', close);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = res.data;
|
||||||
|
const tr = d.traffic;
|
||||||
|
const tempCls = d.tempC == null ? 'muted' : d.tempC >= 80 ? 'status-err' : d.tempC >= 70 ? 'status-warn' : 'status-ok';
|
||||||
|
|
||||||
|
const onus = state.fleet.filter((c) => c?._status?.oltMac === oltMac);
|
||||||
|
const onuRows = onus.slice(0, 300).map((c) => {
|
||||||
|
const cs = onuClientStats(c);
|
||||||
|
const [fc, ftxt] = fecVerdict(cs);
|
||||||
|
const st = onuStatus(c);
|
||||||
|
const stc = st === 'Registered' ? 'status-ok' : (st && DOWN_STATUSES.has(st)) ? 'status-err' : 'status-pending';
|
||||||
|
return `<div class="list-modal-item tappable" data-onu="${escapeHtml(c._id)}">
|
||||||
|
<span class="lm-primary">${escapeHtml(c._id)} <span class="${stc}">●</span></span>
|
||||||
|
<span class="lm-secondary">${escapeHtml(c?.ONU?.Name || '')} · <span class="${fc}">${escapeHtml(ftxt)}</span> · RX ${cs.rx == null ? '—' : escapeHtml(cs.rx.toFixed(1))} / TX ${cs.tx == null ? '—' : escapeHtml(cs.tx.toFixed(1))} dBm</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
<h3>OLT ${escapeHtml(d.name || oltMac)} <span class="muted">${escapeHtml(oltMac)}</span></h3>
|
||||||
|
<div class="olt-modal-grid">
|
||||||
|
<div class="detail-card"><h3>OLT</h3><dl class="kv">
|
||||||
|
${kvRow('Model', escapeHtml(d.model || '—'))}
|
||||||
|
${kvRow('FW', escapeHtml(d.fwVersion || '—'))}
|
||||||
|
${kvRow('PON mode', escapeHtml(d.ponMode || '—'))}
|
||||||
|
${d.location ? kvRow('Location', escapeHtml(d.location)) : ''}
|
||||||
|
${d.serialNumber ? kvRow('Serial', escapeHtml(d.serialNumber)) : ''}
|
||||||
|
${d.laser ? kvRow('Laser', `<span class="${d.laser.off ? 'status-err' : 'status-ok'}">${escapeHtml(d.laser.value)}</span>`) : ''}
|
||||||
|
</dl></div>
|
||||||
|
<div class="detail-card"><h3>Traffic / health</h3><dl class="kv">
|
||||||
|
${tr ? kvRow('↓ Down / ↑ Up', `<span class="status-ok">${escapeHtml(fmtBps(tr.downBps))}</span> / <span class="status-info">${escapeHtml(fmtBps(tr.upBps))}</span>`) : kvRow('Traffic', '—')}
|
||||||
|
${tr && tr.rxUtil != null ? kvRow('Util RX/TX', `${escapeHtml(String(tr.rxUtil))}% / ${escapeHtml(String(tr.txUtil))}%`) : ''}
|
||||||
|
${kvRow('Temp (ASIC)', d.tempC == null ? '—' : `<span class="${tempCls}">${escapeHtml(String(d.tempC))} °C</span>`)}
|
||||||
|
${kvRow('ONUs', `${d.onuCounts.total ?? '—'} total · ${d.onuCounts.online ?? '—'} online · ${d.onuCounts.offline ?? '—'} offline`)}
|
||||||
|
</dl></div>
|
||||||
|
${detailAlarmsCard(d)}
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-top:14px">Connected ONUs ${state.fleet.length ? `<span class="muted">(${onus.length})</span>` : ''}</h3>
|
||||||
|
${state.fleet.length
|
||||||
|
? `<div class="list-modal-items olt-onu-list">${onuRows || '<div class="modal-body muted">None on this OLT in the loaded fleet.</div>'}</div>`
|
||||||
|
: '<p class="muted small">Load the fleet (ONU tab) to list per-ONU FEC/optical here.</p>'}
|
||||||
|
<div class="modal-actions"><button class="primary modal-ok" type="button">Close</button></div>`;
|
||||||
|
box.querySelector('.modal-ok').addEventListener('click', close);
|
||||||
|
for (const el of box.querySelectorAll('.list-modal-item.tappable')) {
|
||||||
|
el.addEventListener('click', () => { close(); openOnuInfo(el.dataset.onu); });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sevPillClass(rank) {
|
function sevPillClass(rank) {
|
||||||
|
|
@ -2122,7 +2206,10 @@ function detailAlarmsCard(d) {
|
||||||
const shown = al.slice(0, 12);
|
const shown = al.slice(0, 12);
|
||||||
const rows = shown.map((a) => {
|
const rows = shown.map((a) => {
|
||||||
const dot = a.active ? '<span class="status-err">●</span>' : '<span class="muted">○</span>';
|
const dot = a.active ? '<span class="status-err">●</span>' : '<span class="muted">○</span>';
|
||||||
return `<div class="alarm-row">${dot} <span class="lease-pill ${sevPillClass(a.sevRank)}">${escapeHtml(a.sevLabel || a.severity)}</span> <span class="alarm-text">${escapeHtml(a.text || a.type)}</span><span class="alarm-meta">×${a.raisedCount} · ${escapeHtml(a.active ? `since ${a.lastRaised}` : `last ${a.lastRaised}`)}</span></div>`;
|
return `<div class="alarm-row">
|
||||||
|
<span class="alarm-head">${dot} <span class="lease-pill ${sevPillClass(a.sevRank)}">${escapeHtml(a.sevLabel || a.severity)}</span> <span class="alarm-text">${escapeHtml(a.text || a.type)}</span></span>
|
||||||
|
<span class="alarm-meta">×${a.raisedCount} · ${escapeHtml(a.active ? `since ${a.lastRaised}` : `last ${a.lastRaised}`)}</span>
|
||||||
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
const cls = d.alarmActiveCount ? 'status-err' : 'muted';
|
const cls = d.alarmActiveCount ? 'status-err' : 'muted';
|
||||||
return `<div class="detail-card"><h3>Alarms <span class="${cls}">(${d.alarmActiveCount} active / ${al.length})</span></h3>${rows}${al.length > shown.length ? `<p class="muted small">+${al.length - shown.length} more</p>` : ''}</div>`;
|
return `<div class="detail-card"><h3>Alarms <span class="${cls}">(${d.alarmActiveCount} active / ${al.length})</span></h3>${rows}${al.length > shown.length ? `<p class="muted small">+${al.length - shown.length} more</p>` : ''}</div>`;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,17 @@
|
||||||
<label>Search (name / address / serial)
|
<label>Search (name / address / serial)
|
||||||
<input id="onu-info-search" type="search" placeholder="e.g. ORKANGER or GNXS05…" />
|
<input id="onu-info-search" type="search" placeholder="e.g. ORKANGER or GNXS05…" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>Equipment / version contains
|
||||||
|
<input id="onu-info-eq" type="search" placeholder="e.g. FT-XGS2110 or EV051" />
|
||||||
|
</label>
|
||||||
|
<label>PON mode
|
||||||
|
<select id="onu-info-pon">
|
||||||
|
<option value="">Any</option>
|
||||||
|
<option value="GPON">GPON</option>
|
||||||
|
<option value="XGS-PON">XGS-PON</option>
|
||||||
|
<option value="10G-EPON">10G-EPON</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label>Registration status
|
<label>Registration status
|
||||||
<select id="onu-info-status">
|
<select id="onu-info-status">
|
||||||
<option value="">Any</option>
|
<option value="">Any</option>
|
||||||
|
|
|
||||||
|
|
@ -585,6 +585,28 @@ class McmsClient {
|
||||||
return this._listPaginatedByNext('/v3/olts/states/', { pageSize });
|
return this._listPaginatedByNext('/v3/olts/states/', { pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Single OLT-STATE doc (traffic / temperature / ONU counts / laser). */
|
||||||
|
async getOltState(oltId) {
|
||||||
|
try {
|
||||||
|
const res = await this._request('GET', `/v3/olts/states/${encodeURIComponent(oltId)}/`);
|
||||||
|
return res.body?.data ?? null;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof McmsApiError && e.status === 404) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OLT alarm-history doc (alarms + upstream-switch LLDP). */
|
||||||
|
async getOltAlarms(oltId) {
|
||||||
|
try {
|
||||||
|
const res = await this._request('GET', `/v3/olts/alarm-histories/${encodeURIComponent(oltId)}/`);
|
||||||
|
return res.body?.data ?? null;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof McmsApiError && e.status === 404) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- OLT configuration (PON flooding mode) ---------------------------
|
// --- OLT configuration (PON flooding mode) ---------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
67
src/oltdetail.js
Normal file
67
src/oltdetail.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// OLT detail summarizer for the clickable OLT info card. Pure — shared by the
|
||||||
|
// Electron main process and the web server. Combines OLT-CFG (identity) +
|
||||||
|
// OLT-STATE (traffic / temperature / ONU counts / laser) + alarm-histories
|
||||||
|
// (alarms + upstream-switch LLDP).
|
||||||
|
|
||||||
|
const { summarizeAlarms } = require('./onudetail');
|
||||||
|
|
||||||
|
function num(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeOltDetail({ oltCfg = null, oltState = null, alarmsDoc = null } = {}) {
|
||||||
|
const cOlt = oltCfg?.OLT || {};
|
||||||
|
const sOlt = oltState?.OLT || {};
|
||||||
|
const pon = oltState?.STATS?.['OLT-PON'] || {};
|
||||||
|
const optr = cOlt['FW Bank Ptr'];
|
||||||
|
|
||||||
|
const trafficSeen = pon['TX BW Ethernet Rate bps'] != null || pon['RX BW Ethernet Rate bps'] != null;
|
||||||
|
const traffic = trafficSeen ? {
|
||||||
|
downBps: num(pon['TX BW Ethernet Rate bps']) || 0, // OLT TX = downstream
|
||||||
|
upBps: num(pon['RX BW Ethernet Rate bps']) || 0, // OLT RX = upstream
|
||||||
|
txUtil: num(pon['TX BW Total Util']),
|
||||||
|
rxUtil: num(pon['RX BW Total Util']),
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const onuCounts = {
|
||||||
|
total: num(pon['Total ONUs Count']),
|
||||||
|
online: num(pon['Online ONUs Count']),
|
||||||
|
offline: num(pon['Offline ONUs Count']),
|
||||||
|
};
|
||||||
|
|
||||||
|
const laserStr = sOlt['Laser Shutdown'];
|
||||||
|
const laser = (typeof laserStr === 'string')
|
||||||
|
? { value: laserStr, off: laserStr.toLowerCase() !== 'laser on' }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const s = alarmsDoc?.Switch;
|
||||||
|
const sw = s ? {
|
||||||
|
chassisId: s['Chassis ID'] || '', portId: s['Port ID'] || '', portDesc: s['Port Description'] || '',
|
||||||
|
systemName: s['System Name'] || '', systemDesc: s['System Description'] || '',
|
||||||
|
ipv4: s['IPv4 Address'] || '', ipv6: s['IPv6 Address'] || '',
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const { alarms, alarmActiveCount } = summarizeAlarms(alarmsDoc?.Alarms);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mac: oltCfg?._id || oltState?._id || alarmsDoc?._id || '',
|
||||||
|
name: cOlt.Name || sOlt.Name || '',
|
||||||
|
model: cOlt.Model || sOlt.Model || '',
|
||||||
|
fwVersion: (Array.isArray(cOlt['FW Bank Versions']) && typeof optr === 'number') ? (cOlt['FW Bank Versions'][optr] || '') : (sOlt['FW Version'] || ''),
|
||||||
|
ponMode: cOlt['PON Mode'] || sOlt['PON Mode'] || '',
|
||||||
|
location: cOlt.Location || '',
|
||||||
|
serialNumber: sOlt['Serial Number'] || '',
|
||||||
|
onlineTime: sOlt['Online Time'] || '',
|
||||||
|
traffic,
|
||||||
|
tempC: num(oltState?.STATS?.['OLT-TEMP']?.ASIC),
|
||||||
|
onuCounts,
|
||||||
|
laser,
|
||||||
|
sw,
|
||||||
|
alarms,
|
||||||
|
alarmActiveCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { summarizeOltDetail };
|
||||||
|
|
@ -9,6 +9,36 @@ function num(v) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize an alarm-history "Alarms" array (shared by ONU and OLT detail):
|
||||||
|
// active-first, then severity rank ("N-LABEL"), then most-recently-raised.
|
||||||
|
function summarizeAlarms(rawAlarms) {
|
||||||
|
const list = (Array.isArray(rawAlarms) ? rawAlarms : []).map((a) => {
|
||||||
|
const sev = String(a.Severity || '');
|
||||||
|
const dash = sev.indexOf('-');
|
||||||
|
const rank = dash > 0 ? Number(sev.slice(0, dash)) : NaN;
|
||||||
|
return {
|
||||||
|
id: a['Alarm ID'],
|
||||||
|
text: a.Text || a['Alarm Type'] || '',
|
||||||
|
type: a['Alarm Type'] || '',
|
||||||
|
objectType: a['Object Type'] || '',
|
||||||
|
objectInstance: a['Object Instance'] || '',
|
||||||
|
source: a.Source || '',
|
||||||
|
severity: sev,
|
||||||
|
sevRank: Number.isFinite(rank) ? rank : 99,
|
||||||
|
sevLabel: dash > 0 ? sev.slice(dash + 1) : sev,
|
||||||
|
active: !!a['Active State'],
|
||||||
|
raisedCount: a['Raised Count'] || 0,
|
||||||
|
firstRaised: a['Time First Raised'] || '',
|
||||||
|
lastRaised: a['Time Last Raised'] || '',
|
||||||
|
lastCleared: a['Time Last Cleared'] || '',
|
||||||
|
};
|
||||||
|
}).sort((x, y) =>
|
||||||
|
(Number(y.active) - Number(x.active))
|
||||||
|
|| (x.sevRank - y.sevRank)
|
||||||
|
|| String(y.lastRaised).localeCompare(String(x.lastRaised)));
|
||||||
|
return { alarms: list, alarmActiveCount: list.filter((a) => a.active).length };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} a
|
* @param {object} a
|
||||||
* @param {object} [a.cfg] ONU-CFG doc (firmware banks, UNI-ETH, …)
|
* @param {object} [a.cfg] ONU-CFG doc (firmware banks, UNI-ETH, …)
|
||||||
|
|
@ -42,32 +72,7 @@ function summarizeOnuDetail({ cfg = null, alarmsDoc = null, statsSeries = [], ol
|
||||||
.map((k) => ({ name: k, enable: !!(cfg[k] && cfg[k].Enable) }));
|
.map((k) => ({ name: k, enable: !!(cfg[k] && cfg[k].Enable) }));
|
||||||
|
|
||||||
// --- Alarms ---
|
// --- Alarms ---
|
||||||
const rawAlarms = Array.isArray(alarmsDoc?.Alarms) ? alarmsDoc.Alarms : [];
|
const { alarms, alarmActiveCount } = summarizeAlarms(alarmsDoc?.Alarms);
|
||||||
const alarms = rawAlarms.map((a) => {
|
|
||||||
const sev = String(a.Severity || '');
|
|
||||||
const dash = sev.indexOf('-');
|
|
||||||
const rank = dash > 0 ? Number(sev.slice(0, dash)) : NaN;
|
|
||||||
return {
|
|
||||||
id: a['Alarm ID'],
|
|
||||||
text: a.Text || a['Alarm Type'] || '',
|
|
||||||
type: a['Alarm Type'] || '',
|
|
||||||
objectType: a['Object Type'] || '',
|
|
||||||
objectInstance: a['Object Instance'] || '',
|
|
||||||
source: a.Source || '',
|
|
||||||
severity: sev,
|
|
||||||
sevRank: Number.isFinite(rank) ? rank : 99,
|
|
||||||
sevLabel: dash > 0 ? sev.slice(dash + 1) : sev,
|
|
||||||
active: !!a['Active State'],
|
|
||||||
raisedCount: a['Raised Count'] || 0,
|
|
||||||
firstRaised: a['Time First Raised'] || '',
|
|
||||||
lastRaised: a['Time Last Raised'] || '',
|
|
||||||
lastCleared: a['Time Last Cleared'] || '',
|
|
||||||
};
|
|
||||||
}).sort((x, y) =>
|
|
||||||
(Number(y.active) - Number(x.active))
|
|
||||||
|| (x.sevRank - y.sevRank)
|
|
||||||
|| String(y.lastRaised).localeCompare(String(x.lastRaised)));
|
|
||||||
const alarmActiveCount = alarms.filter((a) => a.active).length;
|
|
||||||
|
|
||||||
// --- Ethernet UNI loss-of-signal (last disconnect) ---
|
// --- Ethernet UNI loss-of-signal (last disconnect) ---
|
||||||
const los = alarms
|
const los = alarms
|
||||||
|
|
@ -133,4 +138,4 @@ function summarizeOnuDetail({ cfg = null, alarmsDoc = null, statsSeries = [], ol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { summarizeOnuDetail };
|
module.exports = { summarizeOnuDetail, summarizeAlarms };
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@
|
||||||
getOnuConfig: (o) => postJSON('getOnuConfig', o),
|
getOnuConfig: (o) => postJSON('getOnuConfig', o),
|
||||||
getOnuCpe: (o) => postJSON('getOnuCpe', o),
|
getOnuCpe: (o) => postJSON('getOnuCpe', o),
|
||||||
getOnuDetail: (o) => postJSON('getOnuDetail', o),
|
getOnuDetail: (o) => postJSON('getOnuDetail', o),
|
||||||
|
getOltDetail: (o) => postJSON('getOltDetail', o),
|
||||||
listOnuFirmware: () => postJSON('listOnuFirmware', {}),
|
listOnuFirmware: () => postJSON('listOnuFirmware', {}),
|
||||||
uploadFirmware: async () => {
|
uploadFirmware: async () => {
|
||||||
const file = await pickFile('.bin');
|
const file = await pickFile('.bin');
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const { summarizeDashboard } = require('../src/dashboard');
|
||||||
const { summarizeCpe } = require('../src/cpe');
|
const { summarizeCpe } = require('../src/cpe');
|
||||||
const { lookupOui } = require('../src/oui');
|
const { lookupOui } = require('../src/oui');
|
||||||
const { summarizeOnuDetail } = require('../src/onudetail');
|
const { summarizeOnuDetail } = require('../src/onudetail');
|
||||||
|
const { summarizeOltDetail } = require('../src/oltdetail');
|
||||||
const { TtlCache } = require('./cache');
|
const { TtlCache } = require('./cache');
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT) || 8080;
|
const PORT = Number(process.env.PORT) || 8080;
|
||||||
|
|
@ -296,6 +297,16 @@ const handlers = {
|
||||||
return { ok: true, data: detail };
|
return { ok: true, data: detail };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getOltDetail(s, body) {
|
||||||
|
const c = s.client;
|
||||||
|
const [oltCfg, oltState, alarmsDoc] = await Promise.all([
|
||||||
|
c.getOltConfig(body.oltMac).catch(() => null),
|
||||||
|
c.getOltState(body.oltMac).catch(() => null),
|
||||||
|
c.getOltAlarms(body.oltMac).catch(() => null),
|
||||||
|
]);
|
||||||
|
return { ok: true, data: summarizeOltDetail({ oltCfg, oltState, alarmsDoc }) };
|
||||||
|
},
|
||||||
|
|
||||||
async listOnuFirmware(s, body) {
|
async listOnuFirmware(s, body) {
|
||||||
const fresh = !!(body && body.fresh);
|
const fresh = !!(body && body.fresh);
|
||||||
return { ok: true, data: await cachedBulk(s, 'firmware', () => s.client.listOnuFirmware(), fresh) };
|
return { ok: true, data: await cachedBulk(s, 'firmware', () => s.client.listOnuFirmware(), fresh) };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue