/** * GenieACS live device status composable. * Looks up devices by serial number via targo-hub → GenieACS NBI proxy. */ import { ref, readonly } from 'vue' import { HUB_URL } from 'src/config/hub' // Singleton state — survives component remounts so cached data shows instantly const cache = new Map() // serial → { data, ts } const hostsCache = new Map() // serial → { data, ts } const oltCache = new Map() // serial → { data, ts } const deviceMap = ref(new Map()) // serial → { ...summarizedDevice } const oltMap = ref(new Map()) // serial → { status, rxPowerOlt, ... } const loading = ref(false) const error = ref(null) const CACHE_TTL = 60_000 // 1 min const HOSTS_TTL = 120_000 // 2 min const OLT_TTL = 60_000 // 1 min /** * Fetch live device info from GenieACS for a list of equipment objects. * Equipment must have serial_number (and optionally mac_address). * Returns a reactive Map. */ export function useDeviceStatus () { async function fetchStatus (equipmentList) { if (!equipmentList || !equipmentList.length) return error.value = null const now = Date.now() const toFetch = [] const map = new Map(deviceMap.value) // Immediately populate from cache so UI shows buffered data for (const eq of equipmentList) { const serial = eq.serial_number if (!serial) continue const cached = cache.get(serial) if (cached) { // Always show cached data immediately (even if stale) map.set(serial, cached.data) // Only skip re-fetch if cache is fresh if ((now - cached.ts) < CACHE_TTL) continue } toFetch.push(serial) } // Apply cached data right away before network fetch if (map.size > deviceMap.value.size) { deviceMap.value = new Map(map) } if (!toFetch.length) return // Batch lookups (GenieACS NBI doesn't support batch, so parallel individual) loading.value = true const results = await Promise.allSettled( toFetch.map(serial => fetch(`${HUB_URL}/devices/lookup?serial=${encodeURIComponent(serial)}`) .then(r => r.ok ? r.json() : null) .then(data => ({ serial, data: Array.isArray(data) && data.length ? data[0] : null })) .catch(() => ({ serial, data: null })) ) ) for (const r of results) { if (r.status === 'fulfilled' && r.value.data) { // Merge fresh data on top of existing cached data (don't clear fields) const existing = map.get(r.value.serial) const fresh = r.value.data const merged = existing ? { ...existing, ...fresh } : fresh map.set(r.value.serial, merged) cache.set(r.value.serial, { data: merged, ts: now }) } } deviceMap.value = map loading.value = false } function getDevice (serial) { return deviceMap.value.get(serial) || null } /** * Fetch OLT SNMP status for a serial number. * Returns { status: 'online'|'offline', rxPowerOlt, distance, ... } or null. */ async function fetchOltStatus (serial) { if (!serial) return null const now = Date.now() const cached = oltCache.get(serial) if (cached && (now - cached.ts) < OLT_TTL) return cached.data try { const res = await fetch(`${HUB_URL}/olt/onus?serial=${encodeURIComponent(serial)}`) if (!res.ok) return cached?.data || null const data = await res.json() if (data && !data.error) { oltCache.set(serial, { data, ts: now }) const map = new Map(oltMap.value) map.set(serial, data) oltMap.value = map return data } } catch {} return cached?.data || null } function getOltData (serial) { return oltMap.value.get(serial) || null } function isOnline (serial) { const d = getDevice(serial) const tr069Online = d && d.lastInform ? (Date.now() - new Date(d.lastInform).getTime()) < 15 * 60 * 1000 : null // Cross-reference OLT SNMP — ground truth for fiber connectivity const olt = getOltData(serial) const oltOnline = olt ? olt.status === 'online' : null // OLT is authoritative: if OLT says online, device has internet regardless of TR-069 if (oltOnline === true) return true if (oltOnline === false) return false // No OLT data available — fall back to TR-069 only return tr069Online } /** * Combined status with source info for UI display. * Returns { online: bool|null, source: 'both'|'olt'|'tr069'|'unknown', label: string, detail: string } */ function combinedStatus (serial) { const d = getDevice(serial) const tr069Online = d && d.lastInform ? (Date.now() - new Date(d.lastInform).getTime()) < 15 * 60 * 1000 : null const olt = getOltData(serial) const oltOnline = olt ? olt.status === 'online' : null // Both agree online if (tr069Online === true && oltOnline === true) { return { online: true, source: 'both', label: 'En ligne', detail: 'TR-069 + Fibre OK' } } // OLT says online, TR-069 stale — device is online, management channel stale if (oltOnline === true && tr069Online !== true) { return { online: true, source: 'olt', label: 'En ligne', detail: 'Fibre OK · TR-069 inactif' } } // Both agree offline if (tr069Online === false && oltOnline === false) { return { online: false, source: 'both', label: 'Hors ligne', detail: 'TR-069 + Fibre hors ligne' } } // OLT says offline, TR-069 unknown if (oltOnline === false && tr069Online === null) { return { online: false, source: 'olt', label: 'Hors ligne', detail: 'Fibre hors ligne' } } // OLT says offline, TR-069 says online (unlikely but possible during transition) if (oltOnline === false && tr069Online === true) { return { online: false, source: 'olt', label: 'Hors ligne', detail: 'Fibre coupée · TR-069 résiduel' } } // No OLT data, TR-069 only if (tr069Online === true) { return { online: true, source: 'tr069', label: 'En ligne', detail: 'TR-069 actif' } } if (tr069Online === false) { return { online: false, source: 'tr069', label: 'Hors ligne', detail: 'TR-069 inactif · OLT non vérifié' } } return { online: null, source: 'unknown', label: 'Inconnu', detail: 'Aucune donnée' } } function signalQuality (serial) { const d = getDevice(serial) if (!d || d.rxPower == null) return null const rx = parseFloat(d.rxPower) if (isNaN(rx)) return null // GPON Rx power: > -8 excellent, -8 to -20 good, -20 to -25 fair, < -25 bad if (rx > -8) return 'excellent' if (rx > -20) return 'good' if (rx > -25) return 'fair' return 'bad' } async function rebootDevice (serial) { const d = getDevice(serial) if (!d) throw new Error('Device not found in ACS') const res = await fetch( `${HUB_URL}/devices/${encodeURIComponent(d._id)}/tasks?connection_request&timeout=5000`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'reboot' }), } ) if (!res.ok) { const err = await res.json().catch(() => ({})) throw new Error(err.error || 'Reboot failed') } return res.json() } async function refreshDeviceParams (serial) { const d = getDevice(serial) if (!d) throw new Error('Device not found in ACS') const res = await fetch( `${HUB_URL}/devices/${encodeURIComponent(d._id)}/tasks?connection_request&timeout=10000`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'getParameterValues', parameterNames: [ 'InternetGatewayDevice.DeviceInfo.', 'InternetGatewayDevice.WANDevice.1.', 'Device.DeviceInfo.', 'Device.Optical.Interface.1.', ], }), } ) if (!res.ok) { const err = await res.json().catch(() => ({})) throw new Error(err.error || 'Refresh failed') } // Mark cache stale so next fetch refreshes, but keep data for instant display const cached = cache.get(serial) if (cached) cached.ts = 0 return res.json() } async function fetchHosts (serial, refresh = false) { // Return cached hosts instantly if available and not forcing refresh const cached = hostsCache.get(serial) if (cached && !refresh && (Date.now() - cached.ts) < HOSTS_TTL) { return cached.data } const d = getDevice(serial) if (!d) return cached?.data || null // return stale cache if device not resolved yet const url = `${HUB_URL}/devices/${encodeURIComponent(d._id)}/hosts${refresh ? '?refresh' : ''}` const res = await fetch(url) if (!res.ok) return cached?.data || null const data = await res.json() hostsCache.set(serial, { data, ts: Date.now() }) return data } /** * Fetch port neighbor context for a serial — shows if other ONUs on the same port are affected. * Returns { total, online, offline, neighbors: [{ serial, status, cause }], is_mass_outage } */ async function fetchPortContext (serial) { if (!serial) return null try { const res = await fetch(`${HUB_URL}/ai/port-health-by-serial?serial=${encodeURIComponent(serial)}`) if (!res.ok) return null return await res.json() } catch { return null } } return { deviceMap: readonly(deviceMap), oltMap: readonly(oltMap), loading: readonly(loading), error: readonly(error), fetchStatus, fetchOltStatus, fetchPortContext, getDevice, getOltData, isOnline, combinedStatus, signalQuality, rebootDevice, refreshDeviceParams, fetchHosts, } }