@@ -532,6 +564,55 @@ const refreshing = ref(false)
const deleting = ref(false)
const portCtx = ref(null)
+// ── Do Stuff en direct (F) : /collab/dostuff interroge l'OLT via n8n (VoIP/IPTV/distance/WAN/profil/firmware).
+// ~30 s, à la demande (charge l'OLT) — jamais auto à l'ouverture. Réservé tech-3 XGS-PON (sinon « OLT non reconnue »).
+const liveF = ref(null); const liveFLoading = ref(false)
+async function fetchLiveF () {
+ const sn = props.doc.serial_number, olt = props.doc.olt_ip
+ if (!sn || !olt) return
+ liveF.value = null; liveFLoading.value = true
+ try {
+ const r = await fetch(`${HUB_URL}/collab/dostuff?serial=${encodeURIComponent(sn)}&olt=${encodeURIComponent(olt)}`)
+ const res = await r.json()
+ if (!res.ok && res.recognized === false) res.error = 'OLT suivie par notre sonde (données ci-dessus) — direct réservé aux OLT XGS-PON (tech 3)'
+ liveF.value = res
+ } catch (e) { liveF.value = { ok: false, error: e.message } } finally { liveFLoading.value = false }
+}
+
+// « Copier pour le ticket » (bouton copy de F) : résumé plaintext de l'appareil, collable tel quel dans un ticket.
+function copyForTicket () {
+ const d = props.doc, dev = device.value || {}, lf = liveF.value, st = status.value
+ const L = []
+ const desc = [d.equipment_type, d.brand, d.model].filter(Boolean).join(' ')
+ L.push('Appareil : ' + (desc || '—'))
+ if (d.serial_number) L.push('SN : ' + d.serial_number)
+ if (d.mac_address) L.push('MAC : ' + d.mac_address)
+ L.push('Statut : ' + (st.online === true ? 'En ligne' : st.online === false ? 'Hors ligne' : '?') + (st.label ? ' (' + st.label + ')' : ''))
+ const rx = (lf && lf.rxPower != null) ? lf.rxPower : dev.rxPower
+ const tx = (lf && lf.txPower != null) ? lf.txPower : dev.txPower
+ if (rx != null && rx !== 0) L.push('Signal : Rx ' + rx + (tx != null && tx !== 0 ? ' / Tx ' + tx : '') + ' dBm')
+ if (lf && lf.distance != null) L.push('Distance : ' + lf.distance + ' m')
+ if (d.olt_ip) L.push('OLT : ' + (d.olt_name || d.olt_ip) + ' — ' + [d.olt_slot, d.olt_port, d.olt_ontid].filter(x => x != null).join('/'))
+ const mgmt = (lf && lf.managementIp) || managementIp.value
+ if (mgmt) L.push('Gestion : https://' + mgmt + '/')
+ const wan = (lf && lf.wanIp) || dev.ip
+ if (wan) L.push('WAN IP : ' + wan)
+ if (lf && lf.voip) L.push('VoIP : ' + lf.voip)
+ if (lf && lf.iptv) L.push('IPTV : ' + lf.iptv)
+ if (lf && lf.profileId) L.push('Profil ligne : ' + lf.profileId)
+ const fw = (lf && lf.firmware) || dev.firmware
+ if (fw) L.push('Firmware : ' + fw)
+ if (dev.uptime) L.push('Uptime : ' + formatUptime(dev.uptime))
+ const txt = L.join('\n')
+ const done = () => $q.notify({ type: 'positive', message: 'Résumé copié — collable dans un ticket', timeout: 1500 })
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(txt).then(done).catch(() => fallbackCopy(txt, done))
+ else fallbackCopy(txt, done)
+}
+function fallbackCopy (txt, done) {
+ try { const ta = document.createElement('textarea'); ta.value = txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done() }
+ catch (e) { $q.notify({ type: 'negative', message: 'Copie impossible' }) }
+}
+
const managementIp = computed(() => {
const iface = device.value?.interfaces?.find(i => i.role === 'management')
return iface?.ip || null
@@ -910,6 +991,14 @@ onUnmounted(() => stopWanLive())
.wan-tick { flex: 1; min-width: 2px; height: 100%; border-radius: 1px; opacity: .85; }
.dv-wan-btn { float: right; margin-top: -2px; }
.wan-spark { width: 100%; height: 38px; display: block; margin-top: 4px; }
+.dsf { margin: 8px 0 10px; padding: 8px 10px; border: 1px solid #ede9fe; background: #faf5ff; border-radius: 8px; }
+.dsf-bar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
+.dsf-title { font-size: 0.76rem; font-weight: 600; color: #6b21a8; display: flex; align-items: center; gap: 4px; }
+.dsf-tiles { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
+.dsf-chip { display: inline-flex; align-items: center; gap: 3px; font-size: 0.74rem; color: #334155; background: #fff; border: 1px solid #e2e8f0; border-radius: 5px; padding: 2px 7px; }
+.dsf-chip.dsf-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; }
+.dsf-link { color: #1e40af; text-decoration: none; }
+.dsf-link:hover { text-decoration: underline; }
.collapse-head { display: flex; align-items: center; cursor: pointer; font-size: 0.8rem; font-weight: 600; color: #475569; padding: 6px 2px; border-top: 1px solid #f1f5f9; user-select: none; }
.collapse-head:hover { color: #1e293b; }
.diag-section { margin-top: 8px; }
diff --git a/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue b/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue
index 8703a37..c56875f 100644
--- a/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue
+++ b/apps/ops/src/components/shared/detail-sections/SubscriptionDetail.vue
@@ -1,10 +1,8 @@
-
+
-
- {{ dsMsg }}
+ $emit('navigate', dt, name, t)" />
Statut{{ doc.status }}
@@ -48,32 +46,13 @@