web: shared MCMS cache + systemd/clone-to-run deploy + NPM docs
Caching (reduce MCMS API load with multiple operators): - web/cache.js: in-memory TTL cache with single-flight, keyed by MCMS host. Concurrent identical bulk reads collapse into ONE upstream fetch; everyone on the same MCMS shares the snapshot. - Wired into the bulk reads only (ONU/OLT config+state lists, controllers, firmware). Per-ONU read-modify-write and the FEC pre-flight stay live. - Writes (delete / per-ONU + bulk upgrade / flood change / firmware upload) invalidate the affected datasets for that host, so changes show on the next load instead of waiting out the TTL. - PFW_CACHE_TTL_SECONDS (default 60, 0 disables). Authenticated _cacheStats / _cacheClear endpoints for ops. Deploy (Debian 13 LXC, clone-to-run) in web/deploy/: - pon-fleet-web.service (runs as unprivileged ponfw, hardened, repo read-only, no disk writes), pon-fleet-web.env.example, update.sh (git pull + npm install --omit=dev + restart; uses install not ci since package-lock.json is gitignored). - README: full LXC setup, Nginx Proxy Manager proxy-host + access-list notes. Streams already send X-Accel-Buffering: no so NPM/nginx don't buffer the NDJSON progress. Verified: node --check; cache unit-tested (single-flight, TTL hit/expiry, fresh, invalidate, key isolation, disable — 9/9); server boots and reports the cache TTL; _cacheStats gated to logged-in sessions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
96b0264179
commit
6ee1d37a79
7 changed files with 309 additions and 35 deletions
|
|
@ -28,11 +28,14 @@ const {
|
|||
} = require('../src/mcms-api');
|
||||
const { planUpgrade } = require('../src/bank-strategy');
|
||||
const { summarizeDashboard } = require('../src/dashboard');
|
||||
const { TtlCache } = require('./cache');
|
||||
|
||||
const PORT = Number(process.env.PORT) || 8080;
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
const IDLE_MS = (Number(process.env.PFW_IDLE_MINUTES) || 30) * 60 * 1000;
|
||||
const VERBOSE = process.env.PFW_VERBOSE === '1';
|
||||
const CACHE_TTL_MS = (process.env.PFW_CACHE_TTL_SECONDS !== undefined
|
||||
? Number(process.env.PFW_CACHE_TTL_SECONDS) : 60) * 1000;
|
||||
const ROOT = __dirname;
|
||||
const RENDERER = path.join(ROOT, '..', 'renderer');
|
||||
const PUBLIC = path.join(ROOT, 'public');
|
||||
|
|
@ -40,6 +43,21 @@ const PUBLIC = path.join(ROOT, 'public');
|
|||
const SMALL_BODY = 8 * 1024 * 1024; // 8 MB for normal JSON requests
|
||||
const UPLOAD_BODY = 256 * 1024 * 1024; // 256 MB for base64 firmware uploads
|
||||
|
||||
// ---- Shared upstream cache ------------------------------------------
|
||||
// Collapses the heavy MCMS bulk reads across all sessions on the same MCMS
|
||||
// host. Per-ONU read-modify-write fetches and the FEC pre-flight stay live
|
||||
// (uncached). Writes invalidate the affected datasets.
|
||||
const cache = new TtlCache(CACHE_TTL_MS);
|
||||
setInterval(() => cache.sweep(Math.max(CACHE_TTL_MS * 5, 10 * 60 * 1000)), 5 * 60 * 1000).unref();
|
||||
|
||||
const ckey = (session, name) => `${session.baseUrl}|${name}`;
|
||||
function cachedBulk(session, name, producer, fresh) {
|
||||
return cache.get(ckey(session, name), producer, { fresh });
|
||||
}
|
||||
function invalidate(session, names) {
|
||||
for (const n of names) cache.invalidate(ckey(session, n));
|
||||
}
|
||||
|
||||
// ---- Sessions --------------------------------------------------------
|
||||
// sid -> { client, baseUrl, user, lastSeen, sid }
|
||||
const sessions = new Map();
|
||||
|
|
@ -210,13 +228,14 @@ const handlers = {
|
|||
return { ok: true, data };
|
||||
},
|
||||
|
||||
async listFleet(s) {
|
||||
async listFleet(s, body) {
|
||||
const c = s.client;
|
||||
const configs = await c.listAllOnuConfigs();
|
||||
const fresh = !!(body && body.fresh);
|
||||
const configs = await cachedBulk(s, 'onuConfigs', () => c.listAllOnuConfigs(), fresh);
|
||||
let states = [], statesError = null;
|
||||
try { states = await c.listAllOnuStates(); } catch (e) { statesError = toWireError(e); }
|
||||
try { states = await cachedBulk(s, 'onuStates', () => c.listAllOnuStates(), fresh); } catch (e) { statesError = toWireError(e); }
|
||||
let oltStates = [], oltStatesError = null;
|
||||
try { oltStates = await c.listAllOltStates(); } catch (e) { oltStatesError = toWireError(e); }
|
||||
try { oltStates = await cachedBulk(s, 'oltStates', () => c.listAllOltStates(), fresh); } catch (e) { oltStatesError = toWireError(e); }
|
||||
|
||||
const statusByOnu = new Map();
|
||||
for (const olt of oltStates) {
|
||||
|
|
@ -249,8 +268,9 @@ const handlers = {
|
|||
return { ok: true, data: await s.client.getOnuConfig(body.onuId) };
|
||||
},
|
||||
|
||||
async listOnuFirmware(s) {
|
||||
return { ok: true, data: await s.client.listOnuFirmware() };
|
||||
async listOnuFirmware(s, body) {
|
||||
const fresh = !!(body && body.fresh);
|
||||
return { ok: true, data: await cachedBulk(s, 'firmware', () => s.client.listOnuFirmware(), fresh) };
|
||||
},
|
||||
|
||||
// Browser sent the .bin as base64 (no server-side file dialog).
|
||||
|
|
@ -264,6 +284,7 @@ const handlers = {
|
|||
Version: versionMatch ? versionMatch[1] : '',
|
||||
};
|
||||
const bodyResp = await s.client.uploadOnuFirmware(filename, base64, metadata);
|
||||
invalidate(s, ['firmware']);
|
||||
return { ok: true, data: { filename, metadata, body: bodyResp } };
|
||||
},
|
||||
|
||||
|
|
@ -283,9 +304,15 @@ const handlers = {
|
|||
},
|
||||
|
||||
async executeBulkTask(s, body) {
|
||||
return { ok: true, data: await s.client.createFirmwareTask(body) };
|
||||
const data = await s.client.createFirmwareTask(body);
|
||||
invalidate(s, ['onuConfigs', 'onuStates']);
|
||||
return { ok: true, data };
|
||||
},
|
||||
|
||||
// Ops/debug: cache visibility + manual clear for the caller's MCMS host.
|
||||
async _cacheStats() { return { ok: true, data: cache.stats() }; },
|
||||
async _cacheClear(s) { return { ok: true, data: { cleared: cache.invalidate(`${s.baseUrl}|`) } }; },
|
||||
|
||||
async getTaskConfig(s, body) {
|
||||
return { ok: true, data: await s.client.getTaskConfig(body.taskId) };
|
||||
},
|
||||
|
|
@ -297,17 +324,21 @@ const handlers = {
|
|||
async dashboardStats(s, body) {
|
||||
const c = s.client;
|
||||
const extraStats = !!(body && body.extraStats);
|
||||
const olts = await c.listAllOltStates();
|
||||
const onuStates = extraStats ? await c.listAllOnuStates() : null;
|
||||
const fresh = !!(body && body.fresh);
|
||||
const olts = await cachedBulk(s, 'oltStates', () => c.listAllOltStates(), fresh);
|
||||
const onuStates = extraStats
|
||||
? await cachedBulk(s, 'onuStates', () => c.listAllOnuStates(), fresh)
|
||||
: null;
|
||||
let controllerCount = null;
|
||||
try { controllerCount = (await c.listAllControllerConfigs()).length; } catch (_) { /* best-effort */ }
|
||||
try { controllerCount = (await cachedBulk(s, 'controllerConfigs', () => c.listAllControllerConfigs(), fresh)).length; } catch (_) { /* best-effort */ }
|
||||
return { ok: true, data: summarizeDashboard({ olts, onuStates, controllerCount }) };
|
||||
},
|
||||
|
||||
async listOlts(s, body) {
|
||||
const c = s.client;
|
||||
const tagPattern = (body && body.tagPattern) || '';
|
||||
const cfgs = await c.listAllOltConfigs();
|
||||
const fresh = !!(body && body.fresh);
|
||||
const cfgs = await cachedBulk(s, 'oltConfigs', () => c.listAllOltConfigs(), fresh);
|
||||
const rows = cfgs.map((cfg) => {
|
||||
const summary = summarizeOltNniNetworks(cfg, tagPattern);
|
||||
const counts = { private: 0, auto: 0, unknown: 0 };
|
||||
|
|
@ -355,6 +386,7 @@ const streamHandlers = {
|
|||
results.push({ onuId, ok: false, error: toWireError(err) });
|
||||
}
|
||||
}
|
||||
invalidate(s, ['onuConfigs', 'onuStates']);
|
||||
return results;
|
||||
},
|
||||
|
||||
|
|
@ -396,7 +428,7 @@ const streamHandlers = {
|
|||
|
||||
async deleteOnus(s, body, write) {
|
||||
const { onuIds, concurrency = 5 } = body;
|
||||
return runPool(onuIds, concurrency, async (id) => {
|
||||
const results = await runPool(onuIds, concurrency, async (id) => {
|
||||
let ok = false, error = null;
|
||||
try {
|
||||
await s.client.deleteOnuConfig(id);
|
||||
|
|
@ -405,6 +437,8 @@ const streamHandlers = {
|
|||
} catch (e) { error = toWireError(e); }
|
||||
return { onuId: id, ok, error };
|
||||
}, (entry, done, total) => write({ onuId: entry.onuId, ok: entry.ok, error: entry.error, done, total }));
|
||||
invalidate(s, ['onuConfigs', 'onuStates', 'oltStates']);
|
||||
return results;
|
||||
},
|
||||
|
||||
async executeFloodChange(s, body, write) {
|
||||
|
|
@ -412,7 +446,7 @@ const streamHandlers = {
|
|||
oltIds, tagPattern, fromMode = 'private', targetMode = 'auto',
|
||||
ponFloodIdValue = 0, concurrency = 3,
|
||||
} = body;
|
||||
return runPool(oltIds, concurrency, async (id) => {
|
||||
const results = await runPool(oltIds, concurrency, async (id) => {
|
||||
try {
|
||||
const cfg = await s.client.getOltConfig(id);
|
||||
if (!cfg) throw new Error('OLT not found');
|
||||
|
|
@ -424,6 +458,8 @@ const streamHandlers = {
|
|||
return { oltId: id, ok: false, error: toWireError(e) };
|
||||
}
|
||||
}, (entry, done, total) => write({ ...entry, done, total }));
|
||||
invalidate(s, ['oltConfigs']);
|
||||
return results;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -503,5 +539,6 @@ const server = http.createServer(async (req, res) => {
|
|||
server.listen(PORT, HOST, () => {
|
||||
console.log(`[pon-fleet-web] listening on http://${HOST}:${PORT}`);
|
||||
console.log(`[pon-fleet-web] idle session timeout: ${IDLE_MS / 60000} min · verbose MCMS: ${VERBOSE}`);
|
||||
console.log('[pon-fleet-web] put a TLS-terminating reverse proxy (Caddy/nginx) in front for remote use.');
|
||||
console.log(`[pon-fleet-web] upstream cache: ${cache.enabled() ? (CACHE_TTL_MS / 1000) + 's TTL (shared per MCMS host)' : 'disabled'}`);
|
||||
console.log('[pon-fleet-web] put a TLS-terminating reverse proxy (Caddy/nginx/NPM) in front for remote use.');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue