diff --git a/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue b/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue index 691e5ff..a36a2ed 100644 --- a/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue +++ b/apps/ops/src/components/shared/detail-sections/EquipmentDetail.vue @@ -130,6 +130,32 @@
{{ airos.registered === false ? 'Signal sans-fil en attente : webhook n8n « get_signal_airos » à configurer.' : airos.error }}
+ +
+ MAC RADIUS {{ airos.radiusMac }} ≠ enregistrée {{ doc.mac_address || '—' }} + +
+ +
+
+ Débit Ethernet client (eth0) + {{ ethLive ? 'Arrêter la mesure' : 'Débit en direct — eth0 du CPE (façon F)' }} +
+ +
@@ -585,7 +611,7 @@ import OnuActionsPanel from './modules/OnuActionsPanel.vue' import { usePermissions } from 'src/composables/usePermissions' import { useDeviceStatus } from 'src/composables/useDeviceStatus' import { useModemDiagnostic } from 'src/composables/useModemDiagnostic' -import { deleteDoc } from 'src/api/erp' +import { deleteDoc, updateDoc } from 'src/api/erp' import { HUB_URL } from 'src/config/hub' const props = defineProps({ doc: { type: Object, required: true }, docName: String }) @@ -618,6 +644,46 @@ async function fetchAiros () { airos.value = await r.json() } 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 +// enregistrée → on le signale + bouton 1-clic pour l'aligner (updateDoc). La saisie manuelle reste dispo (InlineField). +const normMacUi = (m) => String(m || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase() +const macMismatch = computed(() => { const rm = airos.value && airos.value.radiusMac; return !!(rm && normMacUi(rm) && normMacUi(rm) !== normMacUi(props.doc.mac_address)) }) +const macSyncing = ref(false) +async function syncMacFromRadius () { + const rm = airos.value && airos.value.radiusMac; if (!rm) return + macSyncing.value = true + try { await updateDoc('Service Equipment', props.docName, { mac_address: rm }); props.doc.mac_address = rm; $q.notify({ type: 'positive', message: 'MAC mise à jour depuis RADIUS : ' + rm }) } + catch (e) { $q.notify({ type: 'negative', message: 'Échec MAJ MAC : ' + (e.message || e) }) } finally { macSyncing.value = false } +} + +// Débit Ethernet client EN DIRECT (eth0 du CPE) : on sonde /collab/airos-signal et on calcule (Δoctets/Δs)×8. +// eth0 rx = upload (trafic DU client), tx = download (trafic VERS le client). Re-planifié (pas setInterval) → 0 chevauchement. +const ethLive = ref(false); const ethRates = ref([]); const ethErr = ref('') +let ethTimer = null; let ethPrev = null +async function ethPoll () { + try { + const r = await fetch(`${HUB_URL}/collab/airos-signal?serial=${encodeURIComponent(props.doc.serial_number || '')}&ip=${encodeURIComponent(props.doc.ip_address || '')}`) + const d = await r.json() + if (!d || !d.ok || d.ethRx == null) { ethErr.value = (d && d.error) || 'débit indisponible sur ce CPE'; return } + ethErr.value = '' + const cur = { ts: d.ts || Date.now(), rx: d.ethRx, tx: d.ethTx } + if (ethPrev) { + const sec = (cur.ts - ethPrev.ts) / 1000 + const delta = (a, b) => { if (a == null || b == null) return null; let x = b - a; if (x < 0) x = 0; return sec > 0 ? (x / sec) * 8 : null } + ethRates.value = [...ethRates.value, { dl: delta(ethPrev.tx, cur.tx), ul: delta(ethPrev.rx, cur.rx), ts: cur.ts }].slice(-30) + } + ethPrev = cur + } catch (e) { ethErr.value = 'erreur de mesure' } +} +function scheduleEth () { ethTimer = setTimeout(async () => { await ethPoll(); if (ethLive.value) scheduleEth() }, 6000) } +function startEthLive () { ethLive.value = true; ethErr.value = ''; ethRates.value = []; ethPrev = null; ethPoll().then(() => { if (ethLive.value) scheduleEth() }) } +function stopEthTimer () { if (ethTimer) { clearTimeout(ethTimer); ethTimer = null } } +function stopEthLive () { stopEthTimer(); ethLive.value = false } +function toggleEthLive () { ethLive.value ? stopEthLive() : startEthLive() } +const ethNow = computed(() => ethRates.value.length ? ethRates.value[ethRates.value.length - 1] : null) +const ethMax = computed(() => { let m = 1; for (const r of ethRates.value) { if (r.dl > m) m = r.dl; if (r.ul > m) m = r.ul } return m }) +function ethLine (key) { const r = ethRates.value; if (r.length < 2) return ''; const max = ethMax.value, W = 220, H = 38, n = r.length; return r.map((p, i) => `${((i / (n - 1)) * W).toFixed(1)},${(H - ((p[key] || 0) / max) * H).toFixed(1)}`).join(' ') } + // Coloration du signal par INTERVALLE (parité F). Sans-fil (dBm airOS) : ≥-60 bon, -70 correct, -78 faible, < critique. function wifiSigColor (s) { if (s == null) return '#64748b'; if (s >= -60) return '#16a34a'; if (s >= -70) return '#65a30d'; if (s >= -78) return '#d97706'; return '#dc2626' } // Fibre (Rx optique dBm) : plage saine -8..-25 vert ; -25..-28 orange ; < -28 (ou > -8 sur-puissance) rouge. @@ -1013,7 +1079,8 @@ onMounted(async () => { // Chargement PARESSEUX : on n'interroge le modem (modem-bridge) que si l'agent déplie « Appareils connectés ». watch(showHosts, v => { if (v && !hosts.value && device.value) loadHosts() }) watch(() => props.doc.serial_number, sn => { - stopWanLive() // arrêter la mesure live de l'appareil précédent + stopWanLive(); stopEthLive() // arrêter les mesures live de l'appareil précédent + liveF.value = null; airos.value = null // repartir propre pour le nouvel appareil if (sn) { hosts.value = null fetchStatus([{ serial_number: sn }]) @@ -1021,7 +1088,7 @@ watch(() => props.doc.serial_number, sn => { loadEvents(); loadWanHistory() // rafraîchir la bande dispo/événements pour le nouvel appareil } }) -onUnmounted(() => stopWanLive()) +onUnmounted(() => { stopWanLive(); stopEthLive() })