<template>
  <!-- Vue appareil moderne : carte réseau + voyants + tuiles + trafic + événements (le dense est replié plus bas) -->
  <div v-if="device" class="topo-summary">
    <div class="topo-row">
      <div class="topo-node"><q-icon name="public" size="22px" :color="status.online ? 'green-6' : 'grey-5'" /><div class="topo-cap">Internet</div></div>
      <div class="topo-link" :class="{ ok: status.online }"></div>
      <div class="topo-node"><q-icon name="cell_tower" size="22px" color="blue-grey-6" /><div class="topo-cap">OLT</div><div class="topo-sub">{{ oltLabel }}</div></div>
      <div class="topo-link" :class="{ ok: status.online }"></div>
      <div class="topo-node"><div class="topo-modem"><q-icon name="router" size="22px" color="primary" /><span class="topo-dot" :class="status.online ? 'on' : 'off'"></span></div><div class="topo-cap">Modem</div><div class="topo-sub" v-if="doc.olt_slot != null">{{ doc.olt_slot }}/{{ doc.olt_port }}/{{ doc.olt_ontid }}</div></div>
      <div class="topo-link" :class="{ ok: clientCount > 0 }"></div>
      <div class="topo-node topo-click" @click="openHosts"><div style="position:relative;display:inline-block;"><q-icon name="devices" size="22px" color="primary" /><q-circular-progress v-if="hostsLoading" indeterminate size="15px" :thickness="0.22" color="green-5" style="position:absolute;right:-9px;top:-4px;" /></div><div class="topo-cap">{{ clientCount }} app.</div></div>
    </div>
    <div class="topo-badges">
      <q-badge :color="statusBadgeColor">{{ status.label }}</q-badge>
      <q-badge v-if="device.opticalStatus" outline :color="device.opticalStatus === 'Up' ? 'green' : 'red'">Fibre {{ device.opticalStatus }}</q-badge>
      <q-badge v-if="signalLabel" outline :color="signalBadgeColor">Signal {{ signalLabel }}</q-badge>
      <span v-if="device.lastInform" class="text-caption text-grey-5">{{ timeAgo(device.lastInform) }}</span>
      <q-space />
      <q-btn flat dense size="sm" icon="sync" :loading="refreshing" @click="doRefresh" title="Rafraîchir" />
    </div>

    <div class="dv-grid">
      <div class="dv-tile" v-if="device.rxPower != null && device.rxPower !== 0"><div class="dv-tl"><q-icon name="network_check" size="14px" /> Signal Rx/Tx</div><div class="dv-tv" :style="{ color: rxColor }">{{ device.rxPower }}<span v-if="device.txPower != null"> / {{ device.txPower }}</span> dBm</div></div>
      <div class="dv-tile" v-if="device.uptime"><div class="dv-tl"><q-icon name="schedule" size="14px" /> Uptime</div><div class="dv-tv">{{ formatUptime(device.uptime) }}</div></div>
      <div class="dv-tile" v-if="managementIp"><div class="dv-tl"><q-icon name="settings" size="14px" /> Gestion</div><a :href="'https://' + managementIp + '/'" target="_blank" class="dv-tv dv-link">{{ managementIp }}</a></div>
      <div class="dv-tile" v-if="lastOutage"><div class="dv-tl dv-warn"><q-icon name="power_off" size="14px" /> Dern. coupure</div><div class="dv-tv">{{ lastOutage }}</div></div>
    </div>

    <div class="dv-row2">
      <div class="dv-wan">
        <div class="dv-tl">
          <q-icon name="show_chart" size="14px" /> {{ wanLive ? 'Débit WAN en direct' : 'Disponibilité & signal récents' }}
          <q-btn dense flat round size="9px" class="dv-wan-btn" :icon="wanLive ? 'stop' : 'speed'" :color="wanLive ? 'red' : 'primary'" @click="toggleWanLive">
            <q-tooltip>{{ wanLive ? 'Arrêter la mesure' : 'Débit WAN en direct — compteurs OLT (façon F)' }}</q-tooltip>
          </q-btn>
        </div>
        <!-- Mode LIVE : débit WAN temps réel (compteurs Counter64 de l'OLT, échantillon 5 s, façon F) -->
        <template v-if="wanLive">
          <div v-if="wanLiveErr" class="dv-wan-ph">{{ wanErrMsg }}</div>
          <template v-else>
            <svg viewBox="0 0 220 40" class="wan-spark" preserveAspectRatio="none">
              <polyline v-if="wanRates.length > 1" :points="wanLine('dl')" fill="none" stroke="#16a34a" stroke-width="1.5" />
              <polyline v-if="wanRates.length > 1" :points="wanLine('ul')" fill="none" stroke="#2563eb" stroke-width="1.5" />
            </svg>
            <div class="text-caption q-mt-xs">
              <span style="color:#16a34a;font-weight:600">▼ {{ fmtBps(wanNow && wanNow.dl) }}</span> &nbsp;
              <span style="color:#2563eb;font-weight:600">▲ {{ fmtBps(wanNow && wanNow.ul) }}</span>
              <q-spinner size="11px" class="q-ml-xs" v-if="!wanNow" /><span class="text-grey-5" v-if="!wanNow"> mesure…</span>
            </div>
          </template>
        </template>
        <!-- Mode défaut : bande disponibilité + signal Rx (historique device_cache, passif) -->
        <template v-else-if="wanHistory.length">
          <div class="wan-strip">
            <span v-for="(p, i) in wanHistory" :key="i" class="wan-tick" :style="{ background: p.online ? '#16a34a' : '#dc2626' }"><q-tooltip>{{ fmtTs(p.ts) }} — {{ p.online ? 'en ligne' : 'hors ligne' }}{{ p.rxPower != null ? ' · ' + p.rxPower + ' dBm' : '' }}</q-tooltip></span>
          </div>
          <div class="text-caption text-grey-6 q-mt-xs">Dispo {{ uptimePct }}% · Rx {{ lastRx }} · {{ wanHistory.length }} pts</div>
        </template>
        <div v-else class="dv-wan-ph"><q-icon name="insights" size="18px" color="grey-4" /> historique en accumulation</div>
      </div>
      <div class="dv-events" v-if="events.length">
        <div class="dv-tl"><q-icon name="history" size="14px" /> Événements récents</div>
        <div v-for="e in events.slice(0, 4)" :key="e.id" class="dv-ev"><q-icon :name="evIcon(e)" size="13px" :color="evColor(e)" /> <span class="text-grey-6">{{ evTime(e) }}</span> · {{ evLabel(e) }}</div>
      </div>
    </div>
  </div>

  <!-- Do Stuff en direct (F) : interroge l'OLT via n8n (comme F) — remonte VoIP / IPTV / distance / WAN / profil de
       ligne que le poller passif n'expose pas. À la demande (~30 s). + « Copier pour le ticket » (bouton copy de F). -->
  <div v-if="device && doc.serial_number && doc.olt_ip" class="dsf">
    <div class="dsf-bar">
      <q-btn v-if="!liveF && !liveFLoading" dense flat no-caps size="sm" color="deep-purple-6" icon="bolt"
        label="Do Stuff en direct (F)" @click="fetchLiveF">
        <q-tooltip>Interroge l'OLT en direct (VoIP, IPTV, distance, WAN, profil) — ~30 s, comme F</q-tooltip>
      </q-btn>
      <div v-if="liveFLoading" class="text-caption text-deep-purple-6"><q-spinner-dots size="18px" /> Interrogation de l'OLT… (~30 s)</div>
      <template v-if="liveF && !liveFLoading">
        <span class="dsf-title"><q-icon :name="liveF.ok ? (liveF.online ? 'check_circle' : 'cancel') : 'info'" :color="liveF.ok ? (liveF.online ? 'green-6' : 'red-6') : 'grey-5'" size="15px" /> Direct (F)<span v-if="!liveF.ok" class="text-grey-6 q-ml-xs">— {{ liveF.error }}</span></span>
        <q-btn dense flat round size="xs" icon="refresh" color="grey-6" @click="fetchLiveF"><q-tooltip>Réinterroger</q-tooltip></q-btn>
      </template>
      <q-space />
      <q-btn dense flat no-caps size="sm" color="grey-7" icon="content_copy" label="Copier pour le ticket" @click="copyForTicket">
        <q-tooltip>Copie un résumé de l'appareil à coller dans un ticket — comme F</q-tooltip>
      </q-btn>
    </div>
    <div v-if="liveF && liveF.ok" class="dsf-tiles">
      <span v-if="liveF.rxPower != null" class="dsf-chip dsf-sig" :style="{ color: fibreRxColor(liveF.rxPower), borderColor: fibreRxColor(liveF.rxPower) }"><q-icon name="network_check" size="12px" /> {{ liveF.signal }} · Rx {{ liveF.rxPower }}<span v-if="liveF.txPower != null"> / Tx {{ liveF.txPower }}</span> dBm</span>
      <span v-if="liveF.distance != null" class="dsf-chip"><q-icon name="straighten" size="12px" /> {{ liveF.distance }} m</span>
      <span v-if="liveF.voip" class="dsf-chip"><q-icon name="call" size="12px" /> VoIP {{ liveF.voip }}</span>
      <span v-if="liveF.iptv" class="dsf-chip"><q-icon name="live_tv" size="12px" /> IPTV {{ liveF.iptv }}</span>
      <span v-if="liveF.wanIp" class="dsf-chip"><q-icon name="public" size="12px" /> WAN {{ liveF.wanIp }}</span>
      <span v-if="liveF.profileId" class="dsf-chip"><q-icon name="speed" size="12px" /> Profil {{ liveF.profileId }}</span>
      <span v-if="liveF.uptime" class="dsf-chip"><q-icon name="schedule" size="12px" /> {{ liveF.uptime }}</span>
      <span v-if="liveF.lastDown" class="dsf-chip dsf-warn"><q-icon name="history" size="12px" /> {{ liveF.lastDown }}</span>
      <span v-if="liveF.firmware" class="dsf-chip"><q-icon name="memory" size="12px" /> {{ liveF.firmware }}</span>
      <a v-if="liveF.managementIp" :href="'https://' + liveF.managementIp + '/'" target="_blank" class="dsf-chip dsf-link"><q-icon name="router" size="12px" /> GUI {{ liveF.managementIp }}</a>
    </div>
  </div>

  <!-- Do Stuff SANS-FIL (airOS/Cambium) : signal / signal AP / CCQ / débit / capacité airMAX live du CPE, via le
       webhook n8n get_signal_airos (le hub ne joint pas les CPE) — parité avec le device_view sans-fil de F. -->
  <div v-if="isWireless" class="dsf dsf-wl">
    <!-- État RAPIDE au chargement (RADIUS, sans status.cgi) : en ligne / hors ligne + depuis + IP client. -->
    <div v-if="wStatus && wStatus.ok" class="dsf-wstatus">
      <span class="dsf-dot" :class="wStatus.online === true ? 'on' : wStatus.online === false ? 'off' : 'unk'"></span>
      <span class="text-weight-medium">{{ wStatus.online === true ? 'En ligne' : wStatus.online === false ? 'Hors ligne' : 'État inconnu' }}</span>
      <span v-if="wStatus.since" class="text-grey-6">· {{ wStatus.online ? 'depuis' : 'vu' }} {{ timeAgo(wStatus.since) }}</span>
      <span v-if="wStatus.framedIp" class="text-grey-6">· IP {{ wStatus.framedIp }}</span>
    </div>
    <div class="dsf-bar">
      <q-btn v-if="!airos && !airosLoading" dense flat no-caps size="sm" color="deep-purple-6" icon="wifi_tethering"
        label="Do Stuff en direct (sans-fil)" @click="fetchAiros">
        <q-tooltip>Interroge le CPE : signal, CCQ, débit, capacité airMAX — comme F</q-tooltip>
      </q-btn>
      <div v-if="airosLoading" class="text-caption text-deep-purple-6"><q-spinner-dots size="18px" /> Interrogation du CPE…</div>
      <template v-if="airos && !airosLoading">
        <span class="dsf-title"><q-icon :name="airos.ok ? 'wifi' : 'wifi_off'" :color="airos.ok ? 'green-6' : 'grey-5'" size="15px" /> {{ airos.ok ? (airos.model || 'CPE sans-fil') : 'Sans-fil' }}</span>
        <q-btn dense flat round size="xs" icon="refresh" color="grey-6" @click="fetchAiros"><q-tooltip>Réinterroger</q-tooltip></q-btn>
      </template>
      <q-space />
      <a v-if="doc.ip_address" :href="mgmtUrl(doc.ip_address)" target="_blank" class="dsf-open"><q-icon name="open_in_new" size="13px" /> Interface</a>
      <q-btn dense flat no-caps size="sm" color="grey-7" icon="content_copy" label="Copier pour le ticket" @click="copyForTicket" />
    </div>
    <div v-if="airos && airos.ok" class="dsf-tiles">
      <span v-if="airos.signal != null" class="dsf-chip dsf-sig" :style="{ color: wifiSigColor(airos.signal), borderColor: wifiSigColor(airos.signal) }"><q-icon name="network_check" size="12px" /> Signal {{ airos.signal }} dBm<span v-if="airos.signalAp != null" :style="{ color: wifiSigColor(airos.signalAp) }"> · AP {{ airos.signalAp }}</span></span>
      <span v-if="airos.ccq != null" class="dsf-chip"><q-icon name="verified" size="12px" /> CCQ {{ airos.ccq }} %</span>
      <span v-if="airos.txRate || airos.rxRate" class="dsf-chip"><q-icon name="swap_vert" size="12px" /> TX/RX {{ airos.txRate || '—' }} / {{ airos.rxRate || '—' }}</span>
      <span v-if="airos.capDown != null || airos.capUp != null" class="dsf-chip"><q-icon name="speed" size="12px" /> airMAX ▼{{ airos.capDown != null ? airos.capDown : '—' }} ▲{{ airos.capUp != null ? airos.capUp : '—' }} Mbps</span>
      <span v-if="airos.ssid" class="dsf-chip"><q-icon name="rss_feed" size="12px" /> {{ airos.ssid }}</span>
      <span v-if="airos.frequency" class="dsf-chip"><q-icon name="settings_input_antenna" size="12px" /> {{ airos.frequency }}</span>
      <span v-if="airos.txPower" class="dsf-chip"><q-icon name="bolt" size="12px" /> TX {{ airos.txPower }}</span>
      <span v-if="airos.uptime" class="dsf-chip"><q-icon name="schedule" size="12px" /> {{ airos.uptime }}</span>
      <span v-if="airos.version" class="dsf-chip"><q-icon name="memory" size="12px" /> {{ airos.version }}</span>
      <a v-if="airos.apIp" :href="mgmtUrl(airos.apIp)" target="_blank" class="dsf-chip dsf-link"><q-icon name="cell_tower" size="12px" /> AP {{ airos.apIp }}</a>
      <span v-else-if="airos.apMac" class="dsf-chip"><q-icon name="cell_tower" size="12px" /> AP {{ airos.apMac }}</span>
    </div>
    <div v-else-if="airos && !airos.ok" class="dsf-note">
      <q-icon name="info" size="12px" /> {{ airos.registered === false ? 'Signal sans-fil en attente : webhook n8n « get_signal_airos » à configurer.' : airos.error }}
    </div>
    <!-- MAC RADIUS (WPA2) = autoritaire, AUTO-synchronisée côté serveur au remplacement d'équipement. -->
    <div v-if="airos && airos.ok && airos.macSynced" class="dsf-note" style="color:#166534">
      <q-icon name="check_circle" size="13px" color="green-7" /> MAC synchronisée automatiquement depuis RADIUS : <code>{{ airos.radiusMac }}</code><span v-if="airos.macWas"> (était {{ airos.macWas }})</span>
    </div>
    <div v-else-if="airos && airos.ok && macMismatch" class="dsf-macfix">
      <q-icon name="sync_problem" size="13px" color="deep-orange-7" /> MAC RADIUS <code>{{ airos.radiusMac }}</code> ≠ enregistrée <code>{{ doc.mac_address || '—' }}</code>
      <q-btn dense flat no-caps size="sm" color="primary" icon="sync" label="Mettre à jour" :loading="macSyncing" @click="syncMacFromRadius" />
    </div>
    <!-- Débit Ethernet client EN DIRECT (eth0 du CPE) — façon F. À la demande (charge le CPE). ▼ download / ▲ upload. -->
    <div v-if="airos && airos.ok" class="dsf-eth">
      <div class="dsf-eth-row">
        <span class="dsf-eth-tl"><q-icon name="show_chart" size="13px" /> Débit Ethernet client (eth0)</span>
        <q-btn dense flat round size="9px" :icon="ethLive ? 'stop' : 'speed'" :color="ethLive ? 'red' : 'primary'" @click="toggleEthLive"><q-tooltip>{{ ethLive ? 'Arrêter la mesure' : 'Débit en direct — eth0 du CPE (façon F)' }}</q-tooltip></q-btn>
      </div>
      <template v-if="ethLive">
        <div v-if="ethErr" class="dsf-note">{{ ethErr }}</div>
        <template v-else>
          <svg viewBox="0 0 220 40" class="wan-spark" preserveAspectRatio="none">
            <polyline v-if="ethRates.length > 1" :points="ethLine('dl')" fill="none" stroke="#16a34a" stroke-width="1.5" />
            <polyline v-if="ethRates.length > 1" :points="ethLine('ul')" fill="none" stroke="#2563eb" stroke-width="1.5" />
          </svg>
          <div class="text-caption q-mt-xs">
            <span style="color:#16a34a;font-weight:600">▼ {{ fmtBps(ethNow && ethNow.dl) }}</span> &nbsp;
            <span style="color:#2563eb;font-weight:600">▲ {{ fmtBps(ethNow && ethNow.ul) }}</span>
            <q-spinner size="11px" class="q-ml-xs" v-if="!ethNow" /><span class="text-grey-5" v-if="!ethNow"> mesure… (~12 s)</span>
          </div>
        </template>
      </template>
    </div>
  </div>

  <!-- Identité de l'appareil (repliée par défaut) -->
  <div class="collapse-head" @click="showIdentity = !showIdentity"><q-icon :name="showIdentity ? 'expand_more' : 'chevron_right'" size="18px" class="q-mr-xs" />Identité de l'appareil</div>
  <div v-show="showIdentity || !device" class="modal-field-grid">
    <div v-for="f in equipFields" :key="f.field" class="mf">
      <span class="mf-label">{{ f.label }}</span>
      <InlineField :value="doc[f.field]" :field="f.field" doctype="Service Equipment" :docname="docName"
        v-bind="f.props || {}" :placeholder="f.placeholder || '—'" @saved="v => doc[f.field] = v.value" />
    </div>
    <div class="mf" v-if="doc.barcode"><span class="mf-label">Code-barres</span><code>{{ doc.barcode }}</code></div>
  </div>

  <div v-if="device" class="q-mt-md">
    <div class="collapse-head" @click="showTech = !showTech"><q-icon :name="showTech ? 'expand_more' : 'chevron_right'" size="18px" class="q-mr-xs" />Détails techniques<span class="text-caption text-grey-5 q-ml-xs">fibre · IP · WiFi · Ethernet · firmware</span></div>
    <div v-show="showTech">
    <div class="diag-section fibre-top">
      <q-icon name="settings_input_hdmi" size="16px" class="q-mr-xs" />
      <span class="text-weight-bold q-mr-sm">Fibre</span>
      <q-badge :color="device.opticalStatus === 'Up' ? 'green' : 'red'" class="q-mr-sm">{{ device.opticalStatus || '—' }}</q-badge>
      <template v-if="device.rxPower != null && device.rxPower !== 0">
        <span class="diag-label q-mr-xs">Rx</span>
        <span class="text-weight-medium" :style="{ color: rxColor }">{{ device.rxPower }} dBm</span>
      </template>
      <template v-if="device.txPower != null && device.txPower !== 0">
        <span class="diag-label q-ml-sm q-mr-xs">Tx</span>
        <span class="text-weight-medium">{{ device.txPower }} dBm</span>
      </template>
    </div>

    <div class="diag-section" v-if="device.interfaces?.length">
      <div class="diag-section-title"><q-icon name="language" size="16px" class="q-mr-xs" />Adresses IP</div>
      <div class="ip-grid">
        <div v-for="iface in sortedInterfaces" :key="iface.iface + iface.ip" class="ip-item">
          <span class="ip-role" :class="'ip-role--' + iface.role">{{ roleLabel(iface.role) }}</span>
          <a v-if="iface.role === 'management'" :href="'https://' + iface.ip + '/superadmin/'" target="_blank" class="ip-addr ip-link">{{ iface.ip }}</a>
          <code v-else class="ip-addr">{{ iface.ip }}</code>
          <span class="text-caption text-grey-5" v-if="iface.name">({{ iface.name }})</span>
        </div>
      </div>
    </div>

    <div class="diag-section" v-if="device.wifi">
      <div class="diag-section-title"><q-icon name="wifi" size="16px" class="q-mr-xs" />WiFi</div>
      <div class="diag-grid">
        <template v-for="(radio, key) in wifiRadios" :key="key">
          <div class="diag-item" v-if="radio.status">
            <span class="diag-label">{{ radio.label }}</span>
            <span :class="radio.status === 'Up' ? 'text-positive' : 'text-negative'">
              {{ radio.status }} &middot; Ch {{ radio.channel || '?' }}
              <template v-if="radio.bandwidth"> &middot; {{ radio.bandwidth }}</template>
            </span>
          </div>
          <div class="diag-item" v-if="radio.status">
            <span class="diag-label">{{ radio.label }} clients</span>
            <span class="text-weight-medium">{{ radio.clients }}</span>
          </div>
        </template>
        <div class="diag-item" v-if="device.wifi.totalClients > 0">
          <span class="diag-label">Total clients</span>
          <span class="text-weight-bold">
            {{ device.wifi.totalClients }}
            <span v-if="device.wifi.meshClients > 0" class="text-caption text-grey-6">
              ({{ device.wifi.directClients }} direct + {{ device.wifi.meshClients }} mesh)
            </span>
          </span>
        </div>
      </div>
    </div>

    <div class="diag-section" v-if="device.mesh?.length > 1 && !hosts">
      <div class="diag-section-title"><q-icon name="hub" size="16px" class="q-mr-xs" />EasyMesh ({{ device.mesh.length }} noeuds)</div>
      <div class="mesh-nodes">
        <div v-for="node in device.mesh" :key="node.id" class="mesh-node">
          <div class="mesh-node-header">
            <q-badge :color="node.active ? 'green' : 'red'" class="q-mr-xs" style="padding: 2px 5px;" />
            <span class="text-weight-medium">{{ node.name || 'Node ' + node.id }}</span>
            <span class="text-caption text-grey-6 q-ml-xs">{{ node.clients }} clients</span>
          </div>
          <div class="text-caption text-grey-5" v-if="node.ip || node.mac">
            <span v-if="node.ip">{{ node.ip }}</span>
            <span v-if="node.mac"> &middot; {{ node.mac }}</span>
          </div>
        </div>
      </div>
    </div>

    <div class="diag-section" v-if="device.ethernet">
      <div class="diag-section-title"><q-icon name="cable" size="16px" class="q-mr-xs" />Ethernet</div>
      <div class="diag-grid">
        <div class="diag-item" v-for="(port, key) in ethernetPorts" :key="key">
          <span class="diag-label">{{ port.label }}</span>
          <span :class="port.status === 'Up' ? 'text-positive' : 'text-grey-6'">
            {{ port.status || '—' }}
            <template v-if="port.speed > 0"> &middot; {{ port.speed }} Mbps</template>
          </span>
        </div>
      </div>
    </div>

    <div class="diag-section">
      <div class="diag-grid">
        <div class="diag-item" v-if="device.firmware"><span class="diag-label">Firmware ACS</span><code class="text-caption">{{ device.firmware }}</code></div>
        <div class="diag-item" v-if="device.ssid"><span class="diag-label">SSID</span>{{ device.ssid }}</div>
        <div class="diag-item" v-if="device.uptime"><span class="diag-label">Uptime</span>{{ formatUptime(device.uptime) }}</div>
      </div>
    </div>
    </div>

    <div class="diag-section">
      <div class="diag-section-title collapse-head" @click="showHosts = !showHosts">
        <q-icon :name="showHosts ? 'expand_more' : 'chevron_right'" size="18px" class="q-mr-xs" />
        <q-icon name="devices" size="16px" class="q-mr-xs" />
        Appareils connectés
        <span v-if="hosts" class="text-caption text-grey-5 q-ml-xs">({{ hosts.total }})</span>
        <q-btn flat dense size="sm" icon="refresh" class="q-ml-sm" :loading="hostsLoading" @click.stop="loadHosts(true)" title="Rafraichir la liste" />
      </div>
      <div v-show="showHosts">
      <div v-if="hostsLoading" class="text-caption text-grey-5 q-py-xs">
        <q-spinner size="14px" class="q-mr-xs" /> Interrogation de l'equipement...
      </div>
      <div v-else-if="hosts?.hosts.length">
        <div v-for="group in groupedHosts" :key="group.nodeId" class="node-group">
          <div class="node-group-header" @click="toggleNode(group.nodeId)">
            <q-icon :name="expandedNodes[group.nodeId] ? 'expand_more' : 'chevron_right'" size="18px" class="node-chevron" />
            <q-badge :color="group.active ? 'green' : 'red'" class="q-mr-xs" style="padding: 2px 5px;" />
            <q-icon :name="group.nodeId === '_wired' ? 'cable' : 'router'" size="14px" class="q-mr-xs" :color="group.nodeId === '_wired' ? 'grey-6' : 'blue-6'" />
            <span class="text-weight-medium">{{ group.label }}</span>
            <q-badge :color="group.active ? 'blue-1' : 'grey-3'" :text-color="group.active ? 'blue-8' : 'grey-6'" class="q-ml-xs" style="font-size:0.7rem;">{{ group.clients.length }}</q-badge>
            <span class="text-caption text-grey-5 q-ml-auto" v-if="group.ip">{{ group.ip }}</span>
            <span class="text-caption text-grey-5 q-ml-xs" v-if="group.mac">&middot; {{ group.mac }}</span>
          </div>
          <div v-show="expandedNodes[group.nodeId]">
            <table class="hosts-table">
              <thead><tr><th>Signal</th><th>Nom</th><th>IP</th><th>MAC</th><th>Connexion</th><th>Bail</th></tr></thead>
              <tbody>
                <tr v-for="h in group.clients" :key="h.id" :class="{ 'text-grey-5': !h.active }">
                  <td class="text-caption">
                    <span v-if="h.signal != null" :style="{ color: signalColor(h.signal) }">
                      <q-icon name="signal_cellular_alt" size="12px" :style="{ color: signalColor(h.signal) }" />
                      {{ formatSignal(h.signal) }}
                    </span>
                    <span v-else>—</span>
                  </td>
                  <td class="text-weight-medium">{{ h.name || '—' }}</td>
                  <td><code class="text-caption">{{ h.ip || '—' }}</code></td>
                  <td><code class="text-caption">{{ h.mac || '—' }}</code></td>
                  <td class="text-caption">{{ h.band || h.connType || '—' }}</td>
                  <td class="text-caption">{{ h.leaseRemaining != null ? formatLease(h.leaseRemaining) : '—' }}</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </div>
      <div v-else-if="hosts" class="text-caption text-grey-5 q-py-xs">Aucun appareil</div>
      </div>
    </div>
  </div>
  <div v-else-if="doc.serial_number && !device" class="q-mt-md text-caption text-grey-5">
    <q-spinner size="14px" class="q-mr-xs" v-if="deviceLoading" /> Chargement diagnostic ACS...
  </div>

  <!-- ── Diagnostic modem avancé (modem-bridge / Playwright) — DÉSACTIVÉ « pour le moment » : trop lourd (~15 s).
       Réactiver : remettre v-if="managementIp || props.doc?.serial_number". Le diagnostic GenieACS/OLT + liste d'appareils suffisent. ── -->
  <div v-if="false" class="q-mt-md">
    <div class="info-block-title">
      <q-icon name="wifi_find" size="16px" class="q-mr-xs" />
      Diagnostic modem
      <q-badge v-if="wifiDiag?.modemType" :label="modemTypeLabel(wifiDiag.modemType)" class="q-ml-xs" :color="modemTypeColor(wifiDiag.modemType)" dense />
      <q-btn flat dense size="sm" :icon="wifiDiag ? 'refresh' : 'play_arrow'" class="q-ml-sm"
        :label="wifiDiag ? 'Relancer' : 'Diagnostic'" :loading="wifiDiagLoading"
        @click="runWifiDiagnostic" color="primary" />
      <span v-if="wifiDiag" class="text-caption text-grey-5 q-ml-sm">{{ wifiDiag.durationMs }}ms</span>
    </div>

    <div v-if="wifiDiagLoading && !wifiDiag" class="text-caption text-grey-5 q-py-sm">
      <q-spinner size="14px" class="q-mr-xs" /> Connexion au modem via Playwright (~15s)...
    </div>
    <div v-if="wifiDiagError" class="text-caption text-negative q-py-xs">{{ wifiDiagError }}</div>

    <template v-if="wifiDiag">
      <!-- Wired equipment detection banner -->
      <div v-if="wifiDiag.wiredEquipment?.length" class="adv-issue adv-issue--info q-mb-sm">
        <div class="adv-issue-header">
          <q-icon name="router" size="16px" />
          <span class="text-weight-bold">{{ wifiDiag.wiredEquipment.length }} repeteur(s) mesh detecte(s)</span>
        </div>
        <div v-for="eq in wifiDiag.wiredEquipment" :key="eq.mac" class="q-ml-md q-mt-xs text-caption">
          <q-icon name="router" size="12px" class="q-mr-xs" color="info" />
          <span class="text-weight-medium">{{ eq.hostname || eq.model }}</span>
          &mdash; {{ eq.ip }} ({{ eq.mac }})
          <q-badge :color="eq.type === 'mesh_repeater' ? 'blue-2' : 'orange-2'" :text-color="eq.type === 'mesh_repeater' ? 'blue-9' : 'orange-9'" class="q-ml-xs" style="font-size:0.6rem;">
            {{ eq.type === 'mesh_repeater' ? 'Confirme' : 'Probable' }}
          </q-badge>
        </div>
      </div>

      <!-- Ethernet ports (enhanced) -->
      <div v-if="wifiDiag.ethernetPorts?.length" class="diag-section">
        <div class="diag-section-title"><q-icon name="cable" size="16px" class="q-mr-xs" />Ports Ethernet</div>
        <div class="diag-grid">
          <div v-for="port in wifiDiag.ethernetPorts" :key="port.port" class="diag-item">
            <span class="diag-label">{{ port.label }}</span>
            <span :class="port.status === 'Up' ? 'text-positive' : 'text-grey-6'">
              {{ port.status || '—' }}
              <template v-if="port.speed > 0"> &middot; {{ port.speed }} Mbps</template>
              <template v-if="port.stats && port.status === 'Up'">
                &middot; {{ formatBytes(port.stats.txBytes) }}↑ {{ formatBytes(port.stats.rxBytes) }}↓
              </template>
            </span>
          </div>
        </div>
      </div>

      <!-- DHCP Leases -->
      <div v-if="wifiDiag.dhcpLeases?.length" class="diag-section">
        <div class="diag-section-title">
          <q-icon name="dns" size="16px" class="q-mr-xs" />
          Baux DHCP ({{ wifiDiag.dhcpLeases.length }})
        </div>
        <table class="hosts-table">
          <thead><tr><th>Nom</th><th>IP</th><th>MAC</th><th>Bail</th><th>Type</th></tr></thead>
          <tbody>
            <tr v-for="l in wifiDiag.dhcpLeases" :key="l.mac">
              <td class="text-weight-medium">{{ l.hostname || '—' }}</td>
              <td><code class="text-caption">{{ l.ip }}</code></td>
              <td><code class="text-caption">{{ l.mac }}</code></td>
              <td class="text-caption">{{ l.expiry != null ? formatLease(l.expiry) : '—' }}</td>
              <td>
                <q-badge v-if="l.isMeshRepeater" color="blue-1" text-color="blue-9" style="font-size:0.6rem;">Mesh</q-badge>
                <q-badge v-else-if="l.isWired" color="grey-3" text-color="grey-8" style="font-size:0.6rem;">Filaire</q-badge>
                <q-badge v-else color="green-2" text-color="green-9" style="font-size:0.6rem;">WiFi</q-badge>
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <!-- Issues panel -->
      <div v-if="wifiDiag.issues.length" class="adv-issues q-mb-sm">
        <div v-for="(issue, i) in wifiDiag.issues" :key="i" class="adv-issue" :class="'adv-issue--' + issue.severity">
          <div class="adv-issue-header">
            <q-icon :name="issue.severity === 'critical' ? 'error' : issue.severity === 'warning' ? 'warning' : 'info'" size="16px" />
            <span class="text-weight-bold">{{ issue.message }}</span>
          </div>
          <div class="adv-issue-detail">{{ issue.detail }}</div>
          <div class="adv-issue-action"><q-icon name="lightbulb" size="12px" class="q-mr-xs" />{{ issue.action }}</div>
        </div>
      </div>
      <div v-else class="adv-ok q-mb-sm">
        <q-icon name="check_circle" size="16px" color="positive" class="q-mr-xs" />
        Aucun probleme detecte — reseau en bon etat.
      </div>

      <!-- WAN IPs -->
      <div v-if="wifiDiag.wanIPs.length" class="diag-section">
        <div class="diag-section-title"><q-icon name="public" size="16px" class="q-mr-xs" />Adresses WAN (ONU)</div>
        <div class="ip-grid">
          <div v-for="w in wifiDiag.wanIPs" :key="w.ip" class="ip-item">
            <span class="ip-role" :class="'ip-role--' + w.role">{{ wanRoleLabel(w.role) }}</span>
            <code class="ip-addr">{{ w.ip }}</code>
            <span class="text-caption text-grey-5">/{{ maskToCidr(w.mask) }} {{ w.type }}</span>
          </div>
        </div>
      </div>

      <!-- Mesh topology -->
      <div v-if="wifiDiag.meshNodes.length > 1" class="diag-section">
        <div class="diag-section-title"><q-icon name="hub" size="16px" class="q-mr-xs" />Topologie Mesh ({{ wifiDiag.meshNodes.length }} noeuds)</div>
        <div class="adv-mesh-nodes">
          <div v-for="node in wifiDiag.meshNodes" :key="node.mac" class="adv-mesh-node">
            <div class="adv-mesh-header">
              <q-badge :color="node.active ? 'green' : 'red'" style="padding: 2px 5px;" />
              <span class="text-weight-bold q-ml-xs">{{ node.hostname }}</span>
              <span class="text-caption text-grey-6 q-ml-xs">{{ node.model }}</span>
              <q-badge v-if="node.isController" outline color="info" class="q-ml-xs" style="font-size:0.6rem;">Routeur</q-badge>
            </div>
            <div class="adv-mesh-stats">
              <span v-if="!node.isController" class="adv-mesh-stat">
                <span class="diag-label">Backhaul</span>
                <span :style="{ color: backhaulColor(node.backhaul.signal) }">
                  <q-icon name="signal_cellular_alt" size="12px" /> {{ node.backhaul.signal }}/255
                </span>
                &middot; {{ node.backhaul.linkRate }} Mbps
                &middot; {{ node.backhaul.utilization }}% util.
              </span>
              <span class="adv-mesh-stat"><span class="diag-label">CPU</span> {{ node.cpu }}%</span>
              <span class="adv-mesh-stat"><span class="diag-label">Uptime</span> {{ formatUptime(node.uptime) }}</span>
              <span v-if="node.speedDown || node.speedUp" class="adv-mesh-stat">
                <span class="diag-label">Debit</span> ↓{{ node.speedDown }} ↑{{ node.speedUp }} Mbps
              </span>
            </div>
          </div>
        </div>
      </div>

      <!-- Radios -->
      <div v-if="wifiDiag.radios.length" class="diag-section">
        <div class="diag-section-title"><q-icon name="settings_input_antenna" size="16px" class="q-mr-xs" />Radios</div>
        <div class="diag-grid">
          <template v-for="r in wifiDiag.radios" :key="r.band">
            <div class="diag-item">
              <span class="diag-label">{{ r.band }}</span>
              <span :class="r.status === 'Up' ? 'text-positive' : 'text-negative'">
                Ch {{ r.channel }} &middot; {{ r.bandwidth }} &middot; {{ r.standard }}
              </span>
            </div>
            <div class="diag-item">
              <span class="diag-label">{{ r.band }} config</span>
              <span>Puissance {{ r.txPower }}% &middot; Canal {{ r.autoChannel ? 'auto' : 'fixe' }}</span>
            </div>
          </template>
        </div>
      </div>

      <!-- Connected clients -->
      <div v-if="wifiDiag.clients.length" class="diag-section">
        <div class="diag-section-title">
          <q-icon name="devices" size="16px" class="q-mr-xs" />
          Clients WiFi ({{ wifiDiag.clients.filter(c => c.active).length }} connectes
          <span v-if="wifiDiag.clients.some(c => !c.active)" class="text-caption text-grey-5">
            / {{ wifiDiag.clients.filter(c => !c.active).length }} hors ligne
          </span>)
        </div>
        <table class="hosts-table adv-clients-table">
          <thead>
            <tr>
              <th>Signal</th>
              <th>Nom</th>
              <th>Noeud</th>
              <th>Bande</th>
              <th>Debit ↓/↑</th>
              <th>Perte</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="c in wifiDiag.clients" :key="c.mac" :class="{ 'text-grey-5 adv-inactive-row': !c.active }">
              <td>
                <template v-if="c.active">
                  <div class="adv-signal-bar">
                    <div class="adv-signal-fill" :style="{ width: signalPercent(c.signal) + '%', background: signalColor(c.signal) }" />
                  </div>
                  <span class="text-caption" :style="{ color: signalColor(c.signal) }">{{ c.signal > 0 ? c.signal : '?' }}</span>
                </template>
                <q-icon v-else name="wifi_off" size="14px" color="grey-4" />
              </td>
              <td>
                <span class="text-weight-medium">{{ c.hostname || '—' }}</span>
                <div class="text-caption text-grey-5">{{ c.ip || 'hors ligne' }}</div>
              </td>
              <td class="text-caption">{{ c.meshNode || '—' }}</td>
              <td class="text-caption">
                <q-badge :color="c.band === '5GHz' ? 'green-2' : c.band === '2.4GHz' ? 'blue-2' : 'grey-3'"
                  :text-color="c.band === '5GHz' ? 'green-9' : c.band === '2.4GHz' ? 'blue-9' : 'grey-7'"
                  style="font-size: 0.65rem;">{{ c.band || c.standard || '?' }}</q-badge>
              </td>
              <td class="text-caption">
                {{ Math.round(c.linkDown / 1000) }}/{{ Math.round(c.linkUp / 1000) }} Mbps
              </td>
              <td>
                <span v-if="c.lossPercent > 10" class="text-negative text-weight-bold">{{ c.lossPercent }}%</span>
                <span v-else-if="c.lossPercent > 5" class="text-warning text-weight-bold">{{ c.lossPercent }}%</span>
                <span v-else-if="c.lossPercent > 0" class="text-caption text-grey-6">{{ c.lossPercent }}%</span>
                <span v-else class="text-caption text-grey-5">—</span>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </template>
  </div>

  <div v-if="doc.olt_name" class="q-mt-md">
    <div class="info-block-title">Information OLT</div>
    <div class="modal-field-grid">
      <div class="mf"><span class="mf-label">OLT</span>{{ doc.olt_name }}</div>
      <div class="mf"><span class="mf-label">IP OLT</span><code>{{ doc.olt_ip }}</code></div>
      <div class="mf"><span class="mf-label">Slot / Port</span>{{ doc.olt_slot }} / {{ doc.olt_port }}</div>
      <div class="mf"><span class="mf-label">ONT ID</span>{{ doc.olt_ontid }}</div>
    </div>
    <div class="mf" v-if="doc.installation_date"><span class="mf-label">Installe le</span>{{ doc.installation_date }}</div>
    <div class="mf" v-if="doc.warranty_end"><span class="mf-label">Fin garantie</span>{{ doc.warranty_end }}</div>
    <div class="mf" v-if="doc.service_location"><span class="mf-label">Adresse</span>{{ doc.service_location }}</div>
    <div class="mf" v-if="doc.subscription"><span class="mf-label">Abonnement</span>{{ doc.subscription }}</div>
  </div>

  <!-- Actions cycle-de-vie ONU (plan→confirm→run : suspendre/rétablir/redémarrer/vitesse/remplacer/retirer) — remplace le
       « Do Stuff » de F. GATÉ manage_settings (écritures réseau : suspendre coupe le client). Bloc AUTONOME (n'exige pas olt_name). -->
  <div v-if="doc.serial_number && doc.olt_ip && can('manage_settings')" class="q-mt-md">
    <div class="info-block-title">Actions appareil (réseau)</div>
    <OnuActionsPanel :serial="doc.serial_number" :olt="doc.olt_ip" :status="doc.status" @changed="onOnuChanged" />
  </div>

  <!-- Port Neighbor Context — shows health of the same OLT port -->
  <div v-if="portCtx && portCtx.total > 0" class="q-mt-md">
    <div class="info-block-title">
      <q-icon name="hub" size="16px" class="q-mr-xs" />
      Voisinage port
      <q-badge v-if="portCtx.is_mass_outage" color="red" class="q-ml-sm">Panne secteur</q-badge>
      <q-badge v-else-if="portCtx.is_warning" color="orange" class="q-ml-sm">Alerte</q-badge>
      <q-badge v-else color="green" class="q-ml-sm">Normal</q-badge>
    </div>
    <div class="port-ctx-bar q-mt-xs">
      <div class="port-ctx-fill port-ctx-fill--online" :style="{ width: (portCtx.online / portCtx.total * 100) + '%' }" />
      <div class="port-ctx-fill port-ctx-fill--offline" :style="{ width: (portCtx.offline / portCtx.total * 100) + '%' }" />
    </div>
    <div class="port-ctx-legend text-caption q-mt-xs">
      <span class="text-positive text-weight-bold">{{ portCtx.online }}</span> en ligne
      <span v-if="portCtx.offline > 0" class="q-ml-md text-negative text-weight-bold">{{ portCtx.offline }}</span>
      <span v-if="portCtx.offline > 0"> hors ligne</span>
      <span class="text-grey-6 q-ml-md">/ {{ portCtx.total }} sur le port</span>
    </div>
    <div v-if="portCtx.offline > 0" class="port-ctx-neighbors q-mt-xs">
      <div v-for="n in portCtx.neighbors.filter(x => !x.is_self && x.status !== 'online')" :key="n.serial_prefix"
        class="port-ctx-neighbor text-caption">
        <q-icon name="circle" size="8px" color="red" class="q-mr-xs" />
        {{ n.serial_prefix }} — {{ n.cause || 'inconnu' }}
        <span v-if="n.distance_m" class="text-grey-5 q-ml-xs">{{ n.distance_m }}m</span>
      </div>
    </div>
  </div>
  <div class="q-mt-md">
    <div class="info-block-title">Acces distant</div>
    <div class="info-row q-py-xs"><span class="mf-label">Utilisateur</span>
      <InlineField :value="doc.login_user" field="login_user" doctype="Service Equipment" :docname="docName"
        placeholder="—" @saved="v => doc.login_user = v.value" />
    </div>
  </div>
  <div class="q-mt-md">
    <div class="info-block-title">Notes</div>
    <InlineField :value="doc.notes" field="notes" doctype="Service Equipment" :docname="docName"
      type="textarea" placeholder="Ajouter des notes..." @saved="v => doc.notes = v.value" />
  </div>
  <div v-if="doc.move_log?.length" class="q-mt-md">
    <div class="info-block-title">Historique ({{ doc.move_log.length }})</div>
    <div v-for="m in doc.move_log" :key="m.idx" class="info-row q-py-xs text-caption">
      {{ m.date }} --- {{ m.from_location || '?' }} &rarr; {{ m.to_location || '?' }} {{ m.reason ? '(' + m.reason + ')' : '' }}
    </div>
  </div>

  <div v-if="can('manage_settings')" class="q-mt-lg q-pt-md" style="border-top: 1px solid #f1f5f9">
    <q-btn flat dense no-caps color="negative" icon="delete_outline" label="Supprimer cet équipement"
      @click="confirmDelete" :loading="deleting" class="text-caption" />
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useQuasar } from 'quasar'
import InlineField from 'src/components/shared/InlineField.vue'
import OnuActionsPanel from './modules/OnuActionsPanel.vue'
import { usePermissions } from 'src/composables/usePermissions'
import { useDeviceStatus } from 'src/composables/useDeviceStatus'
import { useModemDiagnostic } from 'src/composables/useModemDiagnostic'
import { deleteDoc, updateDoc } from 'src/api/erp'
import { HUB_URL } from 'src/config/hub'

const props = defineProps({ doc: { type: Object, required: true }, docName: String })
const emit = defineEmits(['deleted'])

const $q = useQuasar()
const { can } = usePermissions() // écritures réseau ONU + suppression = gated (manage_settings)
const { fetchStatus, fetchOltStatus, fetchPortContext, getDevice, isOnline, combinedStatus, signalQuality, refreshDeviceParams, fetchHosts, loading: deviceLoading } = useDeviceStatus()
const { fetchDiagnostic, fetchDiagnosticAuto, loading: wifiDiagLoading, error: wifiDiagError, data: wifiDiag } = useModemDiagnostic()
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)
// Sans-fil (airOS/Cambium) : pas de fibre (olt_ip), et marque/modèle/type = radio. Le CPE n'est ni dans GenieACS ni
// dans le poller OLT → signal live via webhook n8n (comme la fibre). Heuristique volontairement stricte (marque radio).
const isWireless = computed(() => {
  const d = props.doc
  if (d.olt_ip) return false
  const s = ((d.brand || '') + ' ' + (d.model || '') + ' ' + (d.equipment_type || '')).toLowerCase()
  return /ubiquit|airmax|litebeam|nanostation|nanobridge|rocket|powerbeam|bullet|cambium|airos|\bap\b|sans.?fil|wireless/.test(s)
})
const airos = ref(null); const airosLoading = ref(false)
async function fetchAiros () {
  airos.value = null; airosLoading.value = true
  try {
    const r = await fetch(`${HUB_URL}/collab/airos-signal?serial=${encodeURIComponent(props.doc.serial_number || '')}&ip=${encodeURIComponent(props.doc.ip_address || '')}`)
    airos.value = await r.json()
    // Le hub a auto-synchronisé la MAC depuis RADIUS → refléter localement (l'InlineField Identité suit).
    if (airos.value && airos.value.macSynced && airos.value.radiusMac) props.doc.mac_address = airos.value.radiusMac
  } catch (e) { airos.value = { ok: false, error: e.message } } finally { airosLoading.value = false }
}
// État RAPIDE au chargement (sans-fil) : RADIUS seul (en ligne / depuis / IP), sans le status.cgi lent. Auto-sync MAC aussi.
const wStatus = ref(null)
async function loadWStatus () {
  if (!isWireless.value || (!props.doc.serial_number && !props.doc.ip_address)) return
  try {
    const r = await fetch(`${HUB_URL}/collab/airos-status?serial=${encodeURIComponent(props.doc.serial_number || '')}&ip=${encodeURIComponent(props.doc.ip_address || '')}`)
    wStatus.value = await r.json()
    if (wStatus.value && wStatus.value.macSynced && wStatus.value.radiusMac) props.doc.mac_address = wStatus.value.radiusMac
  } catch (e) { wStatus.value = null }
}

// MAC RADIUS (WPA2) = source autoritaire, s'auto-MàJ au remplacement d'équipement. Si elle diffère de la MAC
// enregistrée → on le signale + bouton 1-clic pour l'aligner (updateDoc). La saisie manuelle reste dispo (InlineField).
const normMacUi = (m) => String(m || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase()
const macMismatch = computed(() => { const rm = airos.value && airos.value.radiusMac; return !!(rm && normMacUi(rm) && normMacUi(rm) !== normMacUi(props.doc.mac_address)) })
const macSyncing = ref(false)
async function syncMacFromRadius () {
  const rm = airos.value && airos.value.radiusMac; if (!rm) return
  macSyncing.value = true
  try { await updateDoc('Service Equipment', props.docName, { mac_address: rm }); props.doc.mac_address = rm; $q.notify({ type: 'positive', message: 'MAC mise à jour depuis RADIUS : ' + rm }) }
  catch (e) { $q.notify({ type: 'negative', message: 'Échec MAJ MAC : ' + (e.message || e) }) } finally { macSyncing.value = false }
}

// Débit Ethernet client EN DIRECT (eth0 du CPE) : on sonde /collab/airos-signal et on calcule (Δoctets/Δs)×8.
// eth0 rx = upload (trafic DU client), tx = download (trafic VERS le client). Re-planifié (pas setInterval) → 0 chevauchement.
const ethLive = ref(false); const ethRates = ref([]); const ethErr = ref('')
let ethTimer = null; let ethPrev = null
async function ethPoll () {
  try {
    const r = await fetch(`${HUB_URL}/collab/airos-signal?serial=${encodeURIComponent(props.doc.serial_number || '')}&ip=${encodeURIComponent(props.doc.ip_address || '')}`)
    const d = await r.json()
    if (!d || !d.ok || d.ethRx == null) { ethErr.value = (d && d.error) || 'débit indisponible sur ce CPE'; return }
    ethErr.value = ''
    const cur = { ts: d.ts || Date.now(), rx: d.ethRx, tx: d.ethTx }
    if (ethPrev) {
      const sec = (cur.ts - ethPrev.ts) / 1000
      const delta = (a, b) => { if (a == null || b == null) return null; let x = b - a; if (x < 0) x = 0; return sec > 0 ? (x / sec) * 8 : null }
      ethRates.value = [...ethRates.value, { dl: delta(ethPrev.tx, cur.tx), ul: delta(ethPrev.rx, cur.rx), ts: cur.ts }].slice(-30)
    }
    ethPrev = cur
  } catch (e) { ethErr.value = 'erreur de mesure' }
}
function scheduleEth () { ethTimer = setTimeout(async () => { await ethPoll(); if (ethLive.value) scheduleEth() }, 6000) }
function startEthLive () { ethLive.value = true; ethErr.value = ''; ethRates.value = []; ethPrev = null; ethPoll().then(() => { if (ethLive.value) scheduleEth() }) }
function stopEthTimer () { if (ethTimer) { clearTimeout(ethTimer); ethTimer = null } }
function stopEthLive () { stopEthTimer(); ethLive.value = false }
function toggleEthLive () { ethLive.value ? stopEthLive() : startEthLive() }
const ethNow = computed(() => ethRates.value.length ? ethRates.value[ethRates.value.length - 1] : null)
const ethMax = computed(() => { let m = 1; for (const r of ethRates.value) { if (r.dl > m) m = r.dl; if (r.ul > m) m = r.ul } return m })
function ethLine (key) { const r = ethRates.value; if (r.length < 2) return ''; const max = ethMax.value, W = 220, H = 38, n = r.length; return r.map((p, i) => `${((i / (n - 1)) * W).toFixed(1)},${(H - ((p[key] || 0) / max) * H).toFixed(1)}`).join(' ') }

// Coloration du signal par INTERVALLE (parité F). Sans-fil (dBm airOS) : ≥-60 bon, -70 correct, -78 faible, < critique.
function wifiSigColor (s) { if (s == null) return '#64748b'; if (s >= -60) return '#16a34a'; if (s >= -70) return '#65a30d'; if (s >= -78) return '#d97706'; return '#dc2626' }
// Fibre (Rx optique dBm) : plage saine -8..-25 vert ; -25..-28 orange ; < -28 (ou > -8 sur-puissance) rouge.
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

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 a = airos.value
  if (a && a.ok) {
    if (a.model) L.push('CPE sans-fil : ' + a.model + (a.version ? ' (' + a.version + ')' : ''))
    if (a.signal != null) L.push('Signal : ' + a.signal + ' dBm' + (a.signalAp != null ? ' (AP ' + a.signalAp + ')' : ''))
    if (a.ccq != null) L.push('CCQ : ' + a.ccq + ' %')
    if (a.txRate || a.rxRate) L.push('Débit TX/RX : ' + (a.txRate || '—') + ' / ' + (a.rxRate || '—'))
    if (a.capDown != null || a.capUp != null) L.push('Capacité airMAX : DL ' + (a.capDown != null ? a.capDown : '—') + ' / UL ' + (a.capUp != null ? a.capUp : '—') + ' Mbps')
  }
  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
})

async function runWifiDiagnostic() {
  const serial = props.doc?.serial_number
  // Try auto-fetch first (credentials resolved server-side)
  if (serial) {
    const result = await fetchDiagnosticAuto(serial)
    if (result) return
  }
  // Fallback: manual password entry on any auto-fetch failure
  const ip = managementIp.value || props.doc?.ip_address
  if (!ip) return
  const errMsg = wifiDiagError.value || ''
  $q.dialog({
    title: 'Mot de passe modem',
    message: errMsg
      ? `Echec auto (${errMsg.substring(0, 80)}) — entrer le mot de passe pour ${ip}`
      : `Entrer le mot de passe superadmin pour ${ip}`,
    prompt: { model: '', type: 'password', filled: true },
    cancel: { flat: true, label: 'Annuler' },
    ok: { label: 'Lancer', color: 'primary' },
    persistent: false,
  }).onOk(pass => {
    if (pass) fetchDiagnostic(ip, pass)
  })
}

const MODEM_TYPE_LABELS = {
  tplink_xx230v: 'TP-Link',
  raisecom_boa: 'Raisecom 803-W',
  raisecom_php: 'Raisecom 803-WS2',
}
const MODEM_TYPE_COLORS = {
  tplink_xx230v: 'teal',
  raisecom_boa: 'orange',
  raisecom_php: 'deep-purple',
}
const modemTypeLabel = (type) => MODEM_TYPE_LABELS[type] || type
const modemTypeColor = (type) => MODEM_TYPE_COLORS[type] || 'grey'

const wanRoleLabel = (role) => ROLE_LABELS[role] || role

function maskToCidr(mask) {
  if (!mask) return '?'
  return mask.split('.').reduce((c, o) => c + (parseInt(o) >>> 0).toString(2).replace(/0/g, '').length, 0)
}

function signalPercent(raw) {
  if (!raw || raw <= 0) return 0
  return raw >= 0 ? Math.round((raw / 255) * 100) : Math.round(Math.max(0, Math.min(100, ((raw + 90) / 60) * 100)))
}

function backhaulColor(signal) {
  if (signal >= 100) return '#4ade80'
  if (signal >= 80) return '#a3e635'
  if (signal >= 60) return '#fbbf24'
  return '#f87171'
}

function formatBytes(bytes) {
  if (!bytes || bytes <= 0) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB']
  const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
  return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i]
}

function confirmDelete () {
  $q.dialog({
    title: 'Supprimer cet équipement ?',
    message: `${props.doc.serial_number || props.docName} sera supprimé définitivement.`,
    cancel: { flat: true, label: 'Annuler' },
    ok: { color: 'negative', label: 'Supprimer', flat: true },
    persistent: true,
  }).onOk(async () => {
    deleting.value = true
    try {
      await deleteDoc('Service Equipment', props.docName)
      $q.notify({ type: 'positive', message: 'Équipement supprimé' })
      emit('deleted', props.docName)
    } catch (e) {
      $q.notify({ type: 'negative', message: 'Erreur: ' + e.message })
    } finally {
      deleting.value = false
    }
  })
}
const hosts = ref(null)
const hostsLoading = ref(false)
const expandedNodes = ref({})

const equipFields = [
  { field: 'equipment_type', label: 'Type', props: { type: 'select', options: ['ONT', 'Router', 'Switch', 'AP', 'OLT', 'Décodeur', 'Modem', 'Autre'] } },
  { field: 'status', label: 'Statut', props: { type: 'select', options: ['Active', 'Inactive', 'En stock', 'Défectueux', 'Retourné'] } },
  { field: 'brand', label: 'Marque' },
  { field: 'model', label: 'Modele' },
  { field: 'serial_number', label: 'N serie' },
  { field: 'mac_address', label: 'MAC' },
  { field: 'ip_address', label: 'IP' },
  { field: 'firmware_version', label: 'Firmware' },
  { field: 'ownership', label: 'Propriete', props: { type: 'select', options: ['Client', 'Compagnie', 'Location'] } },
]

const toggleNode = (nodeId) => { expandedNodes.value[nodeId] = !expandedNodes.value[nodeId] }
const device = computed(() => props.doc.serial_number ? getDevice(props.doc.serial_number) : null)
const online = computed(() => isOnline(props.doc.serial_number))
const status = computed(() => combinedStatus(props.doc.serial_number))
const statusBadgeColor = computed(() => {
  if (status.value.source === 'unknown') return 'amber'
  return status.value.online === true ? 'green' : status.value.online === false ? 'red' : 'grey'
})

const rxColor = computed(() => {
  const q = signalQuality(props.doc.serial_number)
  return q === 'excellent' ? '#4ade80' : q === 'good' ? '#a3e635' : q === 'fair' ? '#fbbf24' : '#f87171'
})

const sortedInterfaces = computed(() => {
  if (!device.value?.interfaces) return []
  const order = { internet: 0, management: 1, service: 2, lan: 3, unknown: 4 }
  return [...device.value.interfaces].sort((a, b) => (order[a.role] ?? 9) - (order[b.role] ?? 9))
})

const ROLE_LABELS = { internet: 'Internet', management: 'Gestion', service: 'Service', lan: 'LAN', unknown: '?' }
const roleLabel = (role) => ROLE_LABELS[role] || role

const wifiRadios = computed(() => {
  if (!device.value?.wifi) return []
  const w = device.value.wifi
  const radios = []
  if (w.radio1?.status) radios.push({ ...w.radio1, label: '2.4 GHz' })
  if (w.radio2?.status) radios.push({ ...w.radio2, label: '5 GHz' })
  if (w.radio3?.status) radios.push({ ...w.radio3, label: '6 GHz' })
  return radios
})

const ethernetPorts = computed(() => {
  if (!device.value?.ethernet) return []
  const e = device.value.ethernet, ports = []
  if (e.port1) ports.push({ ...e.port1, label: 'Port 1' })
  if (e.port2) ports.push({ ...e.port2, label: 'Port 2' })
  return ports
})

const groupedHosts = computed(() => {
  if (!hosts.value?.hosts) return []
  const meshNodes = device.value?.mesh || []
  const groups = new Map()
  for (const node of meshNodes) {
    groups.set(node.name || node.id, { nodeId: node.name || node.id, label: node.name || 'Node ' + node.id, active: node.active, ip: node.ip, mac: node.mac, clients: [] })
  }
  const wiredGroup = { nodeId: '_wired', label: 'Filaire / Autre', active: true, ip: null, mac: null, clients: [] }
  for (const h of hosts.value.hosts) {
    if (h.isMeshNode) continue
    const nodeName = h.attachedNode
    if (nodeName && groups.has(nodeName)) groups.get(nodeName).clients.push(h)
    else if (nodeName) groups.set(nodeName, { nodeId: nodeName, label: nodeName, active: true, ip: null, mac: null, clients: [h] })
    else wiredGroup.clients.push(h)
  }
  const result = [...groups.values()]
  if (wiredGroup.clients.length) result.push(wiredGroup)
  return result
})

const SIGNAL_STEPS = [
  { pct: 0, color: 'hsl(0, 100%, 40%)' }, { pct: 10, color: 'hsl(6, 100%, 42%)' },
  { pct: 20, color: 'hsl(12, 100%, 44%)' }, { pct: 30, color: 'hsl(20, 95%, 45%)' },
  { pct: 40, color: 'hsl(30, 100%, 45%)' }, { pct: 50, color: 'hsl(42, 100%, 45%)' },
  { pct: 60, color: 'hsl(54, 100%, 40%)' }, { pct: 70, color: 'hsl(72, 90%, 38%)' },
  { pct: 80, color: 'hsl(90, 80%, 36%)' }, { pct: 90, color: 'hsl(105, 75%, 35%)' },
  { pct: 100, color: 'hsl(120, 70%, 35%)' },
]

function signalColor (raw) {
  if (raw == null) return '#9ca3af'
  const val = parseFloat(raw)
  if (isNaN(val)) return '#9ca3af'
  const pct = val >= 0 ? Math.round((val / 255) * 100) : Math.round(Math.max(0, Math.min(100, ((val + 90) / 60) * 100)))
  return SIGNAL_STEPS[Math.min(10, Math.max(0, Math.floor(pct / 10)))].color
}

function formatSignal (raw) {
  if (raw == null) return null
  const val = parseFloat(raw)
  return isNaN(val) ? null : val >= 0 ? Math.round((val / 255) * 100) + '%' : val + ' dBm'
}

function timeAgo (dateStr) {
  if (!dateStr) return ''
  const mins = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000)
  if (mins < 1) return "a l'instant"
  if (mins < 60) return `il y a ${mins}m`
  const hrs = Math.floor(mins / 60)
  return hrs < 24 ? `il y a ${hrs}h` : `il y a ${Math.floor(hrs / 24)}j`
}

function formatLease (seconds) {
  if (seconds == null || seconds <= 0) return 'expire'
  const h = Math.floor(seconds / 3600), m = Math.floor((seconds % 3600) / 60)
  return h > 0 ? `${h}h${m > 0 ? m + 'm' : ''}` : `${m}m`
}

async function loadHosts (refresh = false) {
  if (!props.doc.serial_number) return
  hostsLoading.value = true
  try {
    hosts.value = await fetchHosts(props.doc.serial_number, refresh)
    if (hosts.value?.hosts) {
      const nodes = new Set()
      for (const h of hosts.value.hosts) nodes.add(h.attachedNode || '_wired')
      if (device.value?.mesh) for (const n of device.value.mesh) nodes.add(n.name || n.id)
      const exp = {}; for (const id of nodes) exp[id] = true
      expandedNodes.value = exp
    }
  } catch {}
  hostsLoading.value = false
}

function formatUptime (seconds) {
  if (!seconds) return '—'
  const d = Math.floor(seconds / 86400), h = Math.floor((seconds % 86400) / 3600), m = Math.floor((seconds % 3600) / 60)
  return d > 0 ? `${d}j ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`
}

// Épuré : sections denses repliées par défaut (l'essentiel est dans le résumé en tête).
const showIdentity = ref(false)
const showTech = ref(false)
const showHosts = ref(false)
const clientCount = computed(() => (hosts.value && hosts.value.total != null) ? hosts.value.total : (device.value?.wifi?.totalClients ?? 0))
const SIG_LBL = { excellent: 'excellent', good: 'bon', fair: 'faible', bad: 'critique' }
const signalLabel = computed(() => SIG_LBL[signalQuality(props.doc.serial_number)] || '')
const signalBadgeColor = computed(() => { const s = signalQuality(props.doc.serial_number); return (s === 'excellent' || s === 'good') ? 'green' : s === 'fair' ? 'orange' : s === 'bad' ? 'red' : 'grey' })
const oltLabel = computed(() => props.doc.olt_name || props.doc.olt_ip || 'OLT')
// Événements récents (register / dying-gasp / online-offline) — depuis nos device_events (le poller les journalise déjà).
const events = ref([])
async function loadEvents () {
  if (!props.doc.serial_number) return
  try { const r = await fetch(`${HUB_URL}/devices/events?serial=${encodeURIComponent(props.doc.serial_number)}&limit=8`); if (r.ok) events.value = await r.json() } catch (e) { /* */ }
}
const lastOutage = computed(() => { const o = events.value.find(e => e.event === 'offline'); return o ? (o.reason || 'coupure') : null })
function evIcon (e) { return e.event === 'offline' ? 'power_off' : e.event === 'online' ? 'check_circle' : 'fiber_manual_record' }
function evColor (e) { return e.event === 'offline' ? 'orange-7' : e.event === 'online' ? 'green-6' : 'grey-5' }
function evLabel (e) { return e.event === 'offline' ? ('Hors ligne' + (e.reason ? ' · ' + e.reason : '')) : e.event === 'online' ? 'En ligne' : (e.event || '') }
function evTime (e) { try { return new Date(e.created_at).toLocaleString('fr-CA', { dateStyle: 'short', timeStyle: 'short' }) } catch (x) { return '' } }
// Graphe : disponibilité + signal Rx récents, depuis l'historique device_cache (vraie data ; le débit Mbps façon F = source ES, plus tard).
const wanHistory = ref([])
async function loadWanHistory () {
  if (!props.doc.serial_number) return
  try { const r = await fetch(`${HUB_URL}/devices/cache?serial=${encodeURIComponent(props.doc.serial_number)}`); if (r.ok) { const d = await r.json(); wanHistory.value = (Array.isArray(d.history) ? d.history : []).slice(-48) } } catch (e) { /* */ }
}
const uptimePct = computed(() => { const h = wanHistory.value; if (!h.length) return null; return Math.round(h.filter(p => p.online).length / h.length * 100) })
const lastRx = computed(() => { for (let i = wanHistory.value.length - 1; i >= 0; i--) { if (wanHistory.value[i].rxPower != null) return wanHistory.value[i].rxPower + ' dBm' } return '—' })
function fmtTs (ts) { try { return new Date(ts).toLocaleString('fr-CA', { dateStyle: 'short', timeStyle: 'short' }) } catch (e) { return '' } }

// Débit WAN EN DIRECT : mirroir de raisecom_rcmg_bandwith.php de F. Le hub lit les compteurs
// d'octets Counter64 de l'OLT (Raisecom/tech-2) ; ici on échantillonne toutes les 5 s et on
// calcule le débit = (Δoctets / Δs) × 8 (façon F). À la demande seulement (charge OLT).
const wanLive = ref(false)
const wanLiveErr = ref('')        // '' | 'unsupported' | 'error' | autre raison
const wanRates = ref([])          // [{ dl, ul, ts }] bits/s, ~40 derniers points
let wanTimer = null
let wanPrev = null                // dernier échantillon brut pour le delta
const WAN_ERR_MSG = {
  unsupported_olt_type: 'Débit en direct indisponible — OLT TP-Link (tech 3). Seules les OLT Raisecom exposent ce compteur.',
  onu_not_resolved: 'ONU absent du sondage OLT en ce moment (OLT hors supervision ?). Débit en direct indisponible.',
  no_counter: 'Aucun compteur de débit pour cet ONU sur l’OLT.',
  snmp_unavailable: 'Sonde SNMP indisponible.',
}
const wanErrMsg = computed(() => WAN_ERR_MSG[wanLiveErr.value] || (wanLiveErr.value ? 'Mesure indisponible pour le moment.' : ''))
const fmtBps = (bps) => (bps == null) ? '—' : (bps >= 1e6 ? (bps / 1e6).toFixed(1) + ' Mbps' : (bps / 1e3).toFixed(0) + ' kbps')
const wanNow = computed(() => wanRates.value.length ? wanRates.value[wanRates.value.length - 1] : null)
const wanMax = computed(() => { let m = 1; for (const r of wanRates.value) { if (r.dl > m) m = r.dl; if (r.ul > m) m = r.ul } return m })
function wanLine (key) {
  const r = wanRates.value; if (r.length < 2) return ''
  const max = wanMax.value, W = 220, H = 38, n = r.length
  return r.map((p, i) => `${((i / (n - 1)) * W).toFixed(1)},${(H - ((p[key] || 0) / max) * H).toFixed(1)}`).join(' ')
}
async function wanPoll () {
  if (!props.doc.serial_number) return
  try {
    const r = await fetch(`${HUB_URL}/collab/wan-rate?serial=${encodeURIComponent(props.doc.serial_number)}`)
    if (!r.ok) { wanLiveErr.value = 'error'; stopWanTimer(); return }
    const d = await r.json()
    // Indisponible (OLT non Raisecom, ONU non résolu, pas de compteur) = état non transitoire → on affiche le motif et on ARRÊTE de sonder (pas de spinner infini).
    if (!d.available) { wanLiveErr.value = d.reason || 'error'; stopWanTimer(); return }
    wanLiveErr.value = ''
    const cur = { ts: d.ts, rx: d.rxOctets, tx: d.txOctets }
    if (wanPrev) {
      const sec = (cur.ts - wanPrev.ts) / 1000
      const delta = (a, b) => { if (a == null || b == null) return null; let x = b - a; if (x < 0) x = 0; return sec > 0 ? (x / sec) * 8 : null }
      wanRates.value = [...wanRates.value, { dl: delta(wanPrev.rx, cur.rx), ul: delta(wanPrev.tx, cur.tx), ts: cur.ts }].slice(-40)
    }
    wanPrev = cur
  } catch (e) { wanLiveErr.value = 'error'; stopWanTimer() }
}
function startWanLive () { wanLive.value = true; wanLiveErr.value = ''; wanRates.value = []; wanPrev = null; wanPoll(); wanTimer = setInterval(wanPoll, 3000) }
function stopWanTimer () { if (wanTimer) { clearInterval(wanTimer); wanTimer = null } }
function stopWanLive () { stopWanTimer(); wanLive.value = false; wanLiveErr.value = '' }
function toggleWanLive () { wanLive.value ? stopWanLive() : startWanLive() }
// Liste d'appareils au clic sur l'icône : on déplie → la version EN CACHE s'affiche vite (le hub sert le cache par défaut) + anneau de chargement pendant le rafraîchissement.
function openHosts () { showHosts.value = true; if (!hosts.value && device.value) loadHosts() }

// Une action ONU a modifié le réseau → refléter le nouveau statut (optimiste) + re-sonder l'appareil.
function onOnuChanged ({ action } = {}) {
  const map = { suspend: 'Suspendu', unsuspend: 'Actif', activate: 'Actif', remove: 'Retiré' }
  if (map[action]) props.doc.status = map[action]
  setTimeout(doRefresh, 1500)
}

async function doRefresh () {
  if (!props.doc.serial_number) return
  refreshing.value = true
  try {
    await refreshDeviceParams(props.doc.serial_number)
    fetchOltStatus(props.doc.serial_number)
    setTimeout(() => { fetchStatus([{ serial_number: props.doc.serial_number }]); refreshing.value = false }, 3000)
  } catch { refreshing.value = false }
}

onMounted(async () => {
  if (props.doc.serial_number) {
    fetchStatus([{ serial_number: props.doc.serial_number }])
    fetchOltStatus(props.doc.serial_number)
    fetchPortContext(props.doc.serial_number).then(ctx => { portCtx.value = ctx })
    loadEvents()
    loadWanHistory()
  }
  loadWStatus() // sans-fil : état rapide RADIUS au chargement (self-guard isWireless)
})

// Chargement PARESSEUX : on n'interroge le modem (modem-bridge) que si l'agent déplie « Appareils connectés ».
watch(showHosts, v => { if (v && !hosts.value && device.value) loadHosts() })
watch(() => props.doc.serial_number, sn => {
  stopWanLive(); stopEthLive() // arrêter les mesures live de l'appareil précédent
  liveF.value = null; airos.value = null; wStatus.value = null // repartir propre pour le nouvel appareil
  if (sn) {
    hosts.value = null
    fetchStatus([{ serial_number: sn }])
    fetchOltStatus(sn)
    loadEvents(); loadWanHistory() // rafraîchir la bande dispo/événements pour le nouvel appareil
  }
  loadWStatus()
})
onUnmounted(() => { stopWanLive(); stopEthLive() })
</script>

<style scoped>
.acs-badge-pulse { animation: badge-pulse 1.2s ease-in-out infinite; }
@keyframes badge-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}
.topo-summary { border: 1px solid #e2e8f0; border-radius: 10px; padding: 12px; background: #f8fafc; margin-bottom: 10px; }
.topo-row { display: flex; align-items: flex-start; justify-content: center; gap: 0; }
.topo-node { display: flex; flex-direction: column; align-items: center; width: 90px; }
.topo-modem { position: relative; }
.topo-dot { position: absolute; right: -2px; bottom: 2px; width: 9px; height: 9px; border-radius: 999px; border: 2px solid #f8fafc; }
.topo-dot.on { background: #21BA45; } .topo-dot.off { background: #C10015; }
.topo-cap { font-size: 0.72rem; color: #64748b; margin-top: 3px; text-align: center; }
.topo-link { flex: 1; max-width: 56px; height: 2px; background: #cbd5e1; margin-top: 12px; }
.topo-link.ok { background: #21BA45; opacity: .55; }
.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; }
.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; }
.dv-tv { font-size: 0.84rem; font-weight: 500; color: #1e293b; margin-top: 2px; }
.dv-link { color: #4f46e5; text-decoration: none; }
.dv-row2 { display: flex; gap: 8px; margin-top: 8px; }
.dv-wan, .dv-events { flex: 1; border: 1px solid #e2e8f0; border-radius: 8px; padding: 7px 9px; background: #fff; }
.dv-wan-ph { font-size: 0.78rem; color: #94a3b8; padding: 10px 0; text-align: center; }
.dv-ev { font-size: 0.74rem; color: #334155; padding: 2px 0; }
.topo-click { cursor: pointer; border-radius: 8px; transition: background .12s; }
.topo-click:hover { background: #eef2ff; }
.wan-strip { display: flex; gap: 2px; align-items: flex-end; height: 22px; margin-top: 4px; }
.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-note { font-size: 0.74rem; color: #92400e; margin-top: 6px; display: flex; align-items: center; gap: 4px; }
.dsf-macfix { font-size: 0.74rem; color: #9a3412; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 6px; padding: 4px 8px; margin-top: 7px; display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.dsf-macfix code { font-size: 0.72rem; }
.dsf-eth { margin-top: 8px; border-top: 1px dashed #e9d5ff; padding-top: 6px; }
.dsf-eth-row { display: flex; align-items: center; gap: 6px; }
.dsf-eth-tl { font-size: 0.72rem; color: #6b21a8; font-weight: 600; display: flex; align-items: center; gap: 3px; }
.dsf-wstatus { display: flex; align-items: center; gap: 5px; font-size: 0.78rem; color: #334155; margin-bottom: 6px; }
.dsf-dot { width: 9px; height: 9px; border-radius: 999px; display: inline-block; }
.dsf-dot.on { background: #21BA45; box-shadow: 0 0 4px #21BA4588; }
.dsf-dot.off { background: #C10015; }
.dsf-dot.unk { background: #9ca3af; }
.dsf-sig { font-weight: 700; }
.dsf-open { display: inline-flex; align-items: center; gap: 3px; font-size: 0.74rem; color: #4f46e5; text-decoration: none; font-weight: 600; margin-right: 6px; }
.dsf-open:hover { text-decoration: underline; }
.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; }
.fibre-top { display: flex; align-items: center; font-size: 0.84rem; padding: 6px 8px; background: #f8fafc; border-radius: 6px; border: 1px solid #e2e8f0; }
.diag-section-title { font-size: 0.78rem; font-weight: 600; color: #4b5563; margin-bottom: 4px; display: flex; align-items: center; }
.diag-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 16px; }
.diag-item { display: flex; align-items: baseline; gap: 6px; padding: 4px 0; font-size: 0.84rem; border-bottom: 1px solid #f1f5f9; }
.diag-label { font-size: 0.72rem; font-weight: 600; color: #9ca3af; min-width: 72px; flex-shrink: 0; }
.ip-grid { display: flex; flex-direction: column; gap: 3px; }
.ip-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 0.84rem; }
.ip-role { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; padding: 1px 6px; border-radius: 3px; min-width: 58px; text-align: center; }
.ip-role--internet { background: #dcfce7; color: #166534; }
.ip-role--management { background: #dbeafe; color: #1e40af; }
.ip-role--service { background: #f3e8ff; color: #6b21a8; }
.ip-role--lan { background: #f1f5f9; color: #475569; }
.ip-addr { font-size: 0.84rem; font-weight: 600; }
.ip-link { color: #1e40af; text-decoration: none; font-family: monospace; }
.ip-link:hover { text-decoration: underline; }
.mesh-nodes { display: flex; flex-direction: column; gap: 6px; }
.mesh-node { padding: 6px 8px; background: #f8fafc; border-radius: 6px; border: 1px solid #e2e8f0; }
.mesh-node-header { display: flex; align-items: center; font-size: 0.84rem; }
.node-group { margin-bottom: 8px; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; }
.node-group-header { display: flex; align-items: center; padding: 6px 10px; background: #f8fafc; border-bottom: 1px solid #e2e8f0; font-size: 0.82rem; cursor: pointer; user-select: none; }
.node-group-header:hover { background: #f1f5f9; }
.node-chevron { color: #9ca3af; margin-right: 2px; transition: transform 0.15s; }
.hosts-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.hosts-table th { text-align: left; font-size: 0.68rem; font-weight: 600; color: #9ca3af; padding: 3px 6px; border-bottom: 1px solid #f1f5f9; }
.hosts-table td { padding: 3px 6px; border-bottom: 1px solid #f1f5f9; }
.hosts-table code { font-size: 0.72rem; }
.port-ctx-bar { height: 6px; border-radius: 3px; background: #f1f5f9; display: flex; overflow: hidden; }
.port-ctx-fill--online { background: #4ade80; }
.port-ctx-fill--offline { background: #f87171; }
.port-ctx-legend { display: flex; align-items: center; }
.port-ctx-neighbors { display: flex; flex-direction: column; gap: 2px; }
.port-ctx-neighbor { padding: 2px 6px; background: #fef2f2; border-radius: 4px; border: 1px solid #fecaca; }
/* Advanced diagnostic */
.adv-issues { display: flex; flex-direction: column; gap: 6px; }
.adv-issue { padding: 8px 10px; border-radius: 6px; font-size: 0.82rem; }
.adv-issue--critical { background: #fef2f2; border: 1px solid #fecaca; }
.adv-issue--warning { background: #fffbeb; border: 1px solid #fde68a; }
.adv-issue--info { background: #eff6ff; border: 1px solid #bfdbfe; }
.adv-issue-header { display: flex; align-items: center; gap: 6px; }
.adv-issue--critical .adv-issue-header { color: #991b1b; }
.adv-issue--warning .adv-issue-header { color: #92400e; }
.adv-issue--info .adv-issue-header { color: #1e40af; }
.adv-issue-detail { font-size: 0.76rem; color: #6b7280; margin-top: 2px; padding-left: 22px; }
.adv-issue-action { font-size: 0.76rem; color: #4b5563; margin-top: 3px; padding-left: 22px; font-style: italic; }
.adv-ok { padding: 8px 10px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 6px; font-size: 0.82rem; color: #166534; display: flex; align-items: center; }
.adv-mesh-nodes { display: flex; flex-direction: column; gap: 6px; }
.adv-mesh-node { padding: 8px 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; }
.adv-mesh-header { display: flex; align-items: center; font-size: 0.84rem; }
.adv-mesh-stats { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 4px; font-size: 0.78rem; }
.adv-mesh-stat { display: flex; align-items: baseline; gap: 4px; }
.adv-signal-bar { width: 40px; height: 6px; background: #e5e7eb; border-radius: 3px; display: inline-block; vertical-align: middle; margin-right: 4px; overflow: hidden; }
.adv-signal-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.adv-clients-table td:first-child { white-space: nowrap; }
.adv-inactive-row { opacity: 0.5; font-style: italic; }

/* Mobile (agents terrain) : la carte réseau et les grilles doivent tenir dans
   l'écran plutôt que déborder/rogner (nœuds topo à largeur fixe = cause du clip). */
@media (max-width: 600px) {
  .topo-summary { padding: 10px; }
  .topo-row { gap: 2px; }
  .topo-node { width: auto; flex: 1 1 0; min-width: 0; }
  .topo-cap, .topo-sub { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .topo-link { max-width: 18px; }
  .dv-grid { grid-template-columns: repeat(2, 1fr); }
  .dv-row2 { flex-direction: column; }
  .diag-grid { grid-template-columns: 1fr; }
}
</style>
