From 3f416fe2ec4d9131c29c12f9be4b8913ba36ad4c Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 20 Jul 2026 18:54:01 -0400 Subject: [PATCH] feat(ops): auto-sync CPE MAC from RADIUS into OPS (no manual step) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAC updates now flow into OPS automatically. On any signal read (wireless panel OR the assistant's check_signal), the hub's airosSignal resolves the Service Equipment record and, if the RADIUS-authoritative MAC (radacct.callingstationid, auto-updated on WPA2 re-auth) differs from the stored mac_address, it updates Service Equipment server-side — idempotent, fill/correct, no click. UI shows a green "MAC synchronisée automatiquement depuis RADIUS (était …)" note and reflects it in the Identity field; the manual button remains only as a fallback if the auto-update can't run. Manual serial/MAC edit (InlineField) still available. Verified: corrupted EQP-0000100001 mac to 00:00:00:00:00:00 → one airosSignal read self-healed it to RADIUS 24:5A:4C:30:CE:AA (logged). SPA deployed; leak-check 0. Co-Authored-By: Claude Opus 4.8 --- .../detail-sections/EquipmentDetail.vue | 9 ++++-- services/targo-hub/lib/ticket-collab.js | 32 ++++++++++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue b/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue index a36a2ed..2f64065 100644 --- a/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue +++ b/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue @@ -130,8 +130,11 @@
{{ airos.registered === false ? 'Signal sans-fil en attente : webhook n8n « get_signal_airos » à configurer.' : airos.error }}
- -
+ +
+ MAC synchronisée automatiquement depuis RADIUS : {{ airos.radiusMac }} (était {{ airos.macWas }}) +
+
MAC RADIUS {{ airos.radiusMac }} ≠ enregistrée {{ doc.mac_address || '—' }}
@@ -642,6 +645,8 @@ async function fetchAiros () { try { const r = await fetch(`${HUB_URL}/collab/airos-signal?serial=${encodeURIComponent(props.doc.serial_number || '')}&ip=${encodeURIComponent(props.doc.ip_address || '')}`) airos.value = await r.json() + // Le hub a auto-synchronisé la MAC depuis RADIUS → refléter localement (l'InlineField Identité suit). + if (airos.value && airos.value.macSynced && airos.value.radiusMac) props.doc.mac_address = airos.value.radiusMac } catch (e) { airos.value = { ok: false, error: e.message } } finally { airosLoading.value = false } } // MAC RADIUS (WPA2) = source autoritaire, s'auto-MàJ au remplacement d'équipement. Si elle diffère de la MAC diff --git a/services/targo-hub/lib/ticket-collab.js b/services/targo-hub/lib/ticket-collab.js index 5de698d..1843564 100644 --- a/services/targo-hub/lib/ticket-collab.js +++ b/services/targo-hub/lib/ticket-collab.js @@ -842,29 +842,39 @@ function httpsGetJson (targetUrl, headers, timeoutMs) { }) } +const macNorm = (m) => String(m || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase() async function airosSignal ({ sn, ip, port, user, mac } = {}) { - // Résout ip/mac depuis Service Equipment (source unique du parc) si non fournis. - if ((!ip || !mac) && sn) { - try { - const rows = await erp.list('Service Equipment', { filters: [['serial_number', '=', String(sn)]], fields: ['ip_address', 'mac_address'], limit: 1 }) - const e = rows && rows[0] - if (e) { if (!ip) ip = e.ip_address || ''; if (!mac) mac = e.mac_address || '' } - } catch (e) { /* best-effort */ } - } + // Résout le Service Equipment (nom + IP + MAC enregistrée) — sert à l'AUTO-SYNC de la MAC depuis RADIUS. + let seName = null, seMac = '' + try { + let rows = [] + if (sn) rows = await erp.list('Service Equipment', { filters: [['serial_number', '=', String(sn)]], fields: ['name', 'ip_address', 'mac_address'], limit: 1 }) + if ((!rows || !rows.length) && ip) rows = await erp.list('Service Equipment', { filters: [['ip_address', '=', String(ip)]], fields: ['name', 'ip_address', 'mac_address'], limit: 1 }) + const e = rows && rows[0] + if (e) { seName = e.name; seMac = e.mac_address || ''; if (!ip) ip = e.ip_address || ''; if (!mac) mac = e.mac_address || '' } + } catch (e) { /* best-effort */ } if (!sn && !ip && !mac) return { ok: false, error: 'serial, ip ou mac requis' } // PRIMAIRE : pont F ops_airos.php (même hôte + token que ops_reassign.php). const token = opsBridgeToken() const base = (process.env.OPS_LEGACY_URL || 'https://facturation.targo.ca/ops_reassign.php').replace(/ops_reassign\.php.*$/, 'ops_airos.php') + let out = null if (token && base) { const q = new URLSearchParams() if (sn) q.set('serial', sn); if (ip) q.set('ip', ip); if (mac) q.set('mac', mac); if (port) q.set('port', String(port)) const r = await httpsGetJson(base + '?' + q.toString(), { 'X-Ops-Token': token }, 25000) - if (r && r.ok === true) return normalizeAiros(r) - if (r && r.ok === false && r.error && !r._err) return { ok: false, error: r.error } // réponse F propre (device introuvable / CPE injoignable) + if (r && r.ok === true) out = normalizeAiros(r) + else if (r && r.ok === false && r.error && !r._err) return { ok: false, error: r.error } // réponse F propre (device introuvable / CPE injoignable) // transport KO (r._err) → tenter n8n en repli } // REPLI : webhook n8n get_signal_airos (post-migration F ; 404 = pas encore créé). - return await airosViaN8n({ sn, ip, port, user }) + if (!out) out = await airosViaN8n({ sn, ip, port, user }) + // AUTO-SYNC MAC : RADIUS (WPA2) = autoritaire, s'auto-MàJ au remplacement. Si la MAC courante diffère de + // l'enregistrée → on met à jour Service Equipment AUTOMATIQUEMENT (côté serveur, fill/correct, idempotent). + if (out && out.ok && out.radiusMac && seName && macNorm(out.radiusMac) && macNorm(out.radiusMac) !== macNorm(seMac)) { + try { await erp.update('Service Equipment', seName, { mac_address: out.radiusMac }); out.macSynced = true; out.macWas = seMac || null; log('[airos] MAC auto-sync ' + seName + ' ' + (seMac || '∅') + ' → ' + out.radiusMac + ' (RADIUS)') } + catch (e) { out.macSyncError = e.message } + } + return out } function airosViaN8n ({ sn, ip, port, user } = {}) {