initial commit
This commit is contained in:
commit
0eb9b55717
11 changed files with 3969 additions and 0 deletions
619
main.js
Normal file
619
main.js
Normal file
|
|
@ -0,0 +1,619 @@
|
|||
// Electron main process for pon-fleet-upgrader.
|
||||
//
|
||||
// Responsibilities:
|
||||
// - Own the MCMS session (cookies stay out of the renderer).
|
||||
// - Expose IPC handlers that the renderer calls via window.api.*.
|
||||
// - Centralize TLS cert handling for self-signed internal MCMS installs.
|
||||
|
||||
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { McmsClient, McmsApiError, extractOnuHealth } = require('./src/mcms-api');
|
||||
const { planUpgrade, activeBankPtr, inactiveBank } = require('./src/bank-strategy');
|
||||
|
||||
/** @type {McmsClient | null} */
|
||||
let client = null;
|
||||
let mainWindow = null;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
height: 900,
|
||||
title: 'PON Fleet Upgrader',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
// ---- IPC handlers ----------------------------------------------------
|
||||
|
||||
function requireClient() {
|
||||
if (!client) throw new Error('Not logged in — call api.login first.');
|
||||
return client;
|
||||
}
|
||||
|
||||
// Format MCMS API errors for the renderer without leaking stack traces.
|
||||
function toWireError(err) {
|
||||
if (err instanceof McmsApiError) {
|
||||
return { message: err.message, status: err.status, body: err.body };
|
||||
}
|
||||
return { message: err?.message || String(err) };
|
||||
}
|
||||
|
||||
ipcMain.handle('api:login', async (_ev, { baseUrl, username, password, acceptSelfSigned }) => {
|
||||
try {
|
||||
client = new McmsClient({
|
||||
baseUrl,
|
||||
rejectUnauthorized: !acceptSelfSigned,
|
||||
// Verbose logging goes to the Electron terminal (npm start output).
|
||||
// Turn off by setting PON_FLEET_VERBOSE=0 in the env.
|
||||
verbose: process.env.PON_FLEET_VERBOSE !== '0',
|
||||
});
|
||||
const result = await client.login(username, password);
|
||||
return { ok: true, data: result };
|
||||
} catch (err) {
|
||||
client = null;
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api:logout', async () => {
|
||||
if (client) await client.logout();
|
||||
client = null;
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('api:listOnuConfigs', async (_ev, opts) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
// No projection and no query — MCMS's schema validator is picky about
|
||||
// projection paths that don't exist on every document version, and its
|
||||
// URL-param encoding of Mongo-style queries has been unreliable across
|
||||
// builds (spaces in field names, JSON escaping). We load the full fleet
|
||||
// (bounded by `limit`) and filter entirely client-side.
|
||||
const data = await c.listOnuConfigs({
|
||||
limit: opts?.limit,
|
||||
skip: opts?.skip,
|
||||
});
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Fast fleet load: configs + onu-states + olt-states. Tries the bulk
|
||||
* state endpoint first; if that fails, the renderer can follow up with
|
||||
* api:fetchStates to fetch states per-ONU. OLT-STATE carries the
|
||||
* registration bucket (Registered / Deregistered / Dying Gasp / etc.)
|
||||
* per ONU — see 323-1961-306 Procedure 2 — so we load it here and tag
|
||||
* each merged ONU with its `_status.status`.
|
||||
*/
|
||||
ipcMain.handle('api:listFleet', async (_ev) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const configs = await c.listAllOnuConfigs();
|
||||
let states = [];
|
||||
let statesError = null;
|
||||
try {
|
||||
states = await c.listAllOnuStates();
|
||||
} catch (e) {
|
||||
statesError = toWireError(e);
|
||||
}
|
||||
// OLT-STATE fetch is best-effort. If it fails the UI falls back to
|
||||
// "unknown" status and the down-duration filter is simply unavailable.
|
||||
let oltStates = [];
|
||||
let oltStatesError = null;
|
||||
try {
|
||||
oltStates = await c.listAllOltStates();
|
||||
} catch (e) {
|
||||
oltStatesError = toWireError(e);
|
||||
}
|
||||
// Build onuId -> { status, oltMac } from OLT-STATE["ONU States"].
|
||||
// An ONU should appear in exactly one bucket on exactly one OLT; in
|
||||
// the rare case of duplicates we prefer a non-"Registered" bucket
|
||||
// since that's the interesting signal (the newer OLT wins otherwise).
|
||||
const statusByOnu = new Map();
|
||||
for (const olt of oltStates) {
|
||||
const buckets = olt?.['ONU States'] || {};
|
||||
const oltMac = olt?._id;
|
||||
for (const [bucket, ids] of Object.entries(buckets)) {
|
||||
if (!Array.isArray(ids)) continue;
|
||||
for (const id of ids) {
|
||||
const existing = statusByOnu.get(id);
|
||||
if (!existing || existing.status === 'Registered') {
|
||||
statusByOnu.set(id, { status: bucket, oltMac });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const stateById = new Map(states.map((s) => [s._id, s]));
|
||||
const merged = configs.map((cfg) => ({
|
||||
...cfg,
|
||||
_state: stateById.get(cfg._id) || null,
|
||||
_status: statusByOnu.get(cfg._id) || null,
|
||||
}));
|
||||
return {
|
||||
ok: true,
|
||||
data: merged,
|
||||
meta: {
|
||||
configCount: configs.length,
|
||||
stateCount: states.length,
|
||||
oltStateCount: oltStates.length,
|
||||
statesError, // null on success, error object on failure
|
||||
oltStatesError, // same, for OLT-STATE fetch
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Phase 2: fetch ONU-STATE per-ONU with bounded concurrency. The web
|
||||
* UI on this MCMS build hits /v3/onus/states/<id>/ individually rather
|
||||
* than the bulk list endpoint, which matches what we do here. Progress
|
||||
* events stream back on 'states:progress' so the UI can fill in the
|
||||
* Equipment ID column as results arrive.
|
||||
*/
|
||||
ipcMain.handle('api:fetchStates', async (event, { onuIds, concurrency = 20 }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const queue = [...onuIds];
|
||||
const results = [];
|
||||
let done = 0;
|
||||
const total = onuIds.length;
|
||||
|
||||
async function worker() {
|
||||
for (;;) {
|
||||
const id = queue.shift();
|
||||
if (id === undefined) return;
|
||||
let state = null;
|
||||
try {
|
||||
state = await c.getOnuState(id);
|
||||
} catch (e) {
|
||||
// Per-ONU failure is non-fatal; the UI will just show a blank
|
||||
// Equipment ID for this row.
|
||||
state = null;
|
||||
}
|
||||
results.push({ onuId: id, state });
|
||||
done += 1;
|
||||
event.sender.send('states:progress', { onuId: id, state, done, total });
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, total) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return { ok: true, data: results };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api:getOnuConfig', async (_ev, { onuId }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const data = await c.getOnuConfig(onuId);
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api:listOnuFirmware', async () => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const data = await c.listOnuFirmware();
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api:uploadFirmware', async () => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const picked = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select ONU firmware (.bin)',
|
||||
filters: [{ name: 'Firmware', extensions: ['bin'] }],
|
||||
properties: ['openFile'],
|
||||
});
|
||||
if (picked.canceled || picked.filePaths.length === 0) {
|
||||
return { ok: false, error: { message: 'cancelled' } };
|
||||
}
|
||||
const filePath = picked.filePaths[0];
|
||||
const buf = fs.readFileSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
const base64 = buf.toString('base64');
|
||||
// Infer version from filename (e.g. Interos-Everest-5.12.0-R-EV05120R.bin
|
||||
// -> version "EV05120R"). Users can fix this up later if wrong.
|
||||
const versionMatch = filename.match(/([A-Z]{2}\d{5}[A-Z]?)/);
|
||||
const metadata = {
|
||||
'Compatible Manufacturer': 'TIBITCOM',
|
||||
'Compatible Model': ['MicroPlug ONU'],
|
||||
Version: versionMatch ? versionMatch[1] : '',
|
||||
};
|
||||
const body = await c.uploadOnuFirmware(filename, base64, metadata);
|
||||
return { ok: true, data: { filename, metadata, body } };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Dry-run planner: given a set of ONUs and a target firmware, return the
|
||||
* planned write-slot + before/after for each ONU *without* mutating anything.
|
||||
*/
|
||||
ipcMain.handle('api:planUpgrade', async (_ev, { onuIds, targetFile, targetVersion }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const plans = [];
|
||||
for (const onuId of onuIds) {
|
||||
const cfg = await c.getOnuConfig(onuId);
|
||||
if (!cfg) {
|
||||
plans.push({ onuId, error: 'ONU config not found' });
|
||||
continue;
|
||||
}
|
||||
const plan = planUpgrade(cfg, { targetFile, targetVersion });
|
||||
plans.push({
|
||||
onuId,
|
||||
name: cfg?.ONU?.Name || '',
|
||||
address: cfg?.ONU?.Address || '',
|
||||
ponMode: cfg?.ONU?.['PON Mode'] || '',
|
||||
...plan.summary,
|
||||
writeSlot: plan.writeSlot,
|
||||
});
|
||||
}
|
||||
return { ok: true, data: plans };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Per-ONU execution (Procedure 7) — iterates, fetches the doc, mutates the
|
||||
* FW bank fields, PUTs the full document back. This is the "one-at-a-time,
|
||||
* see errors live" path.
|
||||
*/
|
||||
ipcMain.handle('api:executePerOnu', async (event, { onuIds, targetFile, targetVersion }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const results = [];
|
||||
for (let i = 0; i < onuIds.length; i++) {
|
||||
const onuId = onuIds[i];
|
||||
// Fire per-ONU progress back to the renderer.
|
||||
event.sender.send('upgrade:progress', {
|
||||
index: i,
|
||||
total: onuIds.length,
|
||||
onuId,
|
||||
phase: 'fetching',
|
||||
});
|
||||
try {
|
||||
const cfg = await c.getOnuConfig(onuId);
|
||||
if (!cfg) throw new Error('ONU config not found');
|
||||
|
||||
const plan = planUpgrade(cfg, { targetFile, targetVersion });
|
||||
// Apply the mutation in place on the fetched document so we PUT the
|
||||
// full doc back (MCMS uses PUT = replace, not partial).
|
||||
const mutated = { ...cfg, ONU: { ...cfg.ONU, ...plan.fwFields } };
|
||||
|
||||
event.sender.send('upgrade:progress', {
|
||||
index: i,
|
||||
total: onuIds.length,
|
||||
onuId,
|
||||
phase: 'writing',
|
||||
writeSlot: plan.writeSlot,
|
||||
});
|
||||
await c.putOnuConfig(onuId, mutated);
|
||||
results.push({ onuId, ok: true, writeSlot: plan.writeSlot });
|
||||
} catch (err) {
|
||||
results.push({ onuId, ok: false, error: toWireError(err) });
|
||||
}
|
||||
}
|
||||
return { ok: true, data: results };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Bulk execution (Procedure 8) — single AUTO-TASK-CFG that MCMS schedules
|
||||
* across all selected serials. Recommended path for large fleets.
|
||||
*/
|
||||
ipcMain.handle('api:executeBulkTask', async (_ev, opts) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const body = await c.createFirmwareTask(opts);
|
||||
return { ok: true, data: body };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api:getTaskConfig', async (_ev, { taskId }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const data = await c.getTaskConfig(taskId);
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('api:getUpgradeStatus', async (_ev, { onuId }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const data = await c.getOnuUpgradeStatus(onuId);
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Pre-flight link-health check. For each selected ONU, re-fetch the
|
||||
* ONU-STATE document (so the snapshot is fresh at decision time, not
|
||||
* stale from the original fleet load) and reduce it to a single
|
||||
* health flag (ok / pre / post / buggy / nodata / error) plus the
|
||||
* raw FEC counters and optical levels.
|
||||
*
|
||||
* Source fields (observed on MCMS 6.2 + Interos Everest 5.x ONUs):
|
||||
* STATE.STATS["ONU-PON"]["RX Pre-FEC BER" | "RX Post-FEC BER"
|
||||
* | "RX Optical Level" | "TX Optical Level"]
|
||||
* STATE.STATS["OLT-PON"]["RX Pre-FEC BER" | "RX Post-FEC BER"]
|
||||
*
|
||||
* Streams progress on 'fec:progress' as each ONU resolves so the
|
||||
* preview table can paint its pill the moment its result lands.
|
||||
*/
|
||||
ipcMain.handle('api:fetchFecHealth', async (event, { onuIds, concurrency = 8 }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const queue = [...onuIds];
|
||||
const results = [];
|
||||
let done = 0;
|
||||
const total = onuIds.length;
|
||||
|
||||
async function worker() {
|
||||
for (;;) {
|
||||
const id = queue.shift();
|
||||
if (id === undefined) return;
|
||||
let entry = { onuId: id, flag: 'nodata' };
|
||||
try {
|
||||
const stateDoc = await c.getOnuState(id);
|
||||
const health = extractOnuHealth(stateDoc);
|
||||
entry = {
|
||||
onuId: id,
|
||||
flag: health.flag,
|
||||
detail: health.detail || '',
|
||||
counters: health.counters || [],
|
||||
optical: health.optical || {},
|
||||
sampleTime: stateDoc?.Time || '',
|
||||
};
|
||||
} catch (e) {
|
||||
entry = { onuId: id, flag: 'error', error: toWireError(e) };
|
||||
}
|
||||
results.push(entry);
|
||||
done += 1;
|
||||
event.sender.send('fec:progress', { ...entry, done, total });
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, total) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return { ok: true, data: results };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal CSV writer used by both the plan-save (on Execute) and the
|
||||
* verify-save (after a comparison run). Always quotes every cell —
|
||||
* MCMS values are full of CSV-hostile characters (commas in addresses,
|
||||
* embedded quotes in operator-entered names, occasional newlines).
|
||||
* Always prepends a UTF-8 BOM so Excel auto-detects the encoding and
|
||||
* Norwegian Æ/Ø/Å survive on Windows.
|
||||
*/
|
||||
function writeCsvFile({ prefix, filenameHint, headers, rows }) {
|
||||
const downloadsDir = app.getPath('downloads');
|
||||
const ts = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const stamp =
|
||||
`${ts.getFullYear()}${pad(ts.getMonth() + 1)}${pad(ts.getDate())}` +
|
||||
`-${pad(ts.getHours())}${pad(ts.getMinutes())}${pad(ts.getSeconds())}`;
|
||||
const safeHint = (filenameHint || 'csv')
|
||||
.toString()
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
||||
.slice(0, 60);
|
||||
const filename = `${prefix}-${safeHint}-${stamp}.csv`;
|
||||
const fullPath = path.join(downloadsDir, filename);
|
||||
|
||||
const csvCell = (v) => {
|
||||
if (v === null || v === undefined) return '';
|
||||
const s = typeof v === 'string' ? v : String(v);
|
||||
return '"' + s.replace(/"/g, '""') + '"';
|
||||
};
|
||||
const csvRow = (cells) => cells.map(csvCell).join(',');
|
||||
const lines = [csvRow(headers)];
|
||||
for (const row of rows) lines.push(csvRow(headers.map((h) => row[h])));
|
||||
const body = '' + lines.join('\r\n') + '\r\n';
|
||||
fs.writeFileSync(fullPath, body, { encoding: 'utf8' });
|
||||
return { path: fullPath, filename, rowCount: rows.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an upgrade plan to a timestamped CSV in the user's Downloads
|
||||
* folder. Called automatically by the renderer the moment the operator
|
||||
* clicks Execute, so there's always a paper trail of what got
|
||||
* scheduled — even if the MCMS write later fails.
|
||||
*/
|
||||
ipcMain.handle('api:savePlanCsv', async (_ev, { rows, filenameHint, headers }) => {
|
||||
try {
|
||||
if (!Array.isArray(rows) || !Array.isArray(headers)) {
|
||||
throw new Error('rows and headers are required arrays');
|
||||
}
|
||||
const data = writeCsvFile({
|
||||
prefix: 'pon-upgrade', filenameHint, headers, rows,
|
||||
});
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Generic CSV save. Used by the verify flow with prefix='pon-verify'.
|
||||
* Same writeCsvFile semantics as the plan save.
|
||||
*/
|
||||
ipcMain.handle('api:saveCsvFile', async (_ev, { rows, filenameHint, headers, prefix = 'pon-csv' }) => {
|
||||
try {
|
||||
if (!Array.isArray(rows) || !Array.isArray(headers)) {
|
||||
throw new Error('rows and headers are required arrays');
|
||||
}
|
||||
const safePrefix = String(prefix).replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 40) || 'pon-csv';
|
||||
const data = writeCsvFile({ prefix: safePrefix, filenameHint, headers, rows });
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Open an existing CSV from disk (default: Downloads). Returns the raw
|
||||
* text so the renderer can parse it with its own CSV parser. We don't
|
||||
* parse here because the renderer already needs to know the column
|
||||
* layout to build comparisons.
|
||||
*/
|
||||
ipcMain.handle('api:openCsvFile', async () => {
|
||||
try {
|
||||
const picked = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Open upgrade plan CSV',
|
||||
defaultPath: app.getPath('downloads'),
|
||||
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
||||
properties: ['openFile'],
|
||||
});
|
||||
if (picked.canceled || !picked.filePaths.length) {
|
||||
return { ok: false, error: { message: 'cancelled' } };
|
||||
}
|
||||
const filePath = picked.filePaths[0];
|
||||
const text = fs.readFileSync(filePath, 'utf8');
|
||||
return { ok: true, data: { path: filePath, text } };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Per-ONU snapshot fetch for the verify flow: pulls BOTH the ONU-STATE
|
||||
* (for current FEC counters and optical levels) AND the ONU-CFG (so we
|
||||
* can verify the active firmware version actually flipped to the
|
||||
* target). Streams progress on 'verify:progress' so the table can
|
||||
* update incrementally on big lists.
|
||||
*/
|
||||
ipcMain.handle('api:fetchOnuSnapshot', async (event, { onuIds, concurrency = 8 }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const queue = [...onuIds];
|
||||
const results = [];
|
||||
let done = 0;
|
||||
const total = onuIds.length;
|
||||
|
||||
async function worker() {
|
||||
for (;;) {
|
||||
const id = queue.shift();
|
||||
if (id === undefined) return;
|
||||
let entry = { onuId: id };
|
||||
try {
|
||||
const [stateDoc, cfgDoc] = await Promise.all([
|
||||
c.getOnuState(id).catch(() => null),
|
||||
c.getOnuConfig(id).catch(() => null),
|
||||
]);
|
||||
const health = extractOnuHealth(stateDoc);
|
||||
entry = {
|
||||
onuId: id,
|
||||
flag: health.flag,
|
||||
counters: health.counters || [],
|
||||
optical: health.optical || {},
|
||||
sampleTime: stateDoc?.Time || '',
|
||||
cfg: cfgDoc,
|
||||
};
|
||||
} catch (e) {
|
||||
entry = { onuId: id, flag: 'error', error: toWireError(e) };
|
||||
}
|
||||
results.push(entry);
|
||||
done += 1;
|
||||
event.sender.send('verify:progress', { onuId: id, done, total, flag: entry.flag });
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, total) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return { ok: true, data: results };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Bulk delete ONUs from MCMS. For each ONU we delete the CFG document
|
||||
* (Procedure 43 in the PON Manager User Guide), then best-effort delete
|
||||
* the STATE document so stale Equipment-ID/registration entries don't
|
||||
* hang around. STATE deletion failures are ignored — the OLT will
|
||||
* rewrite it on next registration anyway, but CFG deletion is the
|
||||
* authoritative action.
|
||||
*
|
||||
* Progress streams back on 'delete:progress' so the renderer can tick
|
||||
* through a large selection without locking up. Concurrency is kept low
|
||||
* (default 5) because DELETE is more disruptive than GET and we don't
|
||||
* want to stampede the MCMS backend.
|
||||
*/
|
||||
ipcMain.handle('api:deleteOnus', async (event, { onuIds, concurrency = 5 }) => {
|
||||
try {
|
||||
const c = requireClient();
|
||||
const queue = [...onuIds];
|
||||
const results = [];
|
||||
let done = 0;
|
||||
const total = onuIds.length;
|
||||
|
||||
async function worker() {
|
||||
for (;;) {
|
||||
const id = queue.shift();
|
||||
if (id === undefined) return;
|
||||
let ok = false;
|
||||
let error = null;
|
||||
try {
|
||||
await c.deleteOnuConfig(id);
|
||||
ok = true;
|
||||
// State deletion is best-effort and does not affect `ok`.
|
||||
try { await c.deleteOnuState(id); } catch (_) { /* ignore */ }
|
||||
} catch (e) {
|
||||
error = toWireError(e);
|
||||
}
|
||||
results.push({ onuId: id, ok, error });
|
||||
done += 1;
|
||||
event.sender.send('delete:progress', { onuId: id, ok, error, done, total });
|
||||
}
|
||||
}
|
||||
|
||||
const workers = Array.from({ length: Math.min(concurrency, total) }, () => worker());
|
||||
await Promise.all(workers);
|
||||
return { ok: true, data: results };
|
||||
} catch (err) {
|
||||
return { ok: false, error: toWireError(err) };
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue