gigafibre-fsm/apps/ops/src/composables/useDeviceStatus.js
louispaulb 10d0854e51 refactor: reduce token count, DRY code, consolidate docs
Backend services:
- targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons
  lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas,
  extract dispatch scoring weights, trim section dividers across 9 files
- modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(),
  consolidate DM query factory, fix duplicate username fill bug, trim headers
  (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%)

Frontend:
- useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into
  6 focused helpers (processOnlineStatus, processWanIPs, processRadios,
  processMeshNodes, processClients, checkRadioIssues)
- EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments

Documentation (17 → 13 files, -1,400 lines):
- New consolidated README.md (architecture, services, dependencies, auth)
- Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md
- Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md
- Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md
- Update ROADMAP.md with current phase status
- Delete CONTEXT.md (absorbed into README)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:39:58 -04:00

284 lines
9.6 KiB
JavaScript

/**
* 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<serial, deviceInfo>.
*/
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,
}
}