refactor(olt-ops): dédup plomberie n8n + effets pilotés par le spec ACTIONS

- extrait n8nRequest(verb,url,body,timeout) partagé par dostuff() et wifiClients() (fin de la double implémentation https-GET-with-body)
- effets déplacés dans ACTIONS[action].effect → supprime l'échelle de if dans plan()
Comportement préservé (vérifié live : plan tech-3 suspend identique, gardes run inchangées). Déployé prod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-19 07:38:48 -04:00
parent 23dde4ecf0
commit 218bef8c5a

View File

@ -39,15 +39,16 @@ const RC = {
const rcOltId = (s, p, o) => Number(s) * 10000000 + Number(p) * 100000 + Number(o)
const rcOnuId = (s, p, o) => (Number(s) + 32) * 8388608 + Number(p) * 65536 + Number(o)
// action → verbe n8n (tech-3), champs requis, techs supportées. tech-3 = n8n ; tech-2 = SNMP natif (G3).
// action → verbe n8n (tech-3), champs requis, techs supportées, effet (affiché dans le plan).
// tech-3 = n8n ; tech-2 = SNMP natif (G3).
const ACTIONS = {
activate: { verb: 'POST', label: 'Activer / provisionner lONU', needs: ['sn', 'olt', 'profileid'], techs: [3] },
replace: { verb: 'PUT', label: 'Remplacer lONU (échange de série)', needs: ['old_sn', 'new_sn', 'olt'], techs: [3] },
speed: { verb: 'PATCH', label: 'Changer le forfait / la vitesse', needs: ['sn', 'olt', 'profileid'], extra: { trigger: 'speedchange' }, techs: [3, 2] },
suspend: { verb: 'PATCH', label: 'Suspendre linternet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'suspend' }, techs: [3, 2] },
unsuspend: { verb: 'PATCH', label: 'Rétablir linternet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'unsuspend' }, techs: [3, 2] },
remove: { verb: 'DELETE', label: 'Retirer / désactiver lONU', needs: ['sn', 'olt'], techs: [3] },
reboot: { verb: null, label: 'Redémarrer lONU', needs: ['sn', 'olt'], techs: [2] }, // tech-3 : utiliser le reboot TR-069
activate: { verb: 'POST', label: 'Activer / provisionner lONU', needs: ['sn', 'olt', 'profileid'], techs: [3], effect: 'Provisionne lONU sur lOLT + met Service Equipment « Actif ».' },
replace: { verb: 'PUT', label: 'Remplacer lONU (échange de série)', needs: ['old_sn', 'new_sn', 'olt'], techs: [3], effect: 'Ré-authentifie la nouvelle série sur lOLT + met à jour le numéro de série de Service Equipment.' },
speed: { verb: 'PATCH', label: 'Changer le forfait / la vitesse', needs: ['sn', 'olt', 'profileid'], extra: { trigger: 'speedchange' }, techs: [3, 2], effect: 'Change le profil de ligne (lONU redémarre).' },
suspend: { verb: 'PATCH', label: 'Suspendre linternet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'suspend' }, techs: [3, 2], effect: 'Bascule le VLAN internet → coupe laccès (facturation inchangée).' },
unsuspend: { verb: 'PATCH', label: 'Rétablir linternet (VLAN)', needs: ['sn', 'olt'], extra: { profileid: '', trigger: 'unsuspend' }, techs: [3, 2], effect: 'Rétablit le VLAN internet.' },
remove: { verb: 'DELETE', label: 'Retirer / désactiver lONU', needs: ['sn', 'olt'], techs: [3], effect: 'Retire lONU de lOLT + délie Service Equipment.' },
reboot: { verb: null, label: 'Redémarrer lONU', needs: ['sn', 'olt'], techs: [2], effect: 'Redémarre lONU.' }, // tech-3 : utiliser le reboot TR-069
}
// SNMP SET Raisecom (tech-2) : applique l'action + SAVE, via net-snmp. Renvoie {ok,error}. LECTURE : voir olt-snmp.
@ -119,25 +120,33 @@ async function resolveEquip (serial) {
return equip
}
// Appel n8n dostuff avec un VERBE arbitraire + body JSON (le module https, comme dostuffStatus).
function dostuff (verb, payload, timeoutMs = 60000) {
// Requête n8n : VERBE HTTP arbitraire + body JSON via le module https (fetch refuse un body en GET). Renvoie le
// JSON brut. Base commune de dostuff() et wifiClients() (F fait pareil).
function n8nRequest (verb, targetUrl, body, timeoutMs) {
return new Promise((resolve) => {
let https; try { https = require('https') } catch (e) { return resolve({ ok: false, error: 'https indisponible' }) }
const body = JSON.stringify(payload)
const req = https.request(N8N_DOSTUFF, { method: verb, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: timeoutMs }, (res) => {
const payload = JSON.stringify(body)
const req = https.request(targetUrl, { method: verb, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, timeout: timeoutMs }, (res) => {
let d = ''; res.on('data', c => { d += c }); res.on('end', () => {
let parsed; try { parsed = JSON.parse(d) } catch (e) { parsed = d }
const r = Array.isArray(parsed) ? parsed[0] : parsed
const recognized = !(r && r.ErrorCode)
resolve({ ok: recognized && res.statusCode < 400, status: res.statusCode, recognized, error: (r && r.ErrorCode) || null, data: r })
resolve({ ok: res.statusCode < 400, status: res.statusCode, data: parsed })
})
})
req.on('error', e => resolve({ ok: false, error: e.message }))
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'délai n8n dépassé (~' + Math.round(timeoutMs / 1000) + ' s)' }) })
req.write(body); req.end()
req.write(payload); req.end()
})
}
// dostuff : verbe = action ; « OLT non reconnue » (ErrorCode) = tech ≠ 3 → non reconnu.
async function dostuff (verb, payload, timeoutMs = 60000) {
const res = await n8nRequest(verb, N8N_DOSTUFF, payload, timeoutMs)
if (res.error) return { ok: false, error: res.error }
const r = Array.isArray(res.data) ? res.data[0] : res.data
const recognized = !(r && r.ErrorCode)
return { ok: recognized && res.ok, status: res.status, recognized, error: (r && r.ErrorCode) || null, data: r }
}
// Construit le payload n8n EXACT pour une action, à partir du contexte résolu + overrides.
function buildPayload (action, equip, opts = {}) {
const spec = ACTIONS[action]
@ -179,13 +188,7 @@ async function plan ({ serial, action, ...opts } = {}) {
const missing = method === 'n8n' ? spec.needs.filter(k => !payload[k] && payload[k] !== 0) : []
if (missing.length) warnings.push('Champs manquants : ' + missing.join(', '))
const canRun = supportsTech && !!payload.olt && (method === 'n8n' ? (tech === 3 && !missing.length) : snmpReady)
const effects = []
if (action === 'activate') effects.push('Provisionne lONU sur lOLT + met Service Equipment « Actif ».')
if (action === 'replace') effects.push('Ré-authentifie la nouvelle série sur lOLT + met à jour le numéro de série de Service Equipment.')
if (action === 'speed') effects.push('Change le profil de ligne (lONU redémarre).')
if (action === 'suspend') effects.push('Bascule le VLAN internet → coupe laccès (facturation inchangée).')
if (action === 'unsuspend') effects.push('Rétablit le VLAN internet.')
if (action === 'remove') effects.push('Retire lONU de lOLT + délie Service Equipment.')
const effects = spec.effect ? [spec.effect] : []
return {
ok: canRun,
action, label: spec.label, verb: spec.verb, tech, method,
@ -233,18 +236,12 @@ async function run ({ serial, action, confirm, idempotencyKey, actor, ...opts }
}
// G1 reste : clients WiFi connectés d'un ONU (n8n get_wifi_info) — LECTURE.
function wifiClients ({ sn } = {}) {
return new Promise((resolve) => {
if (!sn) return resolve({ ok: false, error: 'sn requis' })
let https; try { https = require('https') } catch (e) { return resolve({ ok: false, error: 'https indisponible' }) }
const body = JSON.stringify({ sn: String(sn) })
const req = https.request('https://n8napi.targo.ca/webhook/get_wifi_info', { method: 'GET', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 20000 }, (res) => {
let d = ''; res.on('data', c => { d += c }); res.on('end', () => { let j; try { j = JSON.parse(d) } catch (e) { j = null } resolve({ ok: !!j, clients: Array.isArray(j) ? j : (j && j.clients) || [], raw: j }) })
})
req.on('error', e => resolve({ ok: false, error: e.message }))
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'délai dépassé' }) })
req.write(body); req.end()
})
async function wifiClients ({ sn } = {}) {
if (!sn) return { ok: false, error: 'sn requis' }
const res = await n8nRequest('GET', 'https://n8napi.targo.ca/webhook/get_wifi_info', { sn: String(sn) }, 20000)
if (res.error) return { ok: false, error: res.error }
const j = res.data
return { ok: !!j, clients: Array.isArray(j) ? j : (j && j.clients) || [], raw: j }
}
module.exports = { plan, run, wifiClients, ACTIONS }