(function () {
  const STORAGE = {
    colWidths: 'ra_console_col_widths',
    colOrder: 'ra_console_col_order',
  };

  const RA_COLUMNS = [
    { key: 'device_name', label: 'Device' },
    { key: 'status', label: 'Status' },
    { key: 'opt_in', label: 'Opt-in' },
    { key: 'android_api', label: 'Android API' },
    { key: 'android_version', label: 'Android version' },
    { key: 'last_seen', label: 'Last seen' },
    { key: 'app_version', label: 'App' },
  ];

  const DEFAULT_COL_WIDTHS = {
    expand: 40,
    device_name: 160,
    status: 120,
    opt_in: 88,
    android_api: 96,
    android_version: 112,
    last_seen: 152,
    app_version: 80,
    actions: 220,
  };

  function basePath() {
    return document.body.getAttribute('data-base-path') || '';
  }

  function canOperate() {
    return document.body.getAttribute('data-can-ra-operate') === '1';
  }

  function canAdmin() {
    return document.body.getAttribute('data-can-ra-admin') === '1';
  }

  function lsGet(key, fallback) {
    try {
      const v = localStorage.getItem(key);
      return v === null ? fallback : v;
    } catch {
      return fallback;
    }
  }

  function lsSet(key, value) {
    try {
      localStorage.setItem(key, value);
    } catch { /* ignore */ }
  }

  function apiUrl(action, params) {
    const q = new URLSearchParams(params || {});
    q.set('action', action);
    return basePath() + '/api/remote_access.php?' + q.toString();
  }

  function setStatus(msg, isError) {
    const el = document.getElementById('ra-status');
    if (!el) return;
    el.textContent = msg;
    el.classList.toggle('error', !!isError);
  }

  async function fetchJson(url, opts) {
    const res = await fetch(url, Object.assign({ credentials: 'same-origin' }, opts || {}));
    const data = await res.json().catch(() => ({}));
    if (!res.ok || data.ok === false) {
      throw new Error(data.error || ('HTTP ' + res.status));
    }
    return data;
  }

  function esc(s) {
    const d = document.createElement('div');
    d.textContent = s == null ? '' : String(s);
    return d.innerHTML;
  }

  function formatBytes(n) {
    const b = Number(n) || 0;
    if (b < 1024) return b + ' B';
    if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KiB';
    if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(2) + ' MiB';
    return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GiB';
  }

  function getColWidths() {
    try {
      const w = JSON.parse(lsGet(STORAGE.colWidths, '{}'));
      return { ...DEFAULT_COL_WIDTHS, ...(w && typeof w === 'object' ? w : {}) };
    } catch {
      return { ...DEFAULT_COL_WIDTHS };
    }
  }

  function saveColWidths(widths) {
    lsSet(STORAGE.colWidths, JSON.stringify(widths));
  }

  function getOrderedColumnKeys() {
    const defaults = RA_COLUMNS.map((c) => c.key);
    try {
      const saved = JSON.parse(lsGet(STORAGE.colOrder, '[]'));
      if (Array.isArray(saved) && saved.length) {
        const out = saved.filter((k) => defaults.includes(k));
        defaults.forEach((k) => {
          if (!out.includes(k)) out.push(k);
        });
        return out;
      }
    } catch { /* ignore */ }
    return defaults;
  }

  function saveColumnOrder(keys) {
    lsSet(STORAGE.colOrder, JSON.stringify(keys));
  }

  function getDisplayColumns() {
    return getOrderedColumnKeys()
      .map((k) => RA_COLUMNS.find((c) => c.key === k))
      .filter(Boolean);
  }

  function columnLabelByKey(key) {
    const c = RA_COLUMNS.find((col) => col.key === key);
    if (c) return c.label;
    if (key === 'actions') return 'Actions';
    return key;
  }

  function colspan() {
    return getDisplayColumns().length + 2;
  }

  function applyColgroup() {
    const cg = document.getElementById('ra-devices-colgroup');
    if (!cg) return;
    const widths = getColWidths();
    const order = getOrderedColumnKeys();
    let html =
      '<col class="col-expand" style="width:' + (widths.expand || 40) + 'px" />';
    order.forEach((k) => {
      html +=
        '<col data-col="' +
        esc(k) +
        '" style="width:' +
        (widths[k] || 100) +
        'px" />';
    });
    html +=
      '<col class="col-actions" style="width:' + (widths.actions || 220) + 'px" />';
    cg.innerHTML = html;
  }

  function clearColumnDropMarkers(thead) {
    thead.querySelectorAll('.th-drop-target').forEach((el) => {
      el.classList.remove('th-drop-target');
    });
    thead.querySelectorAll('.th-dragging').forEach((el) => {
      el.classList.remove('th-dragging');
    });
  }

  function renderHead() {
    const thead = document.getElementById('ra-devices-thead');
    if (!thead) return;
    const cols = getDisplayColumns();
    const widths = getColWidths();
    let html =
      '<tr><th class="report-tree-head" aria-label="Expand"><span class="col-resizer" data-resize-col="expand"></span></th>';
    cols.forEach((c) => {
      const w = widths[c.key] ? ' style="width:' + widths[c.key] + 'px"' : '';
      html +=
        '<th class="th-draggable"' +
        ' draggable="true" data-col-key="' +
        esc(c.key) +
        '" title="Drag to reorder"' +
        w +
        ' scope="col"><span class="th-inner">' +
        '<span class="th-drag" aria-hidden="true" title="Drag column">⋮⋮</span>' +
        '<span class="th-label">' +
        esc(c.label) +
        '</span></span>' +
        '<span class="col-resizer" data-resize-col="' +
        esc(c.key) +
        '"></span></th>';
    });
    html +=
      '<th class="col-actions" scope="col" style="width:' +
      (widths.actions || 220) +
      'px">Actions<span class="col-resizer" data-resize-col="actions"></span></th></tr>';
    thead.innerHTML = html;
  }

  function bindTableLayout() {
    const table = document.getElementById('ra-devices-table');
    const thead = document.getElementById('ra-devices-thead');
    if (!table || !thead || table.dataset.layoutBound === '1') return;
    table.dataset.layoutBound = '1';

    thead.addEventListener('dragstart', (e) => {
      if (e.target.closest('.col-resizer')) {
        e.preventDefault();
        return;
      }
      const th = e.target.closest('th.th-draggable[data-col-key]');
      if (!th) return;
      const sourceKey = th.getAttribute('data-col-key');
      if (!sourceKey) return;
      table.dataset.dragColKey = sourceKey;
      th.classList.add('th-dragging');
      const ghost = document.createElement('div');
      ghost.className = 'col-drag-ghost';
      ghost.textContent = columnLabelByKey(sourceKey);
      ghost.setAttribute('aria-hidden', 'true');
      document.body.appendChild(ghost);
      table._dragGhost = ghost;
      if (e.dataTransfer) {
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', sourceKey);
        try {
          e.dataTransfer.setDragImage(ghost, 16, 14);
        } catch { /* ignore */ }
      }
    });

    thead.addEventListener('dragend', () => {
      clearColumnDropMarkers(thead);
      delete table.dataset.dragColKey;
      if (table._dragGhost) {
        table._dragGhost.remove();
        table._dragGhost = null;
      }
    });

    thead.addEventListener('dragenter', (e) => {
      if (!table.dataset.dragColKey) return;
      e.preventDefault();
    });

    thead.addEventListener('dragover', (e) => {
      if (!table.dataset.dragColKey) return;
      e.preventDefault();
      if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
      const th = e.target.closest('th[data-col-key]');
      if (!th) return;
      const active = thead.querySelector('.th-drop-target');
      if (active && active !== th) active.classList.remove('th-drop-target');
      th.classList.add('th-drop-target');
    });

    thead.addEventListener('dragleave', (e) => {
      const th = e.target.closest('th[data-col-key]');
      if (!th) return;
      const rel = e.relatedTarget;
      if (rel && th.contains(rel)) return;
      th.classList.remove('th-drop-target');
    });

    thead.addEventListener('drop', (e) => {
      const th = e.target.closest('th[data-col-key]');
      if (!th) return;
      e.preventDefault();
      e.stopPropagation();
      const sourceKey =
        table.dataset.dragColKey ||
        (e.dataTransfer && e.dataTransfer.getData('text/plain')) ||
        '';
      const targetKey = th.getAttribute('data-col-key');
      clearColumnDropMarkers(thead);
      delete table.dataset.dragColKey;
      if (table._dragGhost) {
        table._dragGhost.remove();
        table._dragGhost = null;
      }
      if (!sourceKey || !targetKey || sourceKey === targetKey) return;
      const order = getOrderedColumnKeys();
      const from = order.indexOf(sourceKey);
      const to = order.indexOf(targetKey);
      if (from < 0 || to < 0) return;
      order.splice(from, 1);
      order.splice(to, 0, sourceKey);
      saveColumnOrder(order);
      refreshTableChrome(lastDevices, lastExpanded);
    });

    table.addEventListener('mousedown', (e) => {
      const handle = e.target.closest('.col-resizer');
      if (!handle) return;
      e.preventDefault();
      e.stopPropagation();
      const colKey = handle.getAttribute('data-resize-col');
      const th = handle.closest('th');
      if (!colKey || !th) return;
      const widths = getColWidths();
      const startX = e.clientX;
      const startW = th.getBoundingClientRect().width;

      const onMove = (ev) => {
        const w = Math.max(48, Math.round(startW + (ev.clientX - startX)));
        widths[colKey] = w;
        th.style.width = w + 'px';
        th.style.minWidth = w + 'px';
        th.style.maxWidth = w + 'px';
        const cg = document.getElementById('ra-devices-colgroup');
        const col = cg && cg.querySelector('col[data-col="' + colKey + '"]');
        if (col) col.style.width = w + 'px';
        else if (colKey === 'actions') {
          const actionCol = cg && cg.querySelector('col.col-actions');
          if (actionCol) actionCol.style.width = w + 'px';
        } else if (colKey === 'expand') {
          const expandCol = cg && cg.querySelector('col.col-expand');
          if (expandCol) expandCol.style.width = w + 'px';
        }
      };
      const onUp = () => {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        saveColWidths(widths);
        applyColgroup();
      };
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
  }

  function statusBadge(d) {
    const label = d.status_label || '';
    if (label === 'needs_whitelist') {
      return '<span class="tag-pill tag-pill--warn">Needs whitelist</span>';
    }
    if (label === 'whitelisted') {
      return '<span class="tag-pill">Whitelisted</span>';
    }
    if (label === 'stale') {
      return '<span class="muted">Stale</span>';
    }
    if (label === 'not_opted_in') {
      return '<span class="muted">Not opted in</span>';
    }
    return '<span class="muted">Polling</span>';
  }

  function deviceTitle(d) {
    const name = (d.device_name || d.device_display || '').trim();
    if (name) return name;
    const id = String(d.device_id || '');
    if (id.length > 14) return id.slice(0, 8) + '…' + id.slice(-4);
    return id;
  }

  function formatAbis(abis) {
    if (!Array.isArray(abis) || !abis.length) return '—';
    return abis.join(', ');
  }

  function formatCell(key, d) {
    switch (key) {
      case 'device_name':
        return (
          '<code class="ra-device-id" title="' +
          esc(d.device_id) +
          '">' +
          esc(deviceTitle(d)) +
          '</code>'
        );
      case 'status':
        return statusBadge(d);
      case 'opt_in':
        return esc(d.opt_in_mode || 'none');
      case 'android_api':
        return d.sdk_int != null && d.sdk_int !== '' ? esc(String(d.sdk_int)) : '—';
      case 'android_version':
        return esc(d.os_release || '—');
      case 'last_seen':
        return esc(d.last_seen_at || '—');
      case 'app_version':
        return esc(d.app_version || '—');
      default:
        return '—';
    }
  }

  function detailLines(d) {
    const lines = [];
    lines.push('Device ID: ' + (d.device_id || '—'));
    const name = (d.device_name || d.device_display || '').trim();
    if (name) lines.push('Device name: ' + name);
    if (d.manufacturer || d.model) {
      lines.push('Hardware: ' + [d.manufacturer, d.model].filter(Boolean).join(' '));
    }
    if (d.brand) lines.push('Brand: ' + d.brand);
    if (d.product) lines.push('Product: ' + d.product);
    if (d.hardware_device) lines.push('Device codename: ' + d.hardware_device);
    if (d.sdk_int != null) lines.push('Android API: ' + d.sdk_int);
    if (d.os_release) lines.push('Android version: ' + d.os_release);
    if (d.abis && d.abis.length) lines.push('ABIs: ' + formatAbis(d.abis));
    if (d.lan_ip) lines.push('Device LAN IP: ' + d.lan_ip);
    if (d.device_wan_ip) lines.push('Device WAN IP: ' + d.device_wan_ip);
    if (d.poll_source_ip) {
      let pollLabel = 'Poll source IP (server view)';
      if (d.poll_source_is_private) {
        pollLabel += ' — intra/private hop, not device WAN';
      }
      lines.push(pollLabel + ': ' + d.poll_source_ip);
    }
    if (d.vpn_ip) lines.push('VPN IP: ' + d.vpn_ip);
    if (d.vpn_route_scope) lines.push('VPN route scope: ' + d.vpn_route_scope);
    if (d.vpn_app_scope) lines.push('VPN app scope: ' + d.vpn_app_scope);
    if (d.vpn_public_key) lines.push('VPN public key: ' + d.vpn_public_key);
    if (d.wg_rx_bytes != null || d.wg_tx_bytes != null) {
      const rx = formatBytes(d.wg_rx_bytes || 0);
      const tx = formatBytes(d.wg_tx_bytes || 0);
      lines.push('VPN traffic (BE wg): ↓' + rx + ' ↑' + tx);
    }
    if (d.wg_latest_handshake > 0) {
      lines.push('VPN last handshake: ' + new Date(d.wg_latest_handshake * 1000).toISOString());
    }
    if (d.wg_endpoint) lines.push('VPN endpoint: ' + d.wg_endpoint);
    lines.push('App version: ' + (d.app_version || '—'));
    lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
    lines.push('Last seen: ' + (d.last_seen_at || '—'));
    if (d.notes) lines.push('Notes: ' + d.notes);
    if (d.issue_count != null) lines.push('Linked issues: ' + d.issue_count);
    if (d.graph_session_count != null) lines.push('Graph sessions: ' + d.graph_session_count);
    return lines;
  }

  function detailLinks(d) {
    const links = d.links || [];
    if (!links.length) return '';
    let html = '<div class="ra-detail-links">';
    links.forEach((link) => {
      html +=
        '<a class="btn btn-sm" href="' +
        esc(link.href) +
        '">' +
        esc(link.label) +
        '</a> ';
    });
    html += '</div>';
    return html;
  }

  function isInteractiveTarget(el) {
    return !!el.closest('.ra-device-actions, .ra-detail-links, .report-tree-toggle, button, a, input, select, textarea, label');
  }

  function bindRaTree(root) {
    root.querySelectorAll('.report-row[data-device-id]').forEach((row) => {
      const btn = row.querySelector('.report-tree-toggle');
      const briefId = btn ? btn.getAttribute('aria-controls') : null;
      const briefRow = briefId ? document.getElementById(briefId) : null;
      if (!briefRow || !btn) return;

      const toggle = (e) => {
        if (e && isInteractiveTarget(e.target)) return;
        const open = briefRow.hidden;
        briefRow.hidden = !open;
        btn.setAttribute('aria-expanded', open ? 'true' : 'false');
        row.classList.toggle('report-row--open', open);
      };

      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        toggle(e);
      });
      row.addEventListener('click', (e) => toggle(e));
      row.addEventListener('keydown', (e) => {
        if (isInteractiveTarget(e.target)) return;
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          toggle(e);
        }
      });
    });
  }

  function captureExpandedDeviceIds() {
    const ids = new Set();
    const tbody = document.getElementById('ra-devices-tbody');
    if (!tbody) return ids;
    tbody.querySelectorAll('.report-row.report-row--open[data-device-id]').forEach((row) => {
      const id = row.getAttribute('data-device-id');
      if (id) ids.add(id);
    });
    return ids;
  }

  let lastDevices = [];
  let lastExpanded = new Set();

  function renderDevices(devices, expandedIds) {
    const tbody = document.getElementById('ra-devices-tbody');
    if (!tbody) return;
    lastDevices = devices || [];
    if (expandedIds) lastExpanded = expandedIds;

    const cols = getDisplayColumns();
    const span = colspan();

    if (!devices || !devices.length) {
      tbody.innerHTML =
        '<tr><td colspan="' +
        span +
        '" class="muted">No devices have polled yet. Enable remote access on a device (dev settings) and wait for the next poll.</td></tr>';
      return;
    }

    let html = '';
    devices.forEach((d, i) => {
      const rowKey = 'ra-' + i;
      const briefId = 'ra-brief-' + rowKey;
      const wl = Number(d.whitelisted) === 1;
      const optIn = d.opt_in_mode || 'none';
      const openReady = wl && (optIn === 'wireguard' || optIn === 'rssh');
      const openHint = !canOperate()
        ? 'Need remote_access_operate permission'
        : !wl
          ? 'Whitelist device first'
          : openReady
            ? 'Open session — device connects on next poll (≤7 min)'
            : 'Phone must poll with RSSH/WG enabled (dev settings on device; wait ≤7 min)';
      const openDisabled = canOperate() ? '' : ' disabled';
      const wlDisabled = canAdmin() ? '' : ' disabled';
      const isOpen = lastExpanded.has(String(d.device_id || ''));
      const rowClass =
        'report-row' +
        (d.needs_whitelist ? ' ra-device-row--needs-wl' : '') +
        (isOpen ? ' report-row--open' : '');

      html += '<tr class="' + rowClass + '" data-device-id="' + esc(d.device_id) + '" tabindex="0">';
      html +=
        '<td class="report-tree-cell"><button type="button" class="report-tree-toggle" aria-expanded="' +
        (isOpen ? 'true' : 'false') +
        '" aria-controls="' +
        briefId +
        '" title="Show device details"><span class="report-tree-arrow" aria-hidden="true"></span></button></td>';

      cols.forEach((c) => {
        html += '<td data-col="' + esc(c.key) + '">' + formatCell(c.key, d) + '</td>';
      });

      html +=
        '<td class="ra-device-actions col-actions">' +
        '<button type="button" class="btn btn-sm btn-primary"' +
        openDisabled +
        ' data-open-session="' +
        esc(d.device_id) +
        '" title="' +
        esc(openHint) +
        '">Open session</button> ' +
        '<button type="button" class="btn btn-sm"' +
        wlDisabled +
        ' data-toggle-wl="' +
        esc(d.device_id) +
        '" data-wl="' +
        (wl ? '0' : '1') +
        '">' +
        (wl ? 'Revoke WL' : 'Whitelist') +
        '</button> ' +
        '<button type="button" class="btn btn-sm" data-copy-id="' +
        esc(d.device_id) +
        '">Copy ID</button>' +
        '</td>';
      html += '</tr>';

      html +=
        '<tr class="report-brief-row" id="' +
        briefId +
        '"' +
        (isOpen ? '' : ' hidden') +
        '><td colspan="' +
        span +
        '" class="report-brief-cell"><div class="report-brief-panel"><div class="report-brief">';
      detailLines(d).forEach((line) => {
        html += '<p>' + esc(line) + '</p>';
      });
      html += detailLinks(d);
      html += '</div></div></td></tr>';
    });
    tbody.innerHTML = html;
    bindRaTree(tbody);
  }

  function refreshTableChrome(devices, expandedIds) {
    applyColgroup();
    renderHead();
    renderDevices(devices || lastDevices, expandedIds || lastExpanded);
  }

  function renderSessions(active, inactive) {
    const aBody = document.getElementById('ra-active-tbody');
    const iBody = document.getElementById('ra-inactive-tbody');
    if (aBody) {
      aBody.innerHTML = '';
      if (!active || !active.length) {
        aBody.innerHTML = '<tr><td colspan="6" class="muted">No pending or active sessions.</td></tr>';
      }
      (active || []).forEach((s) => {
        const tr = document.createElement('tr');
        const closeBtn = canOperate()
          ? '<button type="button" class="btn btn-sm" data-close-session="' + esc(s.session_id) + '" data-device="' + esc(s.device_id) + '">Close</button>'
          : '';
        let endpoint = s.endpoint || '—';
        if (s.tunnel === 'ssh_reverse' && s.rssh_remote_bind) {
          endpoint = endpoint + ' → ' + esc(s.rssh_remote_bind);
        }
        let operator = '';
        if (s.rssh_operator) {
          operator =
            '<div class="ra-rssh-cmds muted"><code>' +
            esc(s.rssh_operator.shell || '') +
            '</code></div>';
        }
        tr.innerHTML =
          '<td><code>' + esc(s.session_id) + '</code></td>' +
          '<td><code>' + esc(s.device_id) + '</code></td>' +
          '<td>' + esc(s.status) + '</td>' +
          '<td>' + esc(s.tunnel) + '</td>' +
          '<td>' + endpoint + operator + '</td>' +
          '<td>' + closeBtn + '</td>';
        aBody.appendChild(tr);
      });
    }
    if (iBody) {
      iBody.innerHTML = '';
      (inactive || []).forEach((s) => {
        const tr = document.createElement('tr');
        tr.innerHTML =
          '<td><code>' + esc(s.session_id) + '</code></td>' +
          '<td><code>' + esc(s.device_id) + '</code></td>' +
          '<td>' + esc(s.status) + '</td>' +
          '<td>' + esc(s.closed_at || '—') + '</td>' +
          '<td>' + esc(s.close_reason || '—') + '</td>';
        iBody.appendChild(tr);
      });
      if (!inactive || !inactive.length) {
        iBody.innerHTML = '<tr><td colspan="5" class="muted">No closed sessions yet.</td></tr>';
      }
    }
  }

  function renderEvents(events) {
    const tbody = document.getElementById('ra-events-tbody');
    if (!tbody) return;
    tbody.innerHTML = '';
    if (!events || !events.length) {
      tbody.innerHTML = '<tr><td colspan="4" class="muted">No audit events.</td></tr>';
      return;
    }
    (events || []).forEach((e) => {
      const tr = document.createElement('tr');
      tr.innerHTML =
        '<td>' + esc(e.created_at) + '</td>' +
        '<td><code>' + esc(e.device_id) + '</code></td>' +
        '<td>' + esc(e.action) + '</td>' +
        '<td>' + esc(e.reason || '—') + '</td>';
      tbody.appendChild(tr);
    });
  }

  let lastConfig = {};

  async function refresh() {
    const expanded = captureExpandedDeviceIds();
    setStatus('Loading…');
    try {
      const data = await fetchJson(apiUrl('dashboard'));
      lastConfig = data.config || {};
      refreshTableChrome(data.devices, expanded);
      renderSessions(data.active_sessions, data.inactive_sessions);
      renderEvents(data.recent_events);
      const ep = lastConfig.wg_endpoint ? ' · WG ' + lastConfig.wg_endpoint : '';
      setStatus('Updated ' + new Date().toLocaleTimeString() + ep);
    } catch (e) {
      setStatus('Failed: ' + e.message, true);
    }
  }

  document.addEventListener('click', async (ev) => {
    const t = ev.target;
    if (!(t instanceof HTMLElement)) return;
    if (t.closest('.report-tree-toggle, .ra-detail-links a')) return;
    const copyId = t.getAttribute('data-copy-id');
    if (copyId) {
      try {
        await navigator.clipboard.writeText(copyId);
        setStatus('Copied device ID');
      } catch (e) {
        setStatus('Copy failed', true);
      }
      return;
    }
    const openId = t.getAttribute('data-open-session');
    if (openId) {
      if (!canOperate()) return;
      const dev = (lastDevices || []).find((d) => String(d.device_id) === openId);
      const wl = dev && Number(dev.whitelisted) === 1;
      const optIn = dev ? dev.opt_in_mode || 'none' : 'none';
      if (!wl) {
        setStatus('Whitelist device ' + openId + ' first', true);
        return;
      }
      if (optIn !== 'wireguard' && optIn !== 'rssh') {
        setStatus(
          'Device opt-in is "' +
            optIn +
            '". On phone: dev settings → Remote access → RSSH, then wait for poll (≤7 min).',
          true
        );
        return;
      }
      try {
        setStatus('Opening session for ' + openId + '…');
        await fetchJson(apiUrl('open_session'), {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ device_id: openId }),
        });
        setStatus('Session opened — device will connect on next poll (≤7 min)');
        await refresh();
      } catch (e) {
        setStatus('Open session: ' + e.message, true);
      }
      return;
    }
    const closeId = t.getAttribute('data-close-session');
    if (closeId) {
      if (!canOperate()) return;
      try {
        await fetchJson(apiUrl('close_session'), {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            session_id: closeId,
            device_id: t.getAttribute('data-device') || '',
          }),
        });
        await refresh();
      } catch (e) {
        setStatus('Close session: ' + e.message, true);
      }
      return;
    }
    const wlDevice = t.getAttribute('data-toggle-wl');
    if (wlDevice) {
      if (!canAdmin()) return;
      try {
        await fetchJson(apiUrl('whitelist'), {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            device_id: wlDevice,
            whitelisted: t.getAttribute('data-wl') === '1',
          }),
        });
        await refresh();
      } catch (e) {
        setStatus('Whitelist: ' + e.message, true);
      }
    }
  });

  const form = document.getElementById('ra-whitelist-form');
  if (form) {
    if (!canAdmin()) form.hidden = true;
    form.addEventListener('submit', async (ev) => {
      ev.preventDefault();
      if (!canAdmin()) return;
      const fd = new FormData(form);
      try {
        await fetchJson(apiUrl('whitelist'), {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            device_id: fd.get('device_id'),
            notes: fd.get('notes'),
            whitelisted: true,
          }),
        });
        form.reset();
        await refresh();
      } catch (e) {
        setStatus('Whitelist add: ' + e.message, true);
      }
    });
  }

  bindTableLayout();
  applyColgroup();
  renderHead();
  refresh();
  setInterval(refresh, 30000);
})();
