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 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) 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 = { const ACTIONS = {
activate: { verb: 'POST', label: 'Activer / provisionner lONU', needs: ['sn', 'olt', 'profileid'], techs: [3] }, 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] }, 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] }, 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] }, 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] }, 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] }, 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] }, // tech-3 : utiliser le reboot TR-069 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. // 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 return equip
} }
// Appel n8n dostuff avec un VERBE arbitraire + body JSON (le module https, comme dostuffStatus). // Requête n8n : VERBE HTTP arbitraire + body JSON via le module https (fetch refuse un body en GET). Renvoie le
function dostuff (verb, payload, timeoutMs = 60000) { // JSON brut. Base commune de dostuff() et wifiClients() (F fait pareil).
function n8nRequest (verb, targetUrl, body, timeoutMs) {
return new Promise((resolve) => { return new Promise((resolve) => {
let https; try { https = require('https') } catch (e) { return resolve({ ok: false, error: 'https indisponible' }) } let https; try { https = require('https') } catch (e) { return resolve({ ok: false, error: 'https indisponible' }) }
const body = JSON.stringify(payload) const payload = JSON.stringify(body)
const req = https.request(N8N_DOSTUFF, { method: verb, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: timeoutMs }, (res) => { 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 d = ''; res.on('data', c => { d += c }); res.on('end', () => {
let parsed; try { parsed = JSON.parse(d) } catch (e) { parsed = d } let parsed; try { parsed = JSON.parse(d) } catch (e) { parsed = d }
const r = Array.isArray(parsed) ? parsed[0] : parsed resolve({ ok: res.statusCode < 400, status: res.statusCode, data: parsed })
const recognized = !(r && r.ErrorCode)
resolve({ ok: recognized && res.statusCode < 400, status: res.statusCode, recognized, error: (r && r.ErrorCode) || null, data: r })
}) })
}) })
req.on('error', e => resolve({ ok: false, error: e.message })) 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.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. // Construit le payload n8n EXACT pour une action, à partir du contexte résolu + overrides.
function buildPayload (action, equip, opts = {}) { function buildPayload (action, equip, opts = {}) {
const spec = ACTIONS[action] 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) : [] const missing = method === 'n8n' ? spec.needs.filter(k => !payload[k] && payload[k] !== 0) : []
if (missing.length) warnings.push('Champs manquants : ' + missing.join(', ')) if (missing.length) warnings.push('Champs manquants : ' + missing.join(', '))
const canRun = supportsTech && !!payload.olt && (method === 'n8n' ? (tech === 3 && !missing.length) : snmpReady) const canRun = supportsTech && !!payload.olt && (method === 'n8n' ? (tech === 3 && !missing.length) : snmpReady)
const effects = [] const effects = spec.effect ? [spec.effect] : []
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.')
return { return {
ok: canRun, ok: canRun,
action, label: spec.label, verb: spec.verb, tech, method, 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. // G1 reste : clients WiFi connectés d'un ONU (n8n get_wifi_info) — LECTURE.
function wifiClients ({ sn } = {}) { async function wifiClients ({ sn } = {}) {
return new Promise((resolve) => { if (!sn) return { ok: false, error: 'sn requis' }
if (!sn) return resolve({ ok: false, error: 'sn requis' }) const res = await n8nRequest('GET', 'https://n8napi.targo.ca/webhook/get_wifi_info', { sn: String(sn) }, 20000)
let https; try { https = require('https') } catch (e) { return resolve({ ok: false, error: 'https indisponible' }) } if (res.error) return { ok: false, error: res.error }
const body = JSON.stringify({ sn: String(sn) }) const j = res.data
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) => { return { ok: !!j, clients: Array.isArray(j) ? j : (j && j.clients) || [], raw: j }
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()
})
} }
module.exports = { plan, run, wifiClients, ACTIONS } module.exports = { plan, run, wifiClients, ACTIONS }