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>
284 lines
9.6 KiB
JavaScript
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,
|
|
}
|
|
}
|