'use strict' // Poller pause control — test-phase friendly on/off switches for the two // network pollers that run inside targo-hub: // // 1. device — GenieACS device-cache poll (~6000 ONTs every 5 min) // 2. olt — SNMP sweep of the 4 OLTs (every 5 min) // // State is persisted to data/poller-control.json so pauses survive container // restarts. Each poller's tick function first calls isPaused('x') and bails // early (logging a one-liner so you can see pauses in the container log). // // HTTP surface: // GET /admin/pollers → { device: {paused, ...}, olt: {...} } // POST /admin/pollers { device, olt } → updates the flags (partial ok) // // Auth alignment: same tier as /olt/config + /olt/poll (unauthenticated at // hub layer — the ops SPA that calls these is already behind Authentik SSO // at the edge). Don't expose this without that front door in place. const fs = require('fs') const path = require('path') const cfg = require('./config') const { log, json, parseBody } = require('./helpers') const STATE_PATH = path.join(__dirname, '..', 'data', 'poller-control.json') const DEFAULT_STATE = { device: { paused: false, lastChange: null, reason: '' }, olt: { paused: false, lastChange: null, reason: '' }, } let state = { ...DEFAULT_STATE } function load () { try { if (fs.existsSync(STATE_PATH)) { const raw = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) state = { device: { ...DEFAULT_STATE.device, ...(raw.device || {}) }, olt: { ...DEFAULT_STATE.olt, ...(raw.olt || {}) }, } if (state.device.paused || state.olt.paused) { log(`[poller-control] loaded — device.paused=${state.device.paused} olt.paused=${state.olt.paused}`) } } } catch (e) { log(`[poller-control] load failed: ${e.message} — using defaults`) } } function persist () { try { const dir = path.dirname(STATE_PATH) if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2)) } catch (e) { log(`[poller-control] persist failed: ${e.message}`) } } function isPaused (kind) { return !!(state[kind] && state[kind].paused) } function setPaused (kind, paused, reason = '') { if (!state[kind]) return false const prev = state[kind].paused state[kind].paused = !!paused state[kind].lastChange = new Date().toISOString() state[kind].reason = reason || '' if (prev !== state[kind].paused) { log(`[poller-control] ${kind} poller ${paused ? 'PAUSED' : 'RESUMED'}${reason ? ` — ${reason}` : ''}`) } persist() return true } function getState () { return JSON.parse(JSON.stringify(state)) } // ── HTTP handler ──────────────────────────────────────────────────────────── async function handle (req, res, method, reqPath) { if (reqPath === '/admin/pollers' && method === 'GET') { return json(res, 200, getState()) } if (reqPath === '/admin/pollers' && method === 'POST') { const body = await parseBody(req) const updates = {} for (const kind of ['device', 'olt']) { if (body && typeof body[kind] === 'object' && body[kind] !== null) { if (typeof body[kind].paused === 'boolean') { setPaused(kind, body[kind].paused, body[kind].reason || '') updates[kind] = true } } else if (body && typeof body[kind] === 'boolean') { // shorthand: { device: true, olt: false } setPaused(kind, body[kind]) updates[kind] = true } } return json(res, 200, { ok: true, updates, state: getState() }) } return json(res, 404, { error: 'not found' }) } // initial load on require load() module.exports = { isPaused, setPaused, getState, handle, load }