Add bulk OLT PON flooding-mode tool; restore README; docs
Adds a second bulk workflow alongside the firmware upgrader: changing PON flooding mode (private<->auto) on OLT NNI services across many OLTs. Flooding mode is encoded by the presence of the "PON FLOOD ID" key on each OLT-CFG["NNI Networks"] entry (present = private, absent = auto). private->auto deletes the key; auto->private sets it to 0. The rest of the OLT-CFG is preserved (PUT-as-replace, same as ONU-CFG). - src/mcms-api.js: listAllOltConfigs / getOltConfig / putOltConfig, plus pure helpers floodModeOfNni, summarizeOltNniNetworks, planFloodChange (planFloodChange clones and never mutates its input) - main.js: api:listOlts + api:executeFloodChange IPC handlers (concurrency 3, GET->plan->PUT, streams olt-flood:progress) - preload.js: expose both channels + onFloodProgress - renderer: OLT inspector view (#view-olts), two-step APPLY confirm, auto-saved pon-olt-flood-*.csv result report - Restore README.md (lost to a filesystem error) and document the new feature in README.md and CLAUDE.md (new section 14) Verified: npm run check passes; pure helpers unit-tested for the no-mutation, idempotency, and rollback invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0eb9b55717
commit
80fd7e8d37
7 changed files with 612 additions and 7 deletions
181
renderer/app.js
181
renderer/app.js
|
|
@ -11,6 +11,9 @@ const state = {
|
|||
fecHealth: new Map(), // onuId -> { flag, detail, sampleTime, error? }
|
||||
campaignMode: 'task', // 'task' | 'peronu'
|
||||
verify: null, // { sourcePath, beforeRows, snapshotById, comparisons }
|
||||
olts: [], // result of listOlts
|
||||
oltFiltered: [], // current visible OLTs
|
||||
oltSelected: new Set(),
|
||||
};
|
||||
|
||||
// -------- Helpers --------
|
||||
|
|
@ -1280,3 +1283,181 @@ function escapeHtml(s) {
|
|||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// -------- OLT inspector: bulk PON flooding mode change --------
|
||||
|
||||
$('#btn-to-olts').addEventListener('click', () => {
|
||||
showView('#view-olts');
|
||||
$('#olt-status').textContent = '';
|
||||
$('#olt-exec-status').textContent = '';
|
||||
});
|
||||
$('#btn-olts-back').addEventListener('click', () => showView('#view-fleet'));
|
||||
|
||||
$('#btn-load-olts').addEventListener('click', loadOlts);
|
||||
$('#olt-tag-pattern').addEventListener('input', applyOltFilters);
|
||||
$('#olt-mode-filter').addEventListener('change', applyOltFilters);
|
||||
$('#olt-name-filter').addEventListener('input', applyOltFilters);
|
||||
$('#btn-olt-select-all').addEventListener('click', () => {
|
||||
for (const r of state.oltFiltered) state.oltSelected.add(r._id);
|
||||
renderOlts();
|
||||
});
|
||||
$('#btn-olt-select-none').addEventListener('click', () => {
|
||||
state.oltSelected.clear();
|
||||
renderOlts();
|
||||
});
|
||||
$('#olt-th-check').addEventListener('change', (e) => {
|
||||
if (e.target.checked) for (const r of state.oltFiltered) state.oltSelected.add(r._id);
|
||||
else for (const r of state.oltFiltered) state.oltSelected.delete(r._id);
|
||||
renderOlts();
|
||||
});
|
||||
|
||||
async function loadOlts() {
|
||||
const tagPattern = $('#olt-tag-pattern').value.trim();
|
||||
$('#olt-status').textContent = 'Loading OLT configs…';
|
||||
const res = await window.api.listOlts({ tagPattern });
|
||||
if (!res.ok) {
|
||||
$('#olt-status').textContent = `Error: ${res.error?.message || 'unknown'}`;
|
||||
return;
|
||||
}
|
||||
state.olts = res.data || [];
|
||||
applyOltFilters();
|
||||
$('#olt-total').textContent = state.olts.length;
|
||||
const totalMatching = state.olts.reduce((acc, r) => acc + r.matchingNni.length, 0);
|
||||
const totalPriv = state.olts.reduce((acc, r) => acc + r.counts.private, 0);
|
||||
$('#olt-status').textContent =
|
||||
`${state.olts.length} OLTs loaded. ${totalMatching} service(s) match TAG pattern; ${totalPriv} currently in private mode.`;
|
||||
}
|
||||
|
||||
function applyOltFilters() {
|
||||
const mode = $('#olt-mode-filter').value;
|
||||
const nameQ = $('#olt-name-filter').value.trim().toLowerCase();
|
||||
state.oltFiltered = state.olts.filter((r) => {
|
||||
if (nameQ && !(r.name || '').toLowerCase().includes(nameQ) && !(r._id || '').toLowerCase().includes(nameQ)) return false;
|
||||
if (mode === 'any') return r.matchingNni.length > 0;
|
||||
return r.matchingNni.some((n) => n.mode === mode);
|
||||
});
|
||||
renderOlts();
|
||||
}
|
||||
|
||||
function renderOlts() {
|
||||
const tbody = $('#olt-tbody');
|
||||
tbody.innerHTML = '';
|
||||
const mode = $('#olt-mode-filter').value;
|
||||
let totalSvcThatWillChange = 0;
|
||||
for (const r of state.oltFiltered) {
|
||||
const tr = document.createElement('tr');
|
||||
const selected = state.oltSelected.has(r._id);
|
||||
if (selected) tr.classList.add('selected');
|
||||
const willChangeMode = mode === 'auto' ? 'auto' : 'private';
|
||||
const matchingHtml = r.matchingNni.map((n) => {
|
||||
const modeCls = n.mode === 'private' ? 'status-warn' : n.mode === 'auto' ? 'status-ok' : 'muted';
|
||||
const willChange = selected && n.mode === willChangeMode;
|
||||
if (willChange) totalSvcThatWillChange += 1;
|
||||
const willFlag = willChange ? ' <span class="status-info">→ will change</span>' : '';
|
||||
const fid = n.ponFloodId !== null && n.ponFloodId !== undefined ? ` (PON FLOOD ID=${escapeHtml(String(n.ponFloodId))})` : '';
|
||||
return `<div><code>${escapeHtml(n.tagMatch)}</code> <span class="${modeCls}">${escapeHtml(n.mode)}</span>${escapeHtml(fid)}${willFlag}</div>`;
|
||||
}).join('');
|
||||
tr.innerHTML = `
|
||||
<td><input type="checkbox" data-id="${escapeHtml(r._id)}" ${selected ? 'checked' : ''}/></td>
|
||||
<td>${escapeHtml(r._id)}</td>
|
||||
<td>${escapeHtml(r.name)}</td>
|
||||
<td>${escapeHtml(r.ponMode)}</td>
|
||||
<td>${escapeHtml(r.activeFwVersion)}</td>
|
||||
<td class="fec-cell">${matchingHtml || '<span class="muted">no matching service</span>'}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
$('#olt-count').textContent = state.oltFiltered.length;
|
||||
$('#olt-sel-count').textContent = state.oltSelected.size;
|
||||
$('#olt-svc-count').textContent = totalSvcThatWillChange;
|
||||
$('#btn-olt-execute').disabled = totalSvcThatWillChange === 0;
|
||||
|
||||
for (const cb of tbody.querySelectorAll('input[type="checkbox"]')) {
|
||||
cb.addEventListener('change', () => {
|
||||
const id = cb.getAttribute('data-id');
|
||||
if (cb.checked) state.oltSelected.add(id); else state.oltSelected.delete(id);
|
||||
renderOlts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$('#btn-olt-execute').addEventListener('click', async () => {
|
||||
const ids = Array.from(state.oltSelected);
|
||||
if (!ids.length) return;
|
||||
const action = $('#olt-action').value;
|
||||
const fromMode = action === 'auto2private' ? 'auto' : 'private';
|
||||
const targetMode = action === 'auto2private' ? 'private' : 'auto';
|
||||
const tagPattern = $('#olt-tag-pattern').value.trim();
|
||||
const summary =
|
||||
`Apply "${fromMode} → ${targetMode}" to NNI services matching "${tagPattern}" ` +
|
||||
`on ${ids.length} OLT(s).\n\nThis GETs each OLT-CFG, mutates the matching ` +
|
||||
`NNI Networks entries, and PUTs the full document back. The operation is ` +
|
||||
`irreversible from this app (no undo).\n\nClick OK to continue to the final confirmation.`;
|
||||
if (!confirm(summary)) return;
|
||||
const typed = prompt(`Type APPLY to confirm bulk mode change on ${ids.length} OLT(s):`);
|
||||
if (typed !== 'APPLY') {
|
||||
$('#olt-exec-status').textContent = 'Cancelled.';
|
||||
return;
|
||||
}
|
||||
|
||||
$('#btn-olt-execute').disabled = true;
|
||||
$('#olt-exec-status').textContent = `Applying change to 0/${ids.length}…`;
|
||||
const unbind = window.api.onFloodProgress(({ done, total, oltId, error, changes }) => {
|
||||
const tail = error ? ` (last error on ${oltId}: ${error.message})`
|
||||
: changes ? ` (${oltId}: ${changes.length} svc updated)` : '';
|
||||
$('#olt-exec-status').textContent = `Applying ${done}/${total}${tail}`;
|
||||
});
|
||||
const res = await window.api.executeFloodChange({
|
||||
oltIds: ids, tagPattern, fromMode, targetMode,
|
||||
});
|
||||
unbind();
|
||||
if (!res.ok) {
|
||||
$('#olt-exec-status').textContent = `Bulk apply failed: ${res.error?.message}`;
|
||||
$('#btn-olt-execute').disabled = false;
|
||||
return;
|
||||
}
|
||||
const okCount = res.data.filter((r) => r.ok && !r.noop).length;
|
||||
const noopCount = res.data.filter((r) => r.noop).length;
|
||||
const failed = res.data.filter((r) => !r.ok);
|
||||
let msg = `Done. ${okCount} OLT(s) updated, ${noopCount} no-op, ${failed.length} failed.`;
|
||||
if (failed.length) {
|
||||
msg += ' Failures: ' + failed.slice(0, 3).map((f) => `${f.oltId} (${f.error?.message || '?'})`).join(', ');
|
||||
if (failed.length > 3) msg += ` and ${failed.length - 3} more`;
|
||||
}
|
||||
$('#olt-exec-status').textContent = msg;
|
||||
|
||||
// Auto-save a result CSV alongside the upgrade/verify ones.
|
||||
const headers = [
|
||||
'OLT MAC', 'OLT name', 'Action', 'TAG pattern',
|
||||
'Result', 'Services changed', 'Services skipped',
|
||||
'Change detail', 'Skip detail', 'Error',
|
||||
];
|
||||
const rows = res.data.map((r) => {
|
||||
const olt = state.olts.find((o) => o._id === r.oltId);
|
||||
const changeDetail = (r.changes || []).map((c) => `${c.tagMatch}: ${c.fromMode}→${c.toMode}`).join('; ');
|
||||
const skipDetail = (r.skipped || []).map((s) => `${s.tagMatch}: ${s.reason}`).join('; ');
|
||||
return {
|
||||
'OLT MAC': r.oltId,
|
||||
'OLT name': olt?.name || '',
|
||||
'Action': `${fromMode}→${targetMode}`,
|
||||
'TAG pattern': tagPattern,
|
||||
'Result': r.ok ? (r.noop ? 'noop' : 'updated') : 'failed',
|
||||
'Services changed': (r.changes || []).length,
|
||||
'Services skipped': (r.skipped || []).length,
|
||||
'Change detail': changeDetail,
|
||||
'Skip detail': skipDetail,
|
||||
'Error': r.error ? r.error.message : '',
|
||||
};
|
||||
});
|
||||
const saveRes = await window.api.saveCsvFile({
|
||||
rows, headers,
|
||||
filenameHint: `${fromMode}-to-${targetMode}-${tagPattern.replace(/[^A-Za-z0-9]+/g, '_')}-${ids.length}olts`,
|
||||
prefix: 'pon-olt-flood',
|
||||
});
|
||||
if (saveRes.ok) {
|
||||
$('#olt-exec-status').textContent += ` Report → ${saveRes.data.path}`;
|
||||
}
|
||||
|
||||
// Refresh the table so the user sees the new state.
|
||||
await loadOlts();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue