-
Signal Rx/Tx
{{ liveF.rxPower }} / {{ liveF.txPower }} dBm
-
Distance
{{ liveF.distance }} m
-
-
-
-
Profil
{{ liveF.profileId }}
-
-
-
Firmware
{{ liveF.firmware }}
-
Dern. coupure
{{ liveF.lastDown }}
+
+
+ {{ c.label }}
+ {{ c.value }}
+
+ {{ c.sub }}
+
{{ liveF.error || 'OLT injoignable' }}
Lecture des données OLT en direct…
@@ -761,6 +761,46 @@ function wifiSigColor (s) { if (s == null) return '#64748b'; if (s >= -60) retur
function fibreRxColor (rx) { if (rx == null) return '#64748b'; if (rx <= -8 && rx >= -25) return '#16a34a'; if (rx < -28) return '#dc2626'; return '#d97706' }
// Port de gestion airOS : F expose souvent l'API sur 2196, mais l'interface web répond en 443 → on lie la racine.
const mgmtUrl = (ip) => ip ? ('https://' + String(ip).replace(/:\d+$/, '') + '/') : null
+// Qualité fibre en MOT clair (dumb-proof) + position 0-100 sur la jauge (fort −8 → plein ; faible −28 → vide).
+function fibreQualWord (rx) { if (rx == null) return ''; if (rx > -8) return 'Trop fort'; if (rx >= -25) return 'Bon'; if (rx >= -28) return 'Faible'; return 'Critique' }
+function fibreMeterPct (rx) { if (rx == null) return 0; return Math.max(4, Math.min(100, Math.round(((rx + 30) / 22) * 100))) }
+// Répartition Wi-Fi / filaire (depuis la liste d'appareils si chargée, sinon compte Wi-Fi ACS).
+const clientsSub = computed(() => {
+ const h = hosts.value && hosts.value.hosts
+ if (h && h.length) { let w = 0, f = 0; for (const x of h) { if (x.isMeshNode) continue; if (x.band) w++; else f++ } return w + ' Wi-Fi · ' + f + ' filaire' }
+ const wc = device.value && device.value.wifi && device.value.wifi.totalClients
+ return wc != null ? (wc + ' Wi-Fi') : 'appareils connectés'
+})
+// Cartes d'état DYNAMIQUES (dumb-proof) : signal, uptime, IP publique, appareils, mesh, gestion, VoIP, IPTV.
+// Source fusionnée : direct OLT (liveF) > ACS (device). Une carte n'apparaît que si sa donnée existe.
+const statCards = computed(() => {
+ const lf = (liveF.value && liveF.value.ok) ? liveF.value : null
+ const d = device.value || {}
+ const cards = []
+ const rx = (lf && lf.rxPower != null) ? lf.rxPower : ((d.rxPower != null && d.rxPower !== 0) ? d.rxPower : null)
+ const tx = (lf && lf.txPower != null) ? lf.txPower : ((d.txPower != null && d.txPower !== 0) ? d.txPower : null)
+ if (rx != null) cards.push({ key: 'sig', icon: 'network_check', label: 'Signal fibre', value: rx + (tx != null ? ' / ' + tx : '') + ' dBm', color: fibreRxColor(rx), meter: fibreMeterPct(rx), sub: fibreQualWord(rx) + ' — sain de −8 à −25 dBm' })
+ else if (isWireless.value && airos.value && airos.value.ok && airos.value.signal != null) cards.push({ key: 'sigw', icon: 'network_check', label: 'Signal sans-fil', value: airos.value.signal + ' dBm', color: wifiSigColor(airos.value.signal), meter: Math.max(4, Math.min(100, Math.round(((airos.value.signal + 90) / 40) * 100))), sub: (airos.value.signal >= -70 ? 'Bon' : airos.value.signal >= -78 ? 'Faible' : 'Critique') })
+ const up = (lf && lf.uptime) ? lf.uptime : (d.uptime ? formatUptime(d.uptime) : (airos.value && airos.value.ok && airos.value.uptime) || null)
+ if (up) cards.push({ key: 'up', icon: 'schedule', label: 'Uptime', value: up, sub: 'depuis le dernier redémarrage' })
+ const wan = (lf && lf.wanIp) || d.ip || null
+ if (wan) cards.push({ key: 'wan', icon: 'public', label: 'IP publique', value: wan, mono: true, sub: 'adresse Internet du client' })
+ const cc = clientCount.value
+ if (cc != null && (cc > 0 || d.wifi)) cards.push({ key: 'cli', icon: 'devices', label: 'Appareils', value: String(cc), sub: clientsSub.value, click: 'hosts' })
+ const mesh = (d.mesh || []).length
+ if (mesh > 1) cards.push({ key: 'mesh', icon: 'hub', label: 'Réseau maillé', value: (mesh - 1) + ' satellite' + (mesh - 1 > 1 ? 's' : ''), color: '#16a34a', sub: d.mesh.filter(n => n.active).length + ' actif(s)' })
+ const mgmt = (lf && lf.managementIp) || managementIp.value || (isWireless.value ? props.doc.ip_address : null)
+ if (mgmt) cards.push({ key: 'mgmt', icon: 'settings', label: 'Gestion', value: mgmt, mono: true, link: mgmtUrl(mgmt), sub: "interface de l'appareil" })
+ if (lf && lf.voip) cards.push({ key: 'voip', icon: 'call', label: 'Téléphonie', value: 'Enregistrée', color: '#16a34a', sub: 'ligne VoIP active' })
+ if (lf && lf.iptv) cards.push({ key: 'iptv', icon: 'live_tv', label: 'Télé', value: 'Connectée', color: '#16a34a', sub: 'décodeur IPTV en ligne' })
+ if (lf && lf.distance != null) cards.push({ key: 'dist', icon: 'straighten', label: 'Distance', value: lf.distance + ' m', sub: 'longueur de fibre estimée' })
+ if (lf && lf.profileId) cards.push({ key: 'prof', icon: 'speed', label: 'Profil de ligne', value: String(lf.profileId), sub: 'forfait OLT' })
+ const fw = (lf && lf.firmware) || d.firmware || null
+ if (fw) cards.push({ key: 'fw', icon: 'memory', label: 'Firmware', value: fw, mono: true })
+ const down = (lf && lf.lastDown) || lastOutage.value || null
+ if (down) cards.push({ key: 'down', icon: 'power_off', label: 'Dern. coupure', value: down, warn: true, sub: 'dernière raison hors ligne' })
+ return cards
+})
async function fetchLiveF () {
const olt = props.doc.olt_ip
@@ -1189,6 +1229,17 @@ onUnmounted(() => { stopWanLive(); stopEthLive() })
.topo-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.topo-sub { font-size: 0.66rem; color: #94a3b8; }
.dv-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 10px; }
+.stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin-top: 10px; }
+.stat-card { background: #f8fafc; border: 1px solid #eef2f7; border-radius: 10px; padding: 9px 11px; text-decoration: none; color: inherit; display: block; }
+.stat-card.stat-click { cursor: pointer; transition: background .12s, border-color .12s; }
+.stat-card.stat-click:hover { background: #eef2ff; border-color: #c7d2fe; }
+.stat-lbl { font-size: 0.68rem; color: #64748b; display: flex; align-items: center; gap: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: .3px; }
+.stat-lbl.stat-warn { color: #b45309; }
+.stat-val { font-size: 1.15rem; font-weight: 700; color: #1e293b; margin-top: 3px; font-variant-numeric: tabular-nums; line-height: 1.2; word-break: break-word; }
+.stat-val.mono { font-family: monospace; font-size: 0.9rem; font-weight: 600; }
+.stat-meter { height: 5px; border-radius: 3px; background: #e2e8f0; margin-top: 7px; overflow: hidden; }
+.stat-meter-fill { height: 100%; border-radius: 3px; transition: width .4s ease; }
+.stat-sub { font-size: 0.7rem; color: #64748b; margin-top: 5px; line-height: 1.3; }
.dv-tile { border: 1px solid #e2e8f0; border-radius: 7px; padding: 6px 8px; background: #fff; }
.dv-tl { font-size: 0.68rem; color: #64748b; display: flex; align-items: center; gap: 3px; }
.dv-tl.dv-warn { color: #b45309; }