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 } = {}) {