const API = '/api';

async function api(path, options = {}, timeoutMs) {
  const res = await fetchWithTimeout(`${API}${path}`, {
    headers: authHeaders({ 'Content-Type': 'application/json', ...(options.headers || {}) }),
    ...options,
  }, timeoutMs);
  if (res.status === 401) {
    logout();
    throw new Error('sessão expirada');
  }
  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    const detail = data.detail;
    const msg = typeof detail === 'object' ? detail.message || JSON.stringify(detail) : (detail || `${res.status} ${path}`);
    throw new Error(msg);
  }
  return res.json();
}

/** Requisições longas (OpenPanel provision) — sem AbortController. */
async function apiLongRunning(path, options = {}) {
  const res = await fetch(`${API}${path}`, {
    headers: authHeaders({ 'Content-Type': 'application/json', ...(options.headers || {}) }),
    ...options,
  });
  if (res.status === 401) {
    logout();
    throw new Error('sessão expirada');
  }
  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    const detail = data.detail;
    const msg = typeof detail === 'object' ? detail.message || JSON.stringify(detail) : (detail || `${res.status} ${path}`);
    throw new Error(msg);
  }
  return res.json();
}

function fmtDate(iso) {
  if (!iso) return '—';
  try {
    return new Date(iso).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
  } catch {
    return iso;
  }
}

function esc(s) {
  return String(s ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function sessionHashHtml(sessionId, { full = true } = {}) {
  const id = (sessionId || '').trim();
  if (!id) return '';
  const shown = full ? id : `${id.slice(0, 8)}…${id.slice(-4)}`;
  return `<code class="session-hash" title="Sessão onboarding VM112">${esc(shown)}</code>`;
}

let state = {
  view: 'dashboard',
  ticketFilter: 'all',
  sourceFilter: 'all',
  eventSourceFilter: 'all',
  eventsTab: 'webhooks',
  selectedTicketId: null,
  selectedSessionId: null,
  tickets: [],
  summary: null,
  scorecardTenant: null,
  scorecardDomain: null,
  accountLoaded: false,
  overviewModal: { tenantId: null, view: 'list', domain: null, data: null, focus: 'onboard' },
  overviewHomeWindow: '24h',
  overviewHomeTrailFilter: 'all',
  overviewHomeDnsDomain: null,
  adminUsers: [],
  adminFilter: { q: '', role: 'all', status: 'all', mfa: 'all' },
  adminSelected: null,
  socWindow: '24h',
  socLastEventId: null,
  openPanelTestRunning: false,
};

const views = {
  dashboard: document.getElementById('view-dashboard'),
  overview: document.getElementById('view-overview'),
  'overview-home': document.getElementById('view-overview-home'),
  tickets: document.getElementById('view-tickets'),
  events: document.getElementById('view-events'),
  tenants: document.getElementById('view-tenants'),
  'email-migration': document.getElementById('view-email-migration'),
  infra: document.getElementById('view-infra'),
  infra2: document.getElementById('view-infra2'),
  messages: document.getElementById('view-messages'),
  admin: document.getElementById('view-admin'),
  account: document.getElementById('view-account'),
  leads: document.getElementById('view-leads'),
  modules: document.getElementById('view-modules'),
};

function roleLabel(role) {
  return ROLE_LABELS[role] || role;
}

const ROLE_LABELS = {
  super_admin: 'Super Admin',
  ops_lead: 'Chefe Ops',
  technician: 'Suporte',
  noc: 'NOC',
  sales_admin: 'Sales Admin',
  sales_support: 'Sales Support',
  finance: 'Financeiro',
  marketing: 'Marketing',
  seo: 'SEO',
  developer: 'Developer',
  devops: 'DevOps',
  security_analyst: 'Segurança / SOC',
  content_editor: 'Conteúdo / CMS',
  agentic_operator: 'Operador Agentes IA',
};

function statusLabel(status) {
  return {
    pending: 'pendente',
    approved: 'aprovado',
    rejected: 'rejeitado',
    active: 'ativo',
    open: 'aberto',
    escalated: 'escalado',
    assisting: 'assistindo',
    resolved: 'resolvido',
    closed: 'fechado',
  }[status] || status;
}

function normalizeAssistStatus(status) {
  if (status === 'active') return 'assisting';
  return status;
}

function assistStatusLabel(status) {
  return {
    observing: 'observando',
    escalated: 'escalado',
    assisting: 'assistindo',
    handed_off: 'devolvido ao cliente',
    ended: 'assistência encerrada',
  }[status] || status || 'observando';
}

function assistBadge(status) {
  if (!status || status === 'observing') {
    return '<span class="badge assist-observing">observando</span>';
  }
  const cls = status === 'assisting' ? 'assisting'
    : status === 'escalated' ? 'escalated'
      : status === 'handed_off' || status === 'ended' ? 'resolved'
        : status === 'closed' ? 'closed'
          : 'open';
  return `<span class="badge ${cls}">${esc(assistStatusLabel(status))}</span>`;
}

function ticketFunnelKvHtml(t) {
  const latest = t.latest_funnel_event || t.event;
  const opened = t.event_opened || t.event;
  const showOpened = opened && latest && opened !== latest;
  const outcome = t.onboarding_outcome;
  const outcomeBadge = outcome === 'completed'
    ? '<span class="badge ok">concluído</span>'
    : outcome === 'failed'
      ? '<span class="badge escalated">falhou</span>'
      : '';
  const label = latest ? (SOC_EVENT_LABELS[latest] || latest) : '—';
  const sev = latest && typeof socEventSeverity === 'function' ? socEventSeverity(latest) : 'open';
  return `
          <dt>Estado funil</dt><dd><span class="badge ${sev}">${esc(label)}</span> ${outcomeBadge}</dd>
          ${showOpened ? `<dt>Abertura ticket</dt><dd class="muted"><code>${esc(opened)}</code></dd>` : ''}`;
}

function setupSidebarUser() {
  const user = getUser();
  const sidebar = document.getElementById('sidebar-user');
  const header = document.getElementById('header-user');
  const logoutBtn = document.getElementById('btn-logout');
  if (!user) return;
  const label = roleLabel(user.role);
  if (sidebar) {
    sidebar.innerHTML = `
      <strong>${esc(user.display_name || user.username)}</strong>
      <span>${esc(user.username)} · ${esc(label)}</span>`;
  }
  if (header) {
    header.hidden = false;
    header.innerHTML = `<strong>${esc(user.display_name || user.username)}</strong><span>${esc(label)}</span>`;
  }
  if (logoutBtn) {
    logoutBtn.hidden = false;
    logoutBtn.onclick = logout;
  }
}

function applyRoleNav() {
  const user = getUser();
  if (!user) return;
  if (!canRunAudit()) {
    document.getElementById('nav-overview')?.setAttribute('hidden', '');
    document.getElementById('nav-overview-home')?.setAttribute('hidden', '');
  }
  if (user.role === 'noc') {
    document.getElementById('nav-tenants')?.setAttribute('hidden', '');
    const navEvents = document.getElementById('nav-events');
    const navEventsLabel = navEvents?.querySelector('.nav-label');
    if (navEventsLabel) navEventsLabel.textContent = 'Wazuh';
  }
  if (canManageUsers()) {
    document.getElementById('nav-messages')?.removeAttribute('hidden');
    document.getElementById('nav-admin')?.removeAttribute('hidden');
  }
  if (user.role === 'super_admin') {
    document.getElementById('nav-modules')?.removeAttribute('hidden');
  }
  if (canReadLeads()) {
    document.getElementById('nav-leads')?.removeAttribute('hidden');
    document.getElementById('filter-leads')?.removeAttribute('hidden');
  }
  if (typeof canManageVm112Domains === 'function' && canManageVm112Domains()) {
    document.getElementById('events-tab-purges')?.removeAttribute('hidden');
  }
  if (canRunAudit()) {
    document.getElementById('events-tab-security')?.removeAttribute('hidden');
  } else {
    document.getElementById('events-tab-security')?.setAttribute('hidden', '');
  }
  if (canReadTickets()) {
    document.getElementById('events-tab-carbonio')?.removeAttribute('hidden');
  }
}

function setView(name) {
  if (window.DeskModules?.loaded && !DeskModules.isViewEnabled(name)) {
    name = 'dashboard';
  }
  if (state.view === 'account' && name !== 'account') {
    state.accountLoaded = false;
  }
  state.view = name;
  const titles = {
    dashboard: 'Dashboard',
    overview: 'Audit Overview',
    'overview-home': 'Serviços',
    tickets: 'Tickets',
    events: 'Eventos webhook',
    tenants: 'Tenants',
    infra: 'Infraestrutura',
    infra2: 'SOC — Infra 2',
    messages: 'Mensagens — pedidos de cadastro',
    admin: 'Administradores',
    account: 'Minha conta',
    leads: 'Leads abandonados',
    modules: 'Módulos',
  };
  const subtitles = {
    dashboard: 'Operações Ligbox — onboarding, tickets e monitoramento',
    overview: 'Visão por tenant — cards de auditoria (versão clássica)',
    'overview-home': 'Desk VM122 · Orquestração MOSP',
    tickets: 'Operações Ligbox — onboarding, tickets e monitoramento',
    events: 'Operações Ligbox — onboarding, tickets e monitoramento',
    tenants: 'Operações Ligbox — onboarding, tickets e monitoramento',
    infra: 'VM112, VM104 e integrações — visão técnica',
    infra2: 'Centro de operações — monitoramento visual VM112 → VM122 em tempo quase real',
    messages: 'Operações Ligbox — onboarding, tickets e monitoramento',
    admin: 'Operações Ligbox — onboarding, tickets e monitoramento',
    account: 'Operações Ligbox — onboarding, tickets e monitoramento',
    leads: 'Operações Ligbox — onboarding, tickets e monitoramento',
    modules: 'Activar ou desativar funcionalidades do Desk sem afectar o núcleo',
  };
  document.getElementById('page-title').textContent = titles[name] || 'Ligbox Ops';
  const subEl = document.getElementById('page-subtitle');
  if (subEl) subEl.textContent = subtitles[name] || subtitles.dashboard;
  document.querySelectorAll('.nav button').forEach((b) => {
    b.classList.toggle('active', b.dataset.view === name);
  });
  Object.entries(views).forEach(([k, el]) => el?.classList.toggle('active', k === name));
  reschedulePoll();
  refresh();
}

let pollTimer = null;
function reschedulePoll() {
  if (pollTimer) clearInterval(pollTimer);
  if (state.openPanelTestRunning) return;
  const ms = state.view === 'infra2' ? 15000 : 30000;
  pollTimer = setInterval(() => refresh({ poll: true }), ms);
}

async function loadHealth() {
  const el = document.getElementById('global-health');
  try {
    const h = await api('/health');
    el.className = 'status-pill ok';
    el.innerHTML = '<span class="dot"></span> API online';
    return h;
  } catch {
    el.className = 'status-pill err';
    el.innerHTML = '<span class="dot"></span> API offline';
    return null;
  }
}

async function renderDashboard() {
  const box = document.getElementById('dashboard-content');
  box.innerHTML = '<p class="loading">Carregando…</p>';
  try {
    const leadsPromise = canReadLeads()
      ? api('/v1/crm/leads').catch(() => ({ leads: [], total: 0 }))
      : Promise.resolve({ leads: [], total: 0 });
    const rankingPromise = canAssist()
      ? api('/v1/assist/technicians/ranking?window_days=30').catch(() => ({ ranking: [] }))
      : Promise.resolve({ ranking: [] });
    const [summary, funnel, audit, vm112, wazuh, leadsData, techRanking] = await Promise.all([
      api('/v1/desk/summary').catch((e) => {
        throw new Error(`Resumo indisponível: ${e.message}`);
      }),
      api('/v1/onboard/funnel').catch(() => ({ stages: {}, active_sessions: [], sessions_total: 0 })),
      canRunAudit() ? api('/v1/audit/overview').catch(() => ({ tenants: [] })) : Promise.resolve({ tenants: [] }),
      api('/v1/infra/vm112/status').catch(() => ({ error: 'indisponível' })),
      api('/v1/infra/wazuh/status').catch(() => ({ error: 'indisponível' })),
      leadsPromise,
      rankingPromise,
    ]);
    state.summary = summary;
    const vmOk = vm112.vm112?.status === 'ok';
    const wazuhOk = wazuh.api_online === true || wazuh.http_status === 401 || wazuh.http_status === 200;
    const sessions = funnel.active_sessions || [];
    const sessionCards = sessions.slice(0, 24).map((s) => {
      const status = s.assist_status || 'observing';
      const statusCls = status === 'assisting' ? 'assisting' : status === 'escalated' ? 'escalated' : 'observing';
      return `
      <button type="button" class="session-card clickable ${s.stale ? 'stale' : ''} session-card--${statusCls}" data-session="${esc(s.session_id || '')}">
        <div class="session-card-top">
          <span class="session-card-dot" aria-hidden></span>
          <strong class="session-card-domain">${esc(s.domain || '—')}</strong>
        </div>
        <div class="session-card-stage">${esc(FUNNEL_LABELS[s.current_stage] || s.current_stage)} · onboarding VM112</div>
        <div class="session-card-meta">
          ${sessionHashHtml(s.session_id)}
          ${s.ticket_id ? `<span class="session-card-ticket">#${s.ticket_id}</span>` : ''}
        </div>
        <div class="session-card-badges">
          ${assistBadge(status)}
          ${s.is_lead || s.crm_track === 'lead' ? '<span class="badge escalated">lead</span>' : ''}
          ${['company_validated','webmail_released','completed'].includes(s.current_stage) ? '<span class="badge billing">💳 billing</span>' : ''}
          ${s.stale && s.crm_track !== 'lead' && !s.is_lead ? '<span class="badge review">abandonado</span>' : ''}
        </div>
      </button>`;
    }).join('');
    box.innerHTML = `
      <section class="dashboard-top">
        <div class="dashboard-kpis">
          ${kpiCard('Abertos', summary.tickets_open, 'open')}
          ${kpiCard('Assistindo', summary.tickets_assisting ?? 0, 'assisting')}
          ${kpiCard('Escalados', summary.tickets_escalated ?? 0, 'escalated')}
          ${kpiCard('Sessões', funnel.sessions_total || 0, 'sessions', { title: 'Sessões onboarding — 48h' })}
          ${window.DeskModules?.isEnabled('billing-recurrence') ? kpiCard('Cobrança pendente', summary.billing_pending ?? 0, 'billing-pending', { title: 'Aguardam validação OPS' }) : ''}
          ${window.DeskModules?.isEnabled('billing-recurrence') ? kpiCard('Recorrência ativa', summary.billing_active ?? 0, 'billing-active', { title: 'Clientes com recorrência' }) : ''}
          ${canReadLeads() ? kpiCard('Leads', summary.leads_abandoned ?? leadsData.total ?? 0, 'leads', { clickable: true, viewJump: 'leads', title: 'Onboarding abandonado' }) : ''}
        </div>
        ${dashboardPulseHtml({ audit, vm112, wazuh, vmOk, wazuhOk })}
      </section>
      <div class="dashboard-ops">
        <div class="card dashboard-funnel">
          <h3>Funil <span class="ticket-meta">48h</span></h3>
          ${funnelBarHtml(funnel.stages || {}, funnel.sessions_total || 0)}
        </div>
        <div class="card dashboard-sessions-panel">
          <div class="card-head-row">
            <h3>Sessões ativas</h3>
            <div class="session-legend">
              <span class="session-legend-item"><i class="dot-assisting"></i> Assistindo</span>
              <span class="session-legend-item"><i class="dot-observing"></i> Observando</span>
              <span class="ticket-meta">${sessions.length} total</span>
            </div>
          </div>
          ${sessionCards
            ? `<div class="session-grid">${sessionCards}</div>`
            : '<p class="loading">Sem sessões recentes</p>'}
        </div>
        ${canReadLeads() ? `
        <div class="card dashboard-leads-panel">
          <div class="card-head-row">
            <h3>Leads abandonados</h3>
            <button type="button" class="btn btn-ghost btn-sm" data-view-jump="leads">Ver todos</button>
          </div>
          ${(leadsData.leads || []).slice(0, 6).map(leadRowHtml).join('') || '<p class="loading">Nenhum lead — sessões stale viram lead após ${summary.onboard_stale_hours ?? 24}h</p>'}
        </div>` : ''}
        <div class="card dashboard-tickets">
          <h3>Tickets recentes</h3>
          <div class="ticket-list ticket-list-compact">
            ${(summary.recent_tickets || []).map(ticketRowHtml).join('') || '<p class="loading">Sem tickets</p>'}
          </div>
        </div>
      </div>
      ${canAssist() && (techRanking.ranking || []).length ? `
      <div class="card card-compact ranking-card">
        <div class="card-head-row">
          <h3>Ranking técnicos</h3>
          <span class="ticket-meta">30d · assumidos / movimento</span>
        </div>
        ${techRankingHtml(techRanking.ranking)}
      </div>` : ''}`;
    box.querySelectorAll('.ticket-row').forEach((btn) => {
      btn.addEventListener('click', () => {
        state.selectedTicketId = Number(btn.dataset.id);
        setView('tickets');
      });
    });
    box.querySelectorAll('[data-session]').forEach((btn) => {
      btn.addEventListener('click', () => {
        const sess = sessions.find((s) => s.session_id === btn.dataset.session);
        state.selectedSessionId = btn.dataset.session;
        state.selectedTicketId = sess?.ticket_id || null;
        setView('tickets');
      });
    });
    box.querySelectorAll('[data-view-jump="leads"]').forEach((el) => {
      el.addEventListener('click', () => setView('leads'));
    });
    box.querySelectorAll('[data-lead-ticket]').forEach((btn) => {
      btn.addEventListener('click', () => {
        state.selectedTicketId = Number(btn.dataset.leadTicket);
        state.selectedSessionId = btn.dataset.leadSession || null;
        setView('tickets');
      });
    });
  } catch (e) {
    box.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

function sourceBadge(src) {
  if (src === 'desk-registration') return '<span class="badge onboard">desk</span>';
  if (src === 'wazuh') return '<span class="badge wazuh">wazuh</span>';
  if (src === 'vm112-onboard') return '<span class="badge onboard">onboard</span>';
  return src ? `<span class="badge">${esc(src)}</span>` : '';
}

function severityBadge(level) {
  if (level == null) return '';
  const n = Number(level);
  let cls = 'sev-low';
  if (n >= 12) cls = 'sev-critical';
  else if (n >= 10) cls = 'sev-high';
  else if (n >= 7) cls = 'sev-med';
  return `<span class="badge ${cls}">L${n}</span>`;
}

const FUNNEL_LABELS = {
  started: 'Iniciado',
  domain_validated: 'Domínio OK',
  dns_applied: 'DNS aplicado',
  account_created: 'Conta criada',
  infra_synced: 'Infra sync',
  completed: 'Concluído',
  failed: 'Falhou',
};

function funnelBarHtml(stages, total) {
  const order = ['started', 'domain_validated', 'dns_applied', 'account_created', 'infra_synced', 'completed', 'failed'];
  const max = Math.max(total || 1, ...order.map((k) => stages[k] || 0));
  return order
    .filter((k) => k !== 'failed' || (stages.failed || 0) > 0)
    .map((key) => {
      const n = stages[key] || 0;
      const pct = max ? Math.round((n / max) * 100) : 0;
      return `
        <div class="funnel-row">
          <span class="funnel-label">${FUNNEL_LABELS[key] || key}</span>
          <div class="funnel-bar"><div class="funnel-fill" style="width:${pct}%"></div></div>
          <strong class="funnel-count">${n}</strong>
        </div>`;
    })
    .join('');
}

function eventTypeLabel(ev) {
  const key = ev?.event_type || ev?.event;
  return SOC_EVENT_LABELS[key] || key || '—';
}

let _liveTimingTimer = null;

function formatDurationSec(seconds) {
  if (seconds == null || Number.isNaN(seconds)) return '—';
  const sec = Math.max(0, Math.round(Number(seconds)));
  if (sec < 60) return `${sec}s`;
  const mins = Math.floor(sec / 60);
  const rem = sec % 60;
  if (mins < 60) return `${mins}m ${rem}s`;
  const hrs = Math.floor(mins / 60);
  const m2 = mins % 60;
  if (hrs < 48) return `${hrs}h ${m2}m`;
  const days = Math.floor(hrs / 24);
  const h2 = hrs % 24;
  return `${days}d ${h2}h`;
}

function stopLiveTimingClock() {
  if (_liveTimingTimer) {
    clearInterval(_liveTimingTimer);
    _liveTimingTimer = null;
  }
}

function bindLiveTimingClock(root = document) {
  stopLiveTimingClock();
  const card = root.querySelector?.('[data-timing-live-card]');
  if (!card || card.dataset.timingCompleted === 'true') return;
  const startedAt = card.dataset.timingStartedAt;
  const lastAt = card.dataset.timingLastAt || startedAt;
  if (!startedAt) return;
  const totalEl = card.querySelector('[data-timing-live="total"]');
  const idleEl = card.querySelector('[data-timing-live="idle"]');
  const accEl = card.querySelector('[data-timing-live="accumulated"]');
  const tick = () => {
    const now = Date.now();
    const startMs = new Date(startedAt).getTime();
    const lastMs = new Date(lastAt).getTime();
    if (!Number.isNaN(startMs) && totalEl) {
      totalEl.textContent = formatDurationSec((now - startMs) / 1000);
    }
    if (!Number.isNaN(lastMs) && idleEl) {
      idleEl.textContent = formatDurationSec((now - lastMs) / 1000);
    }
    if (!Number.isNaN(startMs) && accEl) {
      accEl.textContent = `Σ ${formatDurationSec((now - startMs) / 1000)}`;
    }
  };
  tick();
  _liveTimingTimer = setInterval(tick, 1000);
}

function phaseTimingCardHtml(timing, events) {
  if (!timing || !window.DeskModules?.isEnabled('funnel-timing') || !events?.length) return '';
  const statusBadge = timing.is_completed
    ? '<span class="badge ok">concluído</span>'
    : `<span class="badge review">em curso</span>`;
  const lastEv = events[events.length - 1];
  const rows = events.map((ev, idx) => {
    const prev = idx > 0 ? (ev.duration_from_prev_label || '—') : '—';
    const isLastLive = !timing.is_completed && idx === events.length - 1;
    const total = isLastLive
      ? `<span data-timing-live="accumulated">Σ ${esc(timing.total_duration_label)}</span>`
      : `Σ ${esc(ev.duration_from_start_label || '—')}`;
    return `
      <tr>
        <td>${esc(eventTypeLabel(ev))}</td>
        <td class="timing-cell-time">${fmtDate(ev.created_at || ev.at)}</td>
        <td class="timing-cell-delta">${idx > 0 ? `<strong>+${esc(prev)}</strong>` : '—'}</td>
        <td class="timing-cell-total">${total}</td>
      </tr>`;
  }).join('');
  return `
    <div class="card phase-timing-card" data-timing-live-card
      data-timing-completed="${timing.is_completed ? 'true' : 'false'}"
      data-timing-started-at="${esc(timing.started_at || '')}"
      data-timing-last-at="${esc(timing.last_event_at || lastEv?.created_at || lastEv?.at || timing.started_at || '')}">
      <div class="phase-timing-head">
        <div>
          <h4 style="margin:0">Relógio por fase</h4>
          <p class="ticket-meta">Duração entre etapas do onboarding VM112</p>
        </div>
        ${statusBadge}
      </div>
      <div class="phase-timing-kpis">
        <div class="phase-timing-kpi">
          <span class="phase-timing-kpi-label">Tempo total</span>
          <span class="phase-timing-kpi-value${timing.is_completed ? '' : ' phase-timing-kpi-value--live'}"
            ${timing.is_completed ? '' : ' data-timing-live="total"'}>${esc(timing.total_duration_label)}</span>
        </div>
        <div class="phase-timing-kpi">
          <span class="phase-timing-kpi-label">Início</span>
          <span class="phase-timing-kpi-value phase-timing-kpi-value--sm">${fmtDate(timing.started_at)}</span>
        </div>
        ${timing.is_completed ? `
        <div class="phase-timing-kpi">
          <span class="phase-timing-kpi-label">Concluído</span>
          <span class="phase-timing-kpi-value phase-timing-kpi-value--sm">${fmtDate(timing.completed_at)}</span>
        </div>` : `
        <div class="phase-timing-kpi">
          <span class="phase-timing-kpi-label">Parado há</span>
          <span class="phase-timing-kpi-value${timing.is_completed ? '' : ' phase-timing-kpi-value--live'}"
            data-timing-live="idle">${esc(timing.idle_since_label || '—')}</span>
        </div>`}
      </div>
      <div class="table-wrap">
        <table class="phase-timing-table">
          <thead><tr><th>Fase</th><th>Registado</th><th>Δ fase</th><th>Acumulado</th></tr></thead>
          <tbody>${rows}</tbody>
        </table>
      </div>
    </div>`;
}

function timingSummaryHtml(timing) {
  if (!timing || !window.DeskModules?.isEnabled('funnel-timing')) return '';
  const idle = timing.is_completed ? '' : `<span class="timing-stat">Parado há <strong>${esc(timing.idle_since_label)}</strong></span>`;
  return `
    <div class="timing-summary">
      <span class="timing-stat">Total <strong>${esc(timing.total_duration_label)}</strong></span>
      ${idle}
      ${timing.completed_at ? `<span class="timing-stat">Concluído ${fmtDate(timing.completed_at)}</span>` : ''}
    </div>`;
}

function timelineHtml(events, timingMeta, opts = {}) {
  if (!events?.length) return '';
  const showTiming = !opts.compact && window.DeskModules?.isEnabled('funnel-timing');
  return `${!opts.compact ? timingSummaryHtml(timingMeta) : ''}<ol class="timeline">${events
    .map(
      (e, idx) => {
        const evt = e.event_type || e.event || '—';
        const at = e.created_at || e.at;
        const prevDur = showTiming && idx > 0 && e.duration_from_prev_label && e.duration_from_prev_label !== '—'
          ? `<span class="timing-badge" title="Desde fase anterior">+${esc(e.duration_from_prev_label)}</span>`
          : '';
        const fromStart = showTiming && e.duration_from_start_label
          ? `<span class="timing-badge timing-badge--muted" title="Desde início">Σ ${esc(e.duration_from_start_label)}</span>`
          : '';
        return `
      <li class="timeline-item">
        <span class="timeline-dot"></span>
        <div>
          <strong>${esc(evt)}</strong>
          ${e.stage ? `<span class="badge open">${esc(e.stage)}</span>` : ''}
          ${prevDur}${fromStart}
          <div class="ticket-meta">${fmtDate(at)}</div>
        </div>
      </li>`;
      }
    )
    .join('')}</ol>`;
}

function healthBadge(status) {
  const map = { healthy: 'ok', degraded: 'review', critical: 'closed', unknown: 'open' };
  const cls = map[status] || 'open';
  return `<span class="badge ${cls} health-${esc(status)}">${esc(status || 'unknown')}</span>`;
}

function checkStatusBadge(status) {
  const cls = { pass: 'ok', warn: 'review', fail: 'closed', error: 'closed', skip: 'open' }[status] || 'open';
  return `<span class="badge ${cls}">${esc(status)}</span>`;
}

function leadRowHtml(l) {
  return `
    <button type="button" class="lead-row" data-lead-ticket="${l.ticket_id}" data-lead-session="${esc(l.session_id || '')}">
      <div class="lead-row-top">
        <strong>${esc(l.domain || '—')}</strong>
        <span class="badge escalated">lead</span>
      </div>
      <div class="ticket-meta">${esc(l.email || 'sem e-mail')} · ${esc(FUNNEL_LABELS[l.funnel_stage] || l.funnel_stage || '—')}</div>
      <div class="ticket-meta">#${l.ticket_id} · parado ${fmtDate(l.last_event_at)}</div>
    </button>`;
}

function billingTicketIcon(t) {
  if ((t.subject || '').includes('[billing-validation]') || t.billing_state) return ' 💳';
  return '';
}

function ticketRowHtml(t) {
  const review = t.needs_review ? '<span class="badge review">revisão</span>' : '';
  const verified = t.account_verified ? '<span class="badge ok">verificado</span>' : '';
  const lead = t.crm_track === 'lead' ? '<span class="badge escalated">lead</span>' : '';
  const isOnboard = t.source === 'vm112-onboard' || t.event?.startsWith?.('onboarding') || t.event === 'session.started';
  const sub = t.event === 'wazuh.alert'
    ? esc(t.description || t.subject)
    : isOnboard && !t.domain
      ? `Onboarding VM112 · ${esc(FUNNEL_LABELS[t.lead_funnel_stage] || t.event || 'iniciado')}`
      : esc(t.domain || t.subject);
  const metaParts = [];
  if (isOnboard && t.session_id) metaParts.push(sessionHashHtml(t.session_id));
  if (t.event === 'wazuh.alert') {
    metaParts.push(esc(t.agent || t.domain || ''));
  } else if (t.email) {
    metaParts.push(esc(t.email));
  }
  metaParts.push(fmtDate(t.created_at));
  if (t.assigned_to) metaParts.push(esc(t.assigned_to));
  const meta = metaParts.filter(Boolean).join(' · ');
  return `
    <button type="button" class="ticket-row ${state.selectedTicketId === t.id ? 'selected' : ''}" data-id="${t.id}"${t.session_id ? ` data-session="${esc(t.session_id)}"` : ''}>
      <span class="badge ${t.status}">${esc(statusLabel(t.status))}</span>
      <div>
        <div class="ticket-subject">${sub}</div>
        <div class="ticket-meta ticket-meta--hash">${meta}</div>
      </div>
      <div>${lead}${sourceBadge(t.source)}${severityBadge(t.severity)}${review}${verified}</div>
    </button>`;
}

function assistActionsHtml(sessionId, meta, consoleExtra = {}) {
  if (!canAssist() || !sessionId) return '';
  const canAct = meta?.can_escalate;
  const assistStatus = normalizeAssistStatus(meta?.assist_status);
  const ticketStatus = meta?.ticket_status;
  const isAssisting = assistStatus === 'assisting' || ticketStatus === 'assisting';
  const isEscalated = assistStatus === 'escalated' || ticketStatus === 'escalated';
  const status = assistStatus || ticketStatus;
  const deskActions = (consoleExtra.actions || []).map((a) =>
    `<button type="button" class="btn btn-ghost btn-sm${a.danger ? ' btn-danger' : ''}" data-desk-action="${esc(a.id)}" title="${esc(a.label)}">${esc(a.label)}</button>`
  ).join('');
  const links = (consoleExtra.links || []).map((l) =>
    `<a class="btn btn-ghost btn-sm" href="${esc(l.url)}" target="_blank" rel="noopener">${esc(l.label)}</a>`
  ).join('');
  const audit = (meta?.actions || []).slice(-8).map((a) =>
    `<li><code>${esc(a.action)}</code> · ${esc(a.actor)} · ${fmtDate(a.created_at)}</li>`
  ).join('');
  return `
    <div class="assist-panel">
      <h4>Console de assistência</h4>
      <p class="ticket-meta">${assistBadge(status)}${meta?.assisted_by ? ` · ${esc(meta.assisted_by)}` : ''}</p>
      <div class="actions" style="margin-top:0.75rem;flex-wrap:wrap">
        ${!isAssisting && !isEscalated && canAct ? `<button type="button" class="btn btn-ghost btn-sm" data-assist="escalate">Escalar</button>` : ''}
        ${canAct && !isAssisting ? `<button type="button" class="btn btn-primary btn-sm" data-assist="takeover">Assumir sessão</button>` : ''}
        ${isAssisting ? `<button type="button" class="btn btn-ghost btn-sm" data-assist="handoff">Devolver ao cliente</button>` : ''}
        ${!canAct ? '<span class="ticket-meta">Intervenção disponível após domínio validado</span>' : ''}
      </div>
      ${deskActions ? `<div class="assist-console-actions"><h5>Acções Desk</h5><div class="actions">${deskActions}</div></div>` : ''}
      ${links ? `<div class="assist-console-links"><h5>Links externos</h5><div class="actions">${links}</div></div>` : ''}
      ${audit ? `<h5 style="margin-top:0.75rem">Movimento / audit</h5><ul class="audit-mini">${audit}</ul>` : ''}
    </div>`;
}

async function loadAssistMeta(sessionId) {
  if (!sessionId) return null;
  try {
    const [meta, actionsRes, linksRes] = await Promise.all([
      api(`/v1/assist/sessions/${encodeURIComponent(sessionId)}`),
      api(`/v1/assist/sessions/${encodeURIComponent(sessionId)}/actions`).catch(() => ({ actions: [] })),
      api(`/v1/assist/sessions/${encodeURIComponent(sessionId)}/links`).catch(() => ({ links: [] })),
    ]);
    return { ...meta, _console: { actions: actionsRes.actions || [], links: linksRes.links || [] } };
  } catch {
    return null;
  }
}

async function runAssistAction(action, sessionId) {
  const path = `/v1/assist/sessions/${encodeURIComponent(sessionId)}/${action}`;
  const result = await api(path, { method: 'POST' });
  if ((action === 'takeover' || action === 'resume-wizard') && result.takeover_url) {
    window.open(result.takeover_url, '_blank', 'noopener');
  }
  return result;
}

function bindAssistActions(container, sessionId) {
  container.querySelectorAll('[data-assist]').forEach((btn) => {
    btn.addEventListener('click', async () => {
      btn.disabled = true;
      try {
        await runAssistAction(btn.dataset.assist, sessionId);
        await renderTickets();
      } catch (e) {
        alert(e.message || 'Falha na ação de assistência');
      } finally {
        btn.disabled = false;
      }
    });
  });
  container.querySelectorAll('[data-desk-action]').forEach((btn) => {
    btn.addEventListener('click', async () => {
      const actionId = btn.dataset.deskAction;
      if (actionId === 'onboarding.abort' && !confirm('Abortar onboarding desta sessão?')) return;
      btn.disabled = true;
      try {
        await api(`/v1/assist/sessions/${encodeURIComponent(sessionId)}/actions/${encodeURIComponent(actionId)}`, { method: 'POST' });
        await renderTickets();
      } catch (e) {
        alert(e.message || 'Falha na ação');
      } finally {
        btn.disabled = false;
      }
    });
  });
}

function kpiCard(label, value, variant, opts = {}) {
  const click = opts.clickable ? ' kpi-card--click' : '';
  const jump = opts.viewJump ? ` data-view-jump="${opts.viewJump}"` : '';
  const title = opts.title ? ` title="${esc(opts.title)}"` : '';
  return `
    <div class="kpi-card kpi-card--${variant}${click}"${jump}${title}>
      <span class="kpi-card-glow" aria-hidden="true"></span>
      <div class="kpi-card-inner">
        <span class="kpi-value">${value}</span>
        <span class="kpi-label">${esc(label)}</span>
      </div>
    </div>`;
}

function dashboardPulseHtml({ audit, vm112, wazuh, vmOk, wazuhOk }) {
  const tenants = audit.tenants || [];
  const auditChips = tenants.map((t) => {
    const cls = t.status === 'healthy' ? 'ok' : t.status === 'degraded' ? 'warn' : 'alert';
    return `
      <div class="pulse-chip pulse-chip--${cls}">
        <span class="pulse-dot" aria-hidden="true"></span>
        <div class="pulse-body">
          <strong>${esc(t.name)}</strong>
          <span>${t.score?.pass ?? 0}/${t.score?.total ?? 8} checks</span>
        </div>
        ${healthBadge(t.status)}
      </div>`;
  }).join('');
  return `
    <div class="dashboard-pulse">
      ${auditChips}
      <div class="pulse-chip pulse-chip--${vmOk ? 'ok' : 'warn'}">
        <span class="pulse-dot" aria-hidden="true"></span>
        <div class="pulse-body">
          <strong>VM112 Portal</strong>
          <span>${esc(vm112.vm112?.service || vm112.error || '—')}</span>
        </div>
        <span class="badge ${vmOk ? 'ok' : 'review'}">${vmOk ? 'online' : 'check'}</span>
      </div>
      <div class="pulse-chip pulse-chip--${wazuhOk ? 'ok' : 'warn'}">
        <span class="pulse-dot" aria-hidden="true"></span>
        <div class="pulse-body">
          <strong>VM104 Wazuh</strong>
          <span>API ${wazuh.http_status ?? '—'}</span>
        </div>
        <span class="badge ${wazuhOk ? 'ok' : 'review'}">${wazuhOk ? 'online' : 'check'}</span>
      </div>
    </div>`;
}

function techRankingHtml(ranking) {
  if (!ranking?.length) return '<p class="ticket-meta">Sem movimento no período</p>';
  return `
    <table class="ranking-table">
      <thead><tr><th>#</th><th>Técnico</th><th>Assumidos</th><th>Escalados</th><th>Acções</th><th>Score</th></tr></thead>
      <tbody>
        ${ranking.slice(0, 8).map((r, i) => `
          <tr>
            <td>${i + 1}</td>
            <td><strong>${esc(r.username)}</strong></td>
            <td>${r.assumidos}</td>
            <td>${r.escalados}</td>
            <td>${r.acoes}</td>
            <td><strong>${r.score}</strong></td>
          </tr>`).join('')}
      </tbody>
    </table>`;
}

function dnsPurposeLabel(purpose) {
  return {
    mx: 'MX',
    spf: 'SPF',
    dkim: 'DKIM',
    dmarc: 'DMARC',
    'mail-host': 'Mail host',
    autodiscover: 'Autodiscover',
    'mail-alias': 'Alias',
    other: 'Outro',
  }[purpose] || purpose || '—';
}

async function fetchCloudflareDns(domain, emailService) {
  try {
    return await api(
      `/v1/dns/cloudflare/records?domain=${encodeURIComponent(domain)}&email_service=${emailService ? 'true' : 'false'}`
    );
  } catch (e) {
    return {
      domain,
      records: [],
      email_records: [],
      summary: { total: 0, email_related: 0 },
      error: e.message || 'Falha ao carregar DNS Cloudflare',
    };
  }
}

function isEmailServiceDomain(tenantId, funnelStage) {
  return tenantId === 1 || ['dns_applied', 'account_created', 'infra_synced', 'completed', 'company_validated', 'webmail_released'].includes(funnelStage);
}

async function showOverviewHomeDnsPanel(domain, tenantId, funnelStage, domainMeta = null) {
  const panel = document.getElementById('cf-dns-panel-body');
  const label = document.getElementById('cf-dns-domain-label');
  if (!panel) return;
  state.overviewHomeDnsDomain = domain;
  if (label) label.textContent = domain;
  panel.innerHTML = `<p class="cf-dns-empty">Carregando detalhes de <strong>${esc(domain)}</strong>…</p>`;

  let timing = domainMeta?.timing;
  let timeline = domainMeta?.timeline;
  if (window.DeskModules?.isEnabled('funnel-timing') && (!timing || !timeline?.length) && tenantId) {
    try {
      const details = await api(`/v1/audit/tenants/${tenantId}/details`);
      const match = (details.domains || []).find((item) => item.domain === domain);
      timing = match?.timing || timing;
      timeline = match?.timeline || timeline;
    } catch {
      /* mantém o que tiver */
    }
  }

  const timingCard = phaseTimingCardHtml(timing, timeline);
  const dns = await fetchCloudflareDns(domain, isEmailServiceDomain(tenantId, funnelStage));
  panel.innerHTML = `${timingCard}${htmlCloudflareDnsCardInline(dns)}`;
}

function htmlCloudflareDnsCardInline(dns) {
  if (!dns) {
    return '<p class="cf-dns-empty">Dados DNS indisponíveis.</p>';
  }
  if (dns.error && !dns.records?.length) {
    return `
      <p class="cf-dns-error">${esc(dns.error)}</p>
      ${dns.email_service ? '<p class="cf-dns-meta">Serviço: servidor de e-mail (onboarding)</p>' : ''}`;
  }
  const rows = (dns.records || []).map((r) => `
    <tr class="${r.email_related ? 'dns-email-row' : ''}">
      <td><span class="dns-purpose-badge purpose-${esc(r.purpose || 'other')}">${esc(dnsPurposeLabel(r.purpose))}</span></td>
      <td><code>${esc(r.name)}</code></td>
      <td><strong>${esc(r.type)}</strong></td>
      <td class="dns-content">${esc(r.content)}</td>
    </tr>`).join('');
  const summary = dns.summary || {};
  const zone = dns.zone || {};
  return `
    <div class="cf-dns-inline-summary">
      <div class="cf-metric-stat">
        <strong>${summary.total || 0}</strong>
        <span>registos na zona</span>
      </div>
      <div class="cf-metric-stat">
        <strong>${summary.email_related || 0}</strong>
        <span>para e-mail</span>
      </div>
      <span class="badge ${dns.email_service ? 'onboard' : 'open'}">${dns.email_service ? 'E-mail' : 'DNS'}</span>
    </div>
    <p class="cf-dns-meta">Zona <code>${esc(zone.name || '—')}</code>${dns.error ? ` · ${esc(dns.error)}` : ''}</p>
    <div class="cf-dns-table-wrap">
      <table class="data-table dns-records-table dns-records-table-compact">
        <thead><tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th></tr></thead>
        <tbody>${rows || '<tr><td colspan="4">Sem registos para este domínio.</td></tr>'}</tbody>
      </table>
    </div>`;
}

function htmlCloudflareDnsCard(dns) {
  if (!dns) {
    return `
      <div class="modal-section dns-records-section">
        <h4>Apontamentos DNS (Cloudflare)</h4>
        <p class="loading">Dados DNS indisponíveis.</p>
      </div>`;
  }
  if (dns.error && !dns.records?.length) {
    return `
      <div class="modal-section">
        <h4>Apontamentos DNS (Cloudflare)</h4>
        <p class="ticket-meta">${esc(dns.error)}</p>
        ${dns.email_service ? '<p class="ticket-meta">Serviço: servidor de e-mail (onboarding)</p>' : ''}
      </div>`;
  }
  const rows = (dns.records || []).map((r) => `
    <tr class="${r.email_related ? 'dns-email-row' : ''}">
      <td><span class="dns-purpose-badge purpose-${esc(r.purpose || 'other')}">${esc(dnsPurposeLabel(r.purpose))}</span></td>
      <td><code>${esc(r.name)}</code></td>
      <td><strong>${esc(r.type)}</strong>${r.priority != null ? ` <span class="ticket-meta">prio ${r.priority}</span>` : ''}</td>
      <td class="dns-content">${esc(r.content)}</td>
      <td class="ticket-meta">${r.proxied ? 'proxy' : 'DNS only'} · TTL ${r.ttl ?? '—'}</td>
    </tr>`).join('');
  const summary = dns.summary || {};
  const zone = dns.zone || {};
  return `
    <div class="modal-section dns-records-section">
      <div class="card-head-row">
        <h4>Apontamentos DNS (Cloudflare)</h4>
        <span class="badge ${dns.email_service ? 'onboard' : 'open'}">${dns.email_service ? 'Servidor de e-mail' : 'DNS geral'}</span>
      </div>
      <p class="ticket-meta" style="margin:0 0 0.65rem">
        Zona <code>${esc(zone.name || '—')}</code> · ${summary.total || 0} registo(s)
        · ${summary.email_related || 0} para e-mail
        ${dns.error ? ` · <span class="badge review">${esc(dns.error)}</span>` : ''}
      </p>
      <div class="table-wrap">
        <table class="data-table dns-records-table">
          <thead>
            <tr><th>Função</th><th>Nome</th><th>Tipo</th><th>Conteúdo</th><th>Estado</th></tr>
          </thead>
          <tbody>${rows || '<tr><td colspan="5">Sem registos DNS para este domínio na zona Cloudflare.</td></tr>'}</tbody>
        </table>
      </div>
    </div>`;
}

function executionStatusBadge(status) {
  const map = {
    in_progress: ['assisting', 'em execução'],
    completed: ['ok', 'concluído'],
    failed: ['escalated', 'falhou'],
    registered: ['open', 'registado'],
  };
  const [cls, label] = map[status] || ['open', status || '—'];
  return `<span class="badge ${cls}">${esc(label)}</span>`;
}

function bindOverviewModal() {
  document.querySelectorAll('[data-close-overview-modal]').forEach((el) => {
    el.addEventListener('click', closeOverviewModal);
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeOverviewModal();
  });
}

function closeOverviewModal() {
  const modal = document.getElementById('overview-modal');
  if (!modal) return;
  modal.classList.add('hidden');
  modal.setAttribute('aria-hidden', 'true');
  state.overviewModal = { tenantId: null, view: 'list', domain: null, data: null, focus: 'onboard' };
}

function renderWazuhOverviewCard(t) {
  const issues = (t.top_issues || [])
    .slice(0, 3)
    .map((i) => `<li><code>${esc(i.domain)}</code> · ${esc(i.check_id)} — ${esc(i.message || i.status)}</li>`)
    .join('');
  const apiLabel = t.api_online ? `API online (${t.http_status || '—'})` : 'API offline';
  return `
    <button type="button" class="card health-card health-${esc(t.status)} health-card--click health-card--wazuh" data-open-overview="${t.tenant_id}">
      <div class="health-card-head">
        <div>
          <h3 style="margin:0">${esc(t.name)}</h3>
          <p class="ticket-meta">${esc(t.ip)} · <strong>${t.alerts_total || 0}</strong> alerta(s) · <strong>${t.agents_count || 0}</strong> agente(s)</p>
        </div>
        ${healthBadge(t.status)}
      </div>
      <div class="health-score wazuh-score">
        <span>${t.alerts_high || 0} alto (L≥${t.min_ticket_level || 10})</span>
        <span>${t.open_tickets || 0} ticket(s) aberto(s)</span>
        <span class="${t.api_online ? 'wazuh-api-ok' : 'wazuh-api-bad'}">${esc(apiLabel)}</span>
      </div>
      <p class="ticket-meta">Último alerta: ${fmtDate(t.last_alert_at)}</p>
      ${issues ? `<ul class="issue-list">${issues}</ul>` : '<p class="loading">Sem alertas Wazuh registados — integração ativa aguarda eventos.</p>'}
      <p class="health-card-hint">Clique para ver agentes, alertas por nível e tickets SOC</p>
    </button>`;
}

function renderWazuhSocModal(data) {
  const body = document.getElementById('overview-modal-body');
  const title = document.getElementById('overview-modal-title');
  const sub = document.getElementById('overview-modal-sub');
  if (!body || !title || !sub) return;
  const s = data.summary || {};
  title.textContent = data.name || 'Wazuh SOC';
  sub.textContent = `${data.ip || '—'} · API ${s.api_online ? 'online' : 'offline'} · gerado ${fmtDate(data.generated_at)}`;

  const agentRows = (data.agents || []).map((a) => `
    <tr>
      <td><strong>${esc(a.agent)}</strong></td>
      <td>${esc(a.agent_ip || '—')}</td>
      <td>${a.alerts_count}</td>
      <td>L${a.max_level}</td>
      <td>${relativeTimeAgo(a.last_seen)}</td>
    </tr>`).join('');

  const alertRows = (data.alerts || []).slice(0, 40).map((a) => `
    <tr>
      <td>${severityBadge(a.level)}</td>
      <td>${esc(a.agent)}</td>
      <td>${esc(a.description || '—')}</td>
      <td>${esc(a.srcip || '—')}</td>
      <td>${esc(a.agent_ip || '—')}</td>
      <td>${relativeTimeAgo(a.created_at)}</td>
    </tr>`).join('');

  const ticketRows = (data.tickets || []).slice(0, 15).map((t) => `
    <button type="button" class="ticket-row" data-open-ticket="${t.id}">
      <span class="badge ${t.status}">${esc(statusLabel(t.status))}</span>
      <div>
        <div class="ticket-subject">${esc(t.subject)}</div>
        <div class="ticket-meta">#${t.id} · ${fmtDate(t.created_at)}</div>
      </div>
    </button>`).join('');

  body.innerHTML = `
    <div class="overview-summary wazuh-summary">
      <div class="overview-stat"><strong>${s.alerts_total || 0}</strong><span>Alertas</span></div>
      <div class="overview-stat"><strong>${s.alerts_24h || 0}</strong><span>24h</span></div>
      <div class="overview-stat"><strong>${s.agents_total || 0}</strong><span>Agentes</span></div>
      <div class="overview-stat"><strong>${s.level_10_plus || 0}</strong><span>L≥${data.min_ticket_level || 10}</span></div>
      <div class="overview-stat"><strong>${s.open_tickets || 0}</strong><span>Tickets</span></div>
    </div>
    <p class="ticket-meta" style="margin:0 0 0.75rem">
      Monitorização de segurança VM104 — webhooks <code>wazuh.alert</code> com nível ≥ ${data.min_ticket_level || 10} geram ticket na VM122.
    </p>
    <div class="wazuh-modal-grid">
      <div class="modal-section">
        <h4>Agentes monitorados</h4>
        ${agentRows ? `
        <div class="wazuh-table-wrap">
          <table class="wazuh-table">
            <thead><tr><th>Agente</th><th>IP</th><th>Alertas</th><th>Máx</th><th>Último</th></tr></thead>
            <tbody>${agentRows}</tbody>
          </table>
        </div>` : '<p class="loading">Nenhum agente com alertas registados.</p>'}
      </div>
      <div class="modal-section">
        <h4>Feed de alertas</h4>
        ${alertRows ? `
        <div class="wazuh-table-wrap">
          <table class="wazuh-table">
            <thead><tr><th>Nível</th><th>Agente</th><th>Descrição</th><th>Src IP</th><th>Agent IP</th><th>Hora</th></tr></thead>
            <tbody>${alertRows}</tbody>
          </table>
        </div>` : '<p class="loading">Sem alertas.</p>'}
      </div>
    </div>
    ${ticketRows ? `
    <div class="modal-section" style="margin-top:0.75rem">
      <h4>Tickets Wazuh</h4>
      <div class="ticket-list ticket-list-compact">${ticketRows}</div>
    </div>` : ''}`;

  body.querySelectorAll('[data-open-ticket]').forEach((btn) => {
    btn.addEventListener('click', () => {
      state.selectedTicketId = Number(btn.dataset.openTicket);
      closeOverviewModal();
      setView('tickets');
    });
  });
}

function wizardSecuritySeverityBadge(sev) {
  const map = { high: ['escalated', 'Alto'], critical: ['escalated', 'Crítico'], warn: ['review', 'Atenção'], info: ['open', 'Info'] };
  const [cls, label] = map[sev] || ['open', sev || '—'];
  return `<span class="badge ${cls}">${esc(label)}</span>`;
}

function wizardSecurityEventLabel(ev) {
  return SECURITY_EVENT_LABELS[ev] || SOC_EVENT_LABELS[ev] || ev || '—';
}

const SECURITY_EVENT_LABELS = {
  'security.csp_violation': 'Violação CSP',
  'security.input_warn': 'Input suspeito',
  'security.input_blocked': 'Input bloqueado',
  'security.rate_limited': 'Rate limit',
  'security.handoff_created': 'Handoff criado',
  'security.handoff_consumed': 'Handoff consumido',
  'security.handoff_rejected': 'Handoff rejeitado',
  'security.handoff_expired': 'Handoff expirado',
  'security.auth_failed': 'Auth portal falhou',
  'security.session_anomaly': 'Anomalia sessão',
};

const WIZARD_SEC_COLORS = {
  teal: '#0d9488',
  tealLight: '#14b8a6',
  orange: '#ea580c',
  orangeLight: '#f97316',
  severe: '#7f1d1d',
  high: '#dc2626',
  elevated: '#ea580c',
  guarded: '#eab308',
  low: '#22c55e',
  na: '#94a3b8',
  csp: '#0891b2',
  input: '#dc2626',
  handoff: '#ea580c',
  auth: '#7c3aed',
  rate: '#64748b',
};

function wizardSecRiskScore(severity, eventType) {
  if (severity === 'critical') return 5;
  if (severity === 'high' || (eventType || '').includes('blocked') || (eventType || '').includes('rejected')) return 4;
  if (severity === 'warn' || (eventType || '').includes('csp')) return 3;
  if (severity === 'info') return 2;
  return 1;
}

function wizardSecRiskCell(score) {
  const map = {
    5: ['Severo', WIZARD_SEC_COLORS.severe],
    4: ['Alto', WIZARD_SEC_COLORS.high],
    3: ['Elevado', WIZARD_SEC_COLORS.elevated],
    2: ['Vigiado', WIZARD_SEC_COLORS.guarded],
    1: ['Baixo', WIZARD_SEC_COLORS.low],
    0: ['N/A', WIZARD_SEC_COLORS.na],
  };
  const [label, bg] = map[score] || map[0];
  return `<span class="ws-threat-level" style="background:${bg}">${esc(label)}</span>`;
}

function wizardSecDonutSvg(segments, size = 130) {
  const filtered = segments.filter((s) => s.value > 0);
  const total = filtered.reduce((a, s) => a + s.value, 0) || 1;
  const r = 42;
  const cx = size / 2;
  const cy = size / 2;
  const circ = 2 * Math.PI * r;
  let offset = 0;
  const arcs = filtered.map((s) => {
    const len = (s.value / total) * circ;
    const el = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${s.color}" stroke-width="16" stroke-dasharray="${len} ${circ - len}" stroke-dashoffset="${-offset}" transform="rotate(-90 ${cx} ${cy})"/>`;
    offset += len;
    return el;
  }).join('');
  return `<svg class="ws-donut" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" aria-hidden="true">${arcs}<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="middle" font-size="18" font-weight="700" fill="#0f172a">${total}</text></svg>`;
}

function wizardSecVBarSvg(items, width = 260, height = 120) {
  if (!items.length) return '<p class="ws-empty-chart">Sem dados</p>';
  const max = Math.max(...items.map((i) => i.value), 1);
  const gap = 10;
  const barW = Math.max(18, (width - gap * (items.length + 1)) / items.length);
  const bars = items.map((item, i) => {
    const bh = Math.max(2, (item.value / max) * (height - 36));
    const x = gap + i * (barW + gap);
    const y = height - 24 - bh;
    return `
      <rect x="${x}" y="${y}" width="${barW}" height="${bh}" fill="${item.color}" rx="3"/>
      <text x="${x + barW / 2}" y="${height - 6}" text-anchor="middle" font-size="9" fill="#64748b">${esc(item.short || item.label)}</text>
      <text x="${x + barW / 2}" y="${y - 4}" text-anchor="middle" font-size="10" font-weight="600" fill="#0f172a">${item.value}</text>`;
  }).join('');
  return `<svg class="ws-vbar" viewBox="0 0 ${width} ${height}" width="100%" height="${height}" aria-hidden="true">${bars}</svg>`;
}

function wizardSecHBarHtml(items) {
  if (!items.length) return '<p class="ws-empty-chart">Sem dados</p>';
  const max = Math.max(...items.map((i) => i.value), 1);
  return items.map((item) => `
    <div class="ws-hbar-row">
      <span class="ws-hbar-label">${esc(item.label)}</span>
      <div class="ws-hbar-track"><div class="ws-hbar-fill" style="width:${Math.round((item.value / max) * 100)}%;background:${item.color}"></div></div>
      <span class="ws-hbar-val">${item.value}</span>
    </div>`).join('');
}

function wizardSecVectorBucket(eventType) {
  const ev = eventType || '';
  if (ev.includes('csp')) return 'csp';
  if (ev.includes('input') || ev.includes('rate')) return 'input';
  if (ev.includes('handoff')) return 'handoff';
  if (ev.includes('auth') || ev.includes('session')) return 'auth';
  return 'outro';
}

function wizardSecAccessStatus(s) {
  if ((s.inputs_blocked || 0) + (s.handoffs_rejected || 0) > 0) return 'critical';
  if ((s.total || 0) > 0) return 'degraded';
  return 'healthy';
}

function renderUserAccessOverviewCard(sec) {
  if (!window.DeskModules?.isEnabled('wizard-security')) return '';
  const s = sec || { total: 0, inputs_blocked: 0, handoffs_rejected: 0, csp_violations: 0, sessions_with_alerts: 0, recent: [] };
  const status = wizardSecAccessStatus(s);
  const issues = (s.recent || []).slice(0, 3).map((ev) =>
    `<li><code>${esc((ev.client_ip || '—'))}</code> · ${esc(wizardSecurityEventLabel(ev.event_type))} — ${ev.session_id ? sessionHashHtml(ev.session_id, { full: false }) : 'sem sessão'}</li>`
  ).join('');
  return `
    <button type="button" class="card health-card health-${status} health-card--click ws-access-overview-card" data-open-user-access="1">
      <div class="health-card-head">
        <div>
          <h3 style="margin:0">Acesso Usuário — Cybersecurity</h3>
          <p class="ticket-meta">Portal público · browser · handoff · <strong>não</strong> é o wizard VM112</p>
        </div>
        ${healthBadge(status)}
      </div>
      <div class="health-score">${s.total || 0} alerta(s) · ${s.inputs_blocked || 0} bloq · ${s.handoffs_rejected || 0} handoff · ${s.csp_violations || 0} CSP</div>
      <p class="ticket-meta">${s.sessions_with_alerts || 0} sessão(ões) · janela 24h</p>
      ${issues ? `<ul class="issue-list">${issues}</ul>` : '<p class="loading">Sem alertas de acesso nas últimas 24h</p>'}
      <p class="health-card-hint">Clique para ver dashboard de ameaças, guia técnico e relatório</p>
    </button>`;
}

function renderWizardSecurityCard(sec, opts = {}) {
  const standalone = opts.standalone === true;
  if (!window.DeskModules?.isEnabled('wizard-security')) return '';
  const s = sec || { total: 0, csp_violations: 0, inputs_blocked: 0, handoffs_rejected: 0, sessions_with_alerts: 0, recent: [], by_event: {} };
  const byEvent = s.by_event || {};
  const recent = s.recent || [];

  const severityCounts = { high: 0, warn: 0, info: 0 };
  recent.forEach((ev) => {
    const sev = ev.severity || 'info';
    if (sev === 'high' || sev === 'critical') severityCounts.high += 1;
    else if (sev === 'warn') severityCounts.warn += 1;
    else severityCounts.info += 1;
  });

  const eventBars = Object.entries(byEvent).map(([ev, count]) => ({
    label: wizardSecurityEventLabel(ev),
    short: wizardSecurityEventLabel(ev).split(' ')[0],
    value: count,
    color: ev.includes('blocked') || ev.includes('rejected') ? WIZARD_SEC_COLORS.high
      : ev.includes('csp') ? WIZARD_SEC_COLORS.csp
      : ev.includes('handoff') ? WIZARD_SEC_COLORS.handoff
      : WIZARD_SEC_COLORS.teal,
  })).slice(0, 6);

  const vectors = { csp: 0, input: 0, handoff: 0, auth: 0 };
  Object.entries(byEvent).forEach(([ev, count]) => {
    const bucket = wizardSecVectorBucket(ev);
    if (vectors[bucket] != null) vectors[bucket] += count;
  });
  const vectorBars = [
    { label: 'CSP Browser', value: vectors.csp, color: WIZARD_SEC_COLORS.csp },
    { label: 'Input audit', value: vectors.input, color: WIZARD_SEC_COLORS.input },
    { label: 'Handoff', value: vectors.handoff, color: WIZARD_SEC_COLORS.handoff },
    { label: 'Auth/Sessão', value: vectors.auth, color: WIZARD_SEC_COLORS.auth },
  ];

  const ipMap = {};
  recent.forEach((ev) => {
    const ip = ev.client_ip || '—';
    ipMap[ip] = (ipMap[ip] || 0) + 1;
  });
  const topIps = Object.entries(ipMap).sort((a, b) => b[1] - a[1]).slice(0, 5);

  const riskBars = [
    { label: 'Input bloqueado', value: s.inputs_blocked || 0, color: WIZARD_SEC_COLORS.high },
    { label: 'Handoff rejeitado', value: s.handoffs_rejected || 0, color: WIZARD_SEC_COLORS.orange },
    { label: 'Violação CSP', value: s.csp_violations || 0, color: WIZARD_SEC_COLORS.csp },
    { label: 'Input suspeito', value: s.inputs_warn || 0, color: WIZARD_SEC_COLORS.guarded },
    { label: 'Rate limit', value: s.rate_limited || 0, color: WIZARD_SEC_COLORS.na },
  ];

  const threatRows = recent.slice(0, 8).map((ev) => {
    const score = wizardSecRiskScore(ev.severity, ev.event_type);
    return `
      <tr class="wizard-sec-row" data-wizard-sec-session="${esc(ev.session_id || '')}">
        <td>${esc(wizardSecurityEventLabel(ev.event_type))}</td>
        <td>${wizardSecRiskCell(score)}</td>
        <td>${ev.session_id ? sessionHashHtml(ev.session_id, { full: false }) : '—'}</td>
        <td><code>${esc(ev.client_ip || '—')}</code></td>
        <td>${fmtDate(ev.created_at)}</td>
      </tr>`;
  }).join('');

  const accessStatus = wizardSecAccessStatus(s);
  const issueLines = recent.slice(0, 3).map((ev) =>
    `<li><code>${esc(ev.client_ip || '—')}</code> · ${esc(wizardSecurityEventLabel(ev.event_type))} — ${ev.session_id ? `<code>${esc(ev.session_id.slice(0, 14))}…</code>` : 'sem sessão'}</li>`
  ).join('');

  const dashboardGrid = `
      <div class="ws-dash-grid ws-dash-grid--inner">
        <article class="ws-panel ws-panel--teal">
          <header class="ws-panel-head ws-panel-head--teal">Eventos nas últimas 24h</header>
          <div class="ws-panel-body">${wizardSecVBarSvg(eventBars.length ? eventBars : [{ label: 'Nenhum', short: '—', value: 0, color: WIZARD_SEC_COLORS.na }])}</div>
        </article>
        <article class="ws-panel ws-panel--orange">
          <header class="ws-panel-head ws-panel-head--orange">Risco actual</header>
          <div class="ws-panel-body ws-panel-body--donut">
            ${wizardSecDonutSvg([
              { value: severityCounts.high, color: WIZARD_SEC_COLORS.high },
              { value: severityCounts.warn, color: WIZARD_SEC_COLORS.elevated },
              { value: severityCounts.info, color: WIZARD_SEC_COLORS.low },
            ])}
            <ul class="ws-legend">
              <li><span style="background:${WIZARD_SEC_COLORS.high}"></span>Alto (${severityCounts.high})</li>
              <li><span style="background:${WIZARD_SEC_COLORS.elevated}"></span>Elevado (${severityCounts.warn})</li>
              <li><span style="background:${WIZARD_SEC_COLORS.low}"></span>Baixo (${severityCounts.info})</li>
            </ul>
          </div>
        </article>
        <article class="ws-panel ws-panel--teal">
          <header class="ws-panel-head ws-panel-head--teal">Ameaças por vetor</header>
          <div class="ws-panel-body">${wizardSecVBarSvg(vectorBars)}</div>
        </article>
        <article class="ws-panel ws-panel--orange">
          <header class="ws-panel-head ws-panel-head--orange">IPs com atividade</header>
          <div class="ws-panel-body ws-panel-body--ips">
            ${topIps.length ? topIps.map(([ip, n], i) => `
              <div class="ws-ip-row">
                <span class="ws-ip-rank">${i + 1}</span>
                <code class="ws-ip-addr">${esc(ip)}</code>
                <span class="ws-ip-count" style="background:${i === 0 && n > 1 ? WIZARD_SEC_COLORS.high : WIZARD_SEC_COLORS.orange}">${n} evt</span>
              </div>`).join('') : '<p class="ws-empty-chart">Nenhum IP registado</p>'}
          </div>
        </article>
        <article class="ws-panel ws-panel--teal">
          <header class="ws-panel-head ws-panel-head--teal">Risco por categoria</header>
          <div class="ws-panel-body">${wizardSecHBarHtml(riskBars)}</div>
        </article>
        <article class="ws-panel ws-panel--orange">
          <header class="ws-panel-head ws-panel-head--orange">Relatório de ameaças</header>
          <div class="ws-panel-body ws-panel-body--table">
            <table class="ws-threat-table">
              <thead><tr><th>Ameaça</th><th>Nível</th><th>Sessão</th><th>IP</th><th>Hora</th></tr></thead>
              <tbody>${threatRows || '<tr><td colspan="5">Sem ameaças nas últimas 24h</td></tr>'}</tbody>
            </table>
          </div>
        </article>
      </div>`;

  return `
    <section class="ws-access-zone" id="overview-access-security" aria-label="Acesso de usuário — cibersegurança">
      <div class="ws-zone-banner">
        <span class="ws-zone-tag">Área independente</span>
        <h3 class="ws-zone-title">Acesso de usuário — Cibersegurança</h3>
        <p class="ws-zone-desc">Eventos gerados quando alguém acede ao <strong>portal público</strong>, preenche formulários ou faz login (handoff). Isto é <em>segurança de acesso</em> — não mede DNS, Carbonio, certificados nem progresso do wizard VM112.</p>
      </div>

      <div class="card health-card health-${accessStatus} ws-access-health-card">
        <div class="health-card-head">
          <div>
            <h3 style="margin:0">Threat tracking — portal &amp; sessões</h3>
            <p class="ticket-meta">Browser · CSP · inputs · handoff · Spec 021</p>
          </div>
          ${healthBadge(accessStatus)}
        </div>
        <div class="health-score">${s.total || 0} alerta(s) · ${s.inputs_blocked || 0} bloq · ${s.handoffs_rejected || 0} handoff · ${s.csp_violations || 0} CSP · ${s.sessions_with_alerts || 0} sessões</div>
        <p class="ticket-meta">Janela ${s.window_hours || 24}h · origem <code>vm112-security</code></p>
        ${issueLines ? `<ul class="issue-list">${issueLines}</ul>` : '<p class="ticket-meta">Nenhum incidente de acesso nas últimas 24h</p>'}
        <div class="ws-dash-inner">${dashboardGrid}</div>
        <footer class="ws-dash-foot">
          <button type="button" class="btn btn-ghost btn-sm" data-wizard-sec-goto-events>Ver todos em Eventos → Segurança</button>
          ${standalone ? '<button type="button" class="btn btn-ghost btn-sm" data-open-onboard-from-access>Ver VM112 Onboard →</button>' : ''}
        </footer>
      </div>

      <div class="ws-info-cards-row">
        <article class="card ws-info-card ws-info-card--teal">
          <header class="ws-info-card-head ws-info-card-head--teal">O que monitorizamos</header>
          <div class="ws-info-card-body">
            <p>Este painel cobre apenas o <strong>comportamento de quem acede</strong> ao sistema — visitantes, clientes no portal e tentativas de abuso em formulários públicos.</p>
            <ul class="ws-info-list">
              <li><strong>CSP (browser)</strong> — scripts ou recursos bloqueados no navegador do usuário</li>
              <li><strong>Input audit</strong> — padrões SQL/XSS em campos enviados pelo usuário</li>
              <li><strong>Handoff</strong> — token de login expirado, reutilizado ou inválido</li>
              <li><strong>Auth / sessão</strong> — falhas de autenticação ou anomalias de sessão</li>
            </ul>
            <p class="ws-info-note">${standalone
    ? 'Domínios, DNS e Carbonio estão no card <strong>Ligbox Datacenter — Node VM001</strong> — área separada.'
    : '≠ Saúde do wizard VM112 (domínios, e-mail, certificados) — ver secção <em>Onboard</em> abaixo.'}</p>
          </div>
        </article>
        <article class="card ws-info-card ws-info-card--orange">
          <header class="ws-info-card-head ws-info-card-head--orange">Como proceder — guia técnico</header>
          <div class="ws-info-card-body">
            <ol class="ws-info-steps">
              <li><strong>Input bloqueado / CSP</strong> — Anote hash + IP. Repetição ≥3×/10 min → escale. Provável ataque, não erro de cliente.</li>
              <li><strong>Handoff rejeitado</strong> — Cliente legítimo refaz login. Mesmo IP repetido → scraping (ticket automático).</li>
              <li><strong>Correlacionar</strong> — Tickets → Onboard → hash da sessão. Compare com funil.</li>
              <li><strong>Takeover</strong> — Só com cliente confirmado. Alerta Alto: validar identidade antes de ver credenciais.</li>
              <li><strong>Falso positivo</strong> — Domínios com caracteres especiais podem gerar <em>input_warn</em>.</li>
              <li><strong>Escalação</strong> — Mesmo IP em várias sessões bloqueadas → Chefe Ops / firewall.</li>
            </ol>
          </div>
        </article>
      </div>
    </section>
    ${standalone ? '' : `
    <div class="ws-zone-divider" role="separator">
      <span>Ligbox Datacenter — Node VM001 — wizard, domínios &amp; infraestrutura</span>
    </div>`}`;
}

function bindWizardSecurityCard(root) {
  root.querySelector('[data-wizard-sec-goto-events]')?.addEventListener('click', () => {
    state.eventsTab = 'security';
    state.eventSourceFilter = 'vm112-security';
    closeOverviewModal();
    setView('events');
  });
  root.querySelector('[data-open-onboard-from-access]')?.addEventListener('click', () => {
    openOverviewModal(1, { focus: 'onboard' });
  });
  root.querySelectorAll('[data-wizard-sec-session]').forEach((row) => {
    const sid = row.dataset.wizardSecSession;
    if (!sid) return;
    row.style.cursor = 'pointer';
    row.addEventListener('click', () => {
      state.selectedSessionId = sid;
      closeOverviewModal();
      setView('tickets');
    });
  });
}

function renderOverviewModalList(data) {
  if (data.kind === 'wazuh_soc' && !window.DeskModules?.isEnabled('wazuh-soc')) {
    data = { ...data, kind: 'audit', domains: data.domains || [] };
  }
  if (data.kind === 'wazuh_soc') {
    renderWazuhSocModal(data);
    return;
  }
  const body = document.getElementById('overview-modal-body');
  const title = document.getElementById('overview-modal-title');
  const sub = document.getElementById('overview-modal-sub');
  if (!body || !title || !sub) return;
  const s = data.summary || {};
  const focus = state.overviewModal?.focus || 'onboard';
  const showAccess = focus === 'access' && data.tenant_id === 1 && window.DeskModules?.isEnabled('wizard-security');

  if (showAccess) {
    const sec = data.security || {};
    title.textContent = 'Acesso Usuário — Cybersecurity';
    sub.textContent = `Portal & sessões · ${sec.total || 0} alerta(s) 24h · ${sec.sessions_with_alerts || 0} sessão(ões) · gerado ${fmtDate(data.generated_at)}`;
    body.innerHTML = renderWizardSecurityCard(sec, { standalone: true });
    bindWizardSecurityCard(body);
    return;
  }

  title.textContent = data.name || 'Detalhes do tenant';
  sub.textContent = `${data.ip || '—'} · ${s.domains_total || 0} domínio(s) · wizard & infra · gerado ${fmtDate(data.generated_at)}`;
  const rows = (data.domains || []).map((d) => {
    const issuePreview = (d.issues || []).slice(0, 2).map((i) =>
      `<li><code>${esc(i.check_id)}</code> — ${esc(i.message || i.status)}</li>`
    ).join('');
    return `
      <button type="button" class="overview-domain-row" data-overview-domain="${esc(d.domain)}">
        <div class="overview-domain-top">
          <strong>${esc(d.domain)}</strong>
          <div style="display:flex;gap:0.35rem;flex-wrap:wrap">
            ${executionStatusBadge(d.execution_status)}
            ${healthBadge(d.audit_status)}
          </div>
        </div>
        <div class="overview-domain-meta">
          <span>${esc(d.email || 'sem e-mail')}</span>
          <span>${esc(d.funnel_stage_label || d.funnel_stage || '—')}</span>
          <span>início ${fmtDate(d.started_at)}</span>
          <span>último ${fmtDate(d.last_event_at)}</span>
          ${d.timing && window.DeskModules?.isEnabled('funnel-timing') ? `<span>total ${esc(d.timing.total_duration_label)}</span>` : ''}
          <span>IP ${esc(d.client_ip || '—')}</span>
          ${d.ticket_id ? `<span>ticket #${d.ticket_id}</span>` : ''}
        </div>
        ${issuePreview ? `<ul class="overview-domain-issues">${issuePreview}</ul>` : ''}
      </button>`;
  }).join('');
  body.innerHTML = `
    <section class="overview-tenant-zone" id="overview-tenant-onboard">
      <div class="overview-zone-label">
        <h4>Ligbox Datacenter — Node VM001</h4>
        <p class="ticket-meta">Saúde do wizard, domínios em onboarding, DNS, certificados e Carbonio</p>
        ${window.DeskModules?.isEnabled('wizard-security') ? '<button type="button" class="btn btn-ghost btn-sm" data-open-access-from-onboard>← Acesso Usuário — Cybersecurity</button>' : ''}
      </div>
      <div class="overview-summary">
        <div class="overview-stat"><strong>${s.domains_total || 0}</strong><span>Total</span></div>
        <div class="overview-stat"><strong>${s.in_progress || 0}</strong><span>Em execução</span></div>
        <div class="overview-stat"><strong>${s.completed || 0}</strong><span>Concluídos</span></div>
        <div class="overview-stat"><strong>${s.failed || 0}</strong><span>Falharam</span></div>
        <div class="overview-stat"><strong>${s.with_issues || 0}</strong><span>Com erros</span></div>
      </div>
      <p class="ticket-meta" style="margin:0 0 0.75rem">Clique num domínio para ver apontamentos DNS Cloudflare, timeline, checks e IP de acesso.</p>
      ${rows || '<p class="loading">Nenhum domínio auditado neste tenant.</p>'}
    </section>`;
  body.querySelector('[data-open-access-from-onboard]')?.addEventListener('click', () => {
    openUserAccessModal();
  });
  body.querySelectorAll('[data-overview-domain]').forEach((btn) => {
    btn.addEventListener('click', () => openOverviewDomainDetail(btn.dataset.overviewDomain));
  });
}

async function openOverviewDomainDetail(domain) {
  const body = document.getElementById('overview-modal-body');
  const data = state.overviewModal.data;
  if (!body || !data) return;
  const d = (data.domains || []).find((item) => item.domain === domain);
  if (!d) return;
  state.overviewModal.view = 'domain';
  state.overviewModal.domain = domain;
  body.innerHTML = '<p class="loading">Carregando detalhes…</p>';
  let checks = d.issues || [];
  const isEmailService = isEmailServiceDomain(data.tenant_id, d.funnel_stage);
  try {
    const sc = await api(`/v1/audit/tenants/${data.tenant_id}/scorecard?domain=${encodeURIComponent(domain)}`);
    checks = sc.checks || checks;
  } catch {
    /* usa issues já carregados */
  }
  const dnsData = await fetchCloudflareDns(domain, isEmailService);
  const checkRows = checks.map((c) => `
    <tr>
      <td>${esc(c.label || c.check_id)}</td>
      <td>${checkStatusBadge(c.status)}</td>
      <td>${esc(c.message || '—')}</td>
      <td>${fmtDate(c.checked_at)}</td>
    </tr>`).join('');
  const timelineBlock = d.timeline?.length
    ? `${phaseTimingCardHtml(d.timing, d.timeline)}<h4 style="margin:1rem 0 0.5rem">Eventos</h4>${timelineHtml(d.timeline, d.timing, { compact: true })}`
    : '<p class="loading">Sem eventos webhook para este domínio.</p>';
  const ips = (d.client_ips || []).filter(Boolean);
  body.innerHTML = `
    <div class="modal-breadcrumb">
      <button type="button" class="btn btn-ghost btn-sm" data-overview-back>← Voltar à lista</button>
      <span class="ticket-meta">${esc(data.name)}</span>
    </div>
    <div class="health-card-head">
      <div>
        <h3 style="margin:0">${esc(d.domain)}</h3>
        <p class="ticket-meta">${esc(d.email || 'sem e-mail')} · sessão <code>${esc((d.session_id || '—').slice(0, 18))}</code></p>
      </div>
      <div style="display:flex;gap:0.35rem;flex-wrap:wrap">
        ${executionStatusBadge(d.execution_status)}
        ${healthBadge(d.audit_status)}
      </div>
    </div>
    <dl class="detail-kv">
      <div><dt>Etapa funil</dt><dd>${esc(d.funnel_stage_label || d.funnel_stage || '—')}</dd></div>
      <div><dt>Início</dt><dd>${fmtDate(d.started_at)}</dd></div>
      <div><dt>Último evento</dt><dd>${esc(d.last_event || '—')} · ${fmtDate(d.last_event_at)}</dd></div>
      <div><dt>Último audit</dt><dd>${fmtDate(d.last_audit_at)}</dd></div>
      <div><dt>IP de acesso</dt><dd><code>${esc(d.client_ip || (ips[0] || '—'))}</code></dd></div>
      <div><dt>Ticket</dt><dd>${d.ticket_id ? `#${d.ticket_id} (${esc(d.ticket_status || '—')})` : '—'}</dd></div>
    </dl>
    ${ips.length > 1 ? `<p class="ticket-meta">IPs observados: ${ips.map((ip) => `<code>${esc(ip)}</code>`).join(' · ')}</p>` : ''}
    ${htmlCloudflareDnsCard(dnsData)}
    <div class="modal-section">
      <h4>Checks de auditoria</h4>
      <div class="table-wrap">
        <table>
          <thead><tr><th>Check</th><th>Status</th><th>Mensagem</th><th>Verificado</th></tr></thead>
          <tbody>${checkRows || '<tr><td colspan="4">Sem checks</td></tr>'}</tbody>
        </table>
      </div>
    </div>
    <div class="modal-section">
      <h4>Timeline de execução</h4>
      ${timelineBlock}
    </div>
    ${d.ticket_id ? `<div class="actions"><button type="button" class="btn btn-primary btn-sm" data-open-ticket="${d.ticket_id}">Abrir ticket #${d.ticket_id}</button></div>` : ''}`;
  body.querySelector('[data-overview-back]')?.addEventListener('click', () => renderOverviewModalList(data));
  body.querySelector('[data-open-ticket]')?.addEventListener('click', (btn) => {
    state.selectedTicketId = Number(btn.target.dataset.openTicket);
    closeOverviewModal();
    setView('tickets');
  });
}

async function openOverviewModal(tenantId, options = {}) {
  const modal = document.getElementById('overview-modal');
  const body = document.getElementById('overview-modal-body');
  if (!modal || !body) return;
  const focus = options.focus || 'onboard';
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
  body.innerHTML = '<p class="loading">Carregando detalhes…</p>';
  try {
    const data = await api(`/v1/audit/tenants/${tenantId}/details`);
    state.overviewModal = { tenantId, view: 'list', domain: null, data, focus };
    renderOverviewModalList(data);
  } catch (e) {
    console.error('openOverviewModal', e);
    body.innerHTML = `
      <p class="loading">Erro: ${esc(e.message)}</p>
      <button type="button" class="btn btn-ghost btn-sm" data-retry-overview-modal>Tentar novamente</button>`;
    body.querySelector('[data-retry-overview-modal]')?.addEventListener('click', () => {
      openOverviewModal(tenantId, { focus });
    });
  }
}

async function openUserAccessModal() {
  const modal = document.getElementById('overview-modal');
  const body = document.getElementById('overview-modal-body');
  const title = document.getElementById('overview-modal-title');
  const sub = document.getElementById('overview-modal-sub');
  if (!modal || !body) return;
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
  body.innerHTML = '<p class="loading">Carregando segurança de acesso…</p>';
  try {
    const sec = await api('/v1/security/summary?window_hours=24');
    const generatedAt = new Date().toISOString();
    const data = {
      tenant_id: 1,
      name: 'Acesso Usuário — Cybersecurity',
      generated_at: generatedAt,
      security: sec,
    };
    state.overviewModal = { tenantId: 1, view: 'list', domain: null, data, focus: 'access' };
    if (title) title.textContent = 'Acesso Usuário — Cybersecurity';
    if (sub) {
      sub.textContent = `Portal & sessões · ${sec.total || 0} alerta(s) 24h · ${sec.sessions_with_alerts || 0} sessão(ões) · gerado ${fmtDate(generatedAt)}`;
    }
    body.innerHTML = renderWizardSecurityCard(sec, { standalone: true });
    bindWizardSecurityCard(body);
  } catch (e) {
    console.error('openUserAccessModal', e);
    body.innerHTML = `
      <p class="loading">Erro ao carregar segurança de acesso: ${esc(e.message)}</p>
      <p class="ticket-meta">Verifique ligação ao Desk e permissões de audit.</p>
      <button type="button" class="btn btn-ghost btn-sm" data-retry-user-access>Tentar novamente</button>`;
    body.querySelector('[data-retry-user-access]')?.addEventListener('click', () => openUserAccessModal());
  }
}

async function renderOverview() {
  const el = document.getElementById('overview-content');
  el.innerHTML = '<p class="loading">Carregando overview…</p>';
  try {
    const secPromise = window.DeskModules?.isEnabled('wizard-security')
      ? api('/v1/security/summary?window_hours=24').catch(() => null)
      : Promise.resolve(null);
    const [data, secSummary] = await Promise.all([
      api('/v1/audit/overview'),
      secPromise,
    ]);
    const cards = [];
    if (secSummary?.enabled !== false && window.DeskModules?.isEnabled('wizard-security')) {
      const accessCard = renderUserAccessOverviewCard(secSummary);
      if (accessCard) cards.push(accessCard);
    }
    (data.tenants || []).forEach((t) => {
      if (t.kind === 'wazuh_soc' && window.DeskModules?.isEnabled('wazuh-soc')) {
        cards.push(renderWazuhOverviewCard(t));
        return;
      }
      const issues = (t.top_issues || [])
        .slice(0, 3)
        .map((i) => `<li><code>${esc(i.domain)}</code> · ${esc(i.check_id)} — ${esc(i.message || i.status)}</li>`)
        .join('');
      cards.push(`
        <button type="button" class="card health-card health-${esc(t.status)} health-card--click" data-open-overview="${t.tenant_id}">
          <div class="health-card-head">
            <div>
              <h3 style="margin:0">${esc(t.name)}</h3>
              <p class="ticket-meta">${esc(t.ip)} · <strong>${t.domains_count || 0}</strong> domínio(s) · wizard &amp; infra</p>
            </div>
            ${healthBadge(t.status)}
          </div>
          <div class="health-score">${t.score?.pass ?? 0}/${t.score?.total ?? 8} pass · ${t.score?.warn ?? 0} warn · ${t.score?.fail ?? 0} fail</div>
          <p class="ticket-meta">Último audit: ${fmtDate(t.last_audit_at)}</p>
          ${issues ? `<ul class="issue-list">${issues}</ul>` : '<p class="loading">Sem issues ou aguarde o 1º ciclo de auditoria</p>'}
          <p class="health-card-hint">Clique para ver domínios, DNS, checks e timeline do onboarding</p>
        </button>`);
    });
    el.innerHTML = cards.length
      ? `<div class="health-grid">${cards.join('')}</div>`
      : '<p class="loading">Nenhum tenant auditado. Complete onboarding ou POST /audit/cycle.</p>';
    el.querySelectorAll('[data-open-overview]').forEach((btn) => {
      btn.addEventListener('click', () => {
        openOverviewModal(Number(btn.dataset.openOverview), { focus: 'onboard' });
      });
    });
    el.querySelectorAll('[data-open-user-access]').forEach((btn) => {
      btn.addEventListener('click', () => openUserAccessModal());
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

function overviewHomeWindowHours() {
  return { '24h': 24, '7d': 168, '30d': 720 }[state.overviewHomeWindow] || 24;
}

function isInWindow(iso, hours) {
  if (!iso) return false;
  const t = new Date(iso).getTime();
  if (Number.isNaN(t)) return false;
  return Date.now() - t <= hours * 3600000;
}

function relativeTimeAgo(iso) {
  if (!iso) return '—';
  const diff = Date.now() - new Date(iso).getTime();
  if (diff < 0) return 'agora';
  const mins = Math.floor(diff / 60000);
  if (mins < 1) return 'agora';
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 48) return `${hrs}h ago`;
  const days = Math.floor(hrs / 24);
  return `${days}d ago`;
}

function sparklineSvg(values, color = '#2f6fed') {
  const w = 118;
  const h = 34;
  const pad = 3;
  const data = values?.length ? values : [0, 0, 0, 0, 0, 0];
  const max = Math.max(...data, 1);
  const pts = data.map((v, i) => {
    const x = pad + (i / Math.max(data.length - 1, 1)) * (w - pad * 2);
    const y = h - pad - (v / max) * (h - pad * 2);
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(' ');
  return `<svg class="cf-spark" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true"><polyline fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="${pts}"/></svg>`;
}

function bucketEvents(events, windowHours, buckets = 12) {
  const out = Array(buckets).fill(0);
  const now = Date.now();
  const start = now - windowHours * 3600000;
  for (const ev of events) {
    const t = new Date(ev.at || ev.created_at).getTime();
    if (Number.isNaN(t) || t < start) continue;
    const idx = Math.min(buckets - 1, Math.floor(((t - start) / (windowHours * 3600000)) * buckets));
    out[idx] += 1;
  }
  return out;
}

function domainStatusDot(status) {
  if (status === 'healthy') return 'ok';
  if (status === 'degraded') return 'warn';
  if (status === 'critical') return 'bad';
  return 'unknown';
}

function buildOverviewHomeTrail(events, domainsFlat, filter, windowHours) {
  const rows = [];
  for (const ev of events) {
    if (!isInWindow(ev.created_at, windowHours)) continue;
    const p = ev.payload || {};
    const source = ev.source || p.source || 'unknown';
    if (filter === 'onboard' && source !== 'vm112-onboard') continue;
    if (filter === 'wazuh' && source !== 'wazuh') continue;
    if (filter === 'checks') continue;
    const trailDomain = ev.domain || p.domain || '';
    const trailDomainMeta = domainsFlat.find((item) => item.domain === trailDomain);
    rows.push({
      action: ev.event_type || 'event',
      target: trailDomain || p.data?.agent || '—',
      at: ev.created_at,
      source,
      tenant_id: trailDomainMeta?.tenant_id || (source === 'wazuh' ? 2 : 1),
      funnel_stage: trailDomainMeta?.funnel_stage || '',
      kind: 'webhook',
    });
  }
  for (const d of domainsFlat) {
    for (const issue of d.issues || []) {
      if (!isInWindow(issue.checked_at, windowHours)) continue;
      if (filter === 'onboard' || filter === 'wazuh') continue;
      rows.push({
        action: `check.${issue.status}`,
        target: d.domain,
        detail: `${issue.check_id} — ${issue.message || issue.status}`,
        at: issue.checked_at,
        source: 'audit',
        tenant_id: d.tenant_id,
        funnel_stage: d.funnel_stage || '',
        kind: 'check',
        domain: d.domain,
      });
    }
  }
  rows.sort((a, b) => new Date(b.at) - new Date(a.at));
  return rows.slice(0, 40);
}

async function renderOverviewHome(options = {}) {
  const el = document.getElementById('overview-home-content');
  if (!el) return;
  if (window.DeskServices?.renderPage) {
    await window.DeskServices.renderPage(el, options);
    return;
  }
  if (window.DeskAccounts?.renderPage) {
    await window.DeskAccounts.renderPage(el, options);
    return;
  }
  el.innerHTML = '<p class="loading">Módulo Serviços não carregado.</p>';
}

async function renderLeads() {
  const el = document.getElementById('leads-content');
  if (!canReadLeads()) {
    el.innerHTML = '<p class="loading">Sem permissão para ver leads</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando leads…</p>';
  try {
    const data = await api('/v1/crm/leads');
    const leads = data.leads || [];
    el.innerHTML = `
      <div class="card">
        <div class="card-head-row">
          <h3>Leads abandonados</h3>
          <span class="ticket-meta">Stale ≥ ${data.stale_hours ?? 24}h sem concluir onboarding</span>
        </div>
        <p class="ticket-meta" style="margin-bottom:1rem">
          Tickets promovidos automaticamente pelo worker quando o cliente para no funil.
          Use o e-mail do ticket para recuperação (Spec 012 Fase C — chat).
        </p>
        ${leads.length
          ? `<div class="lead-grid">${leads.map(leadRowHtml).join('')}</div>`
          : '<p class="loading">Nenhum lead no momento</p>'}
      </div>`;
    el.querySelectorAll('[data-lead-ticket]').forEach((btn) => {
      btn.addEventListener('click', () => {
        state.selectedTicketId = Number(btn.dataset.leadTicket);
        state.selectedSessionId = btn.dataset.leadSession || null;
        setView('tickets');
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function renderTickets(options = {}) {
  const { poll = false } = options;
  stopLiveTimingClock();
  const listEl = document.getElementById('ticket-list');
  const detailEl = document.getElementById('ticket-detail');
  if (poll && window.TicketsWorkspace?._pageReady) {
    await TicketsWorkspace.softRefresh();
    return;
  }
  listEl.innerHTML = '<p class="loading">Carregando tickets…</p>';
  try {
    let tickets = [];
    if (state.ticketFilter === 'leads') {
      const data = await api('/v1/crm/leads');
      tickets = (data.leads || []).map((l) => ({
        id: l.ticket_id,
        subject: l.subject,
        domain: l.domain,
        email: l.email,
        status: l.status,
        created_at: l.created_at,
        source: 'vm112-onboard',
        crm_track: 'lead',
        assigned_to: l.assigned_to,
        session_id: l.session_id,
        lead_funnel_stage: l.funnel_stage,
      }));
    } else {
      let q = '';
      const params = [];
      if (state.ticketFilter !== 'all' && state.ticketFilter !== 'active') {
        params.push(`status=${state.ticketFilter}`);
      }
      if (state.sourceFilter !== 'all') params.push(`source=${state.sourceFilter}`);
      if (params.length) q = '?' + params.join('&');
      const data = await api(`/v1/desk/tickets${q}`);
      tickets = data.tickets || [];
      if (state.ticketFilter === 'active') {
        tickets = tickets.filter((t) => ['open', 'escalated', 'assisting', 'resolved'].includes(t.status));
      }
    }
    if (window.TicketsWorkspace) {
      await TicketsWorkspace.renderPage({ listEl, detailEl, tickets });
    } else {
      state.tickets = tickets;
      listEl.innerHTML = state.tickets.length
        ? state.tickets.map(ticketRowHtml).join('')
        : '<p class="loading">Nenhum ticket neste filtro</p>';
      listEl.querySelectorAll('.ticket-row').forEach((btn) => {
        btn.addEventListener('click', () => {
          state.selectedTicketId = Number(btn.dataset.id);
          state.selectedSessionId = null;
          renderTicketDetail();
          listEl.querySelectorAll('.ticket-row').forEach((r) => r.classList.remove('selected'));
          btn.classList.add('selected');
        });
      });
      if (state.selectedTicketId) await renderTicketDetail();
      else if (state.selectedSessionId) await renderSessionDetail();
      else detailEl.innerHTML = '<div class="card detail-panel"><p class="empty">Selecione um ticket ou sessão do funil</p></div>';
    }
  } catch (e) {
    listEl.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function renderSessionDetail() {
  const detailEl = document.getElementById('ticket-detail');
  const sessionId = state.selectedSessionId;
  if (!sessionId) return;
  detailEl.innerHTML = '<div class="card detail-panel"><p class="loading">Carregando sessão…</p></div>';
  try {
    const meta = await loadAssistMeta(sessionId);
    detailEl.innerHTML = `
      <div class="card detail-panel">
        <h3 style="margin:0">Sessão onboarding</h3>
        <dl class="kv">
          <dt>Domínio</dt><dd>${esc(meta.domain || '—')}</dd>
          <dt>Etapa</dt><dd>${esc(FUNNEL_LABELS[meta.funnel_stage] || meta.funnel_stage || '—')}</dd>
          <dt>Sessão</dt><dd><code>${esc(meta.session_id)}</code></dd>
          ${meta.ticket_id ? `<dt>Ticket</dt><dd>#${meta.ticket_id}</dd>` : ''}
        </dl>
        ${assistActionsHtml(sessionId, {
          can_escalate: meta.can_escalate,
          assist_status: meta.ticket_status || meta.assist_status,
          assisted_by: meta.assisted_by,
          actions: meta.actions,
        }, meta._console || {})}
        ${meta.timeline?.length ? `${phaseTimingCardHtml(meta.timing, meta.timeline)}<h3 style="margin-top:1.25rem">Eventos</h3>${timelineHtml(meta.timeline, meta.timing, { compact: true })}` : ''}
      </div>`;
    bindAssistActions(detailEl, sessionId);
    bindLiveTimingClock(detailEl);
  } catch (e) {
    detailEl.innerHTML = `<div class="card"><p class="loading">Erro: ${esc(e.message)}</p></div>`;
  }
}

async function renderTicketDetail() {
  const detailEl = document.getElementById('ticket-detail');
  if (!state.selectedTicketId) return;
  if (window.TicketsDetailPanel) {
    await TicketsDetailPanel.render(state.selectedTicketId, detailEl);
    return;
  }
  detailEl.innerHTML = '<div class="card detail-panel"><p class="loading">Carregando…</p></div>';
  try {
    const t = await api(`/v1/desk/tickets/${state.selectedTicketId}`);
    const sessionId = t.session_id || state.selectedSessionId;
    const assistMeta = sessionId && t.source === 'vm112-onboard'
      ? await loadAssistMeta(sessionId)
      : null;
    if (sessionId) state.selectedSessionId = sessionId;
    let carbonioBlock = null;
    if (t.source === 'vm112-onboard' && window.DeskModules?.isEnabled('carbonio-release')) {
      try {
        const byTicket = await api(`/v1/carbonio-blocks?ticket_id=${t.id}&status=pending&limit=1`);
        carbonioBlock = byTicket.blocks?.[0] || null;
        if (!carbonioBlock && sessionId) {
          const bySession = await api(`/v1/carbonio-blocks?session_id=${encodeURIComponent(sessionId)}&status=pending&limit=1`);
          carbonioBlock = bySession.blocks?.[0] || null;
        }
      } catch {
        carbonioBlock = null;
      }
    }
    const timeline = assistMeta?.timeline?.length
      ? assistMeta.timeline
      : (t.timeline || t.related_events || []);
    const timing = assistMeta?.timing || t.timing;
    const closeStatuses = ['open', 'escalated', 'assisting', 'resolved'];
    detailEl.innerHTML = `
      <div class="card detail-panel">
        <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem;flex-wrap:wrap">
          <h3 style="margin:0">Ticket #${t.id}</h3>
          <span class="badge ${t.status}">${esc(statusLabel(t.status))}</span>
        </div>
        <dl class="kv">
          <dt>Origem</dt><dd>${sourceBadge(t.source)}</dd>
          <dt>Domínio/Agente</dt><dd>${esc(t.domain || t.agent || '—')}</dd>
          <dt>Email</dt><dd>${esc(t.email || '—')}</dd>
          <dt>Evento</dt><dd>${esc(t.event || '—')}</dd>
          ${t.assigned_to ? `<dt>Atribuído</dt><dd>${esc(t.assigned_to)}</dd>` : ''}
          ${t.assisted_by ? `<dt>Assistido por</dt><dd>${esc(t.assisted_by)}</dd>` : ''}
          ${t.client_paused ? '<dt>Cliente</dt><dd><span class="badge assisting">pausado</span></dd>' : ''}
          ${t.ready_for_ops ? '<dt>Ops</dt><dd><span class="badge ok">ready for ops</span></dd>' : ''}
          ${t.severity != null ? `<dt>Severidade</dt><dd>${severityBadge(t.severity)}</dd>` : ''}
          ${t.rule_id ? `<dt>Regra</dt><dd>${esc(t.rule_id)}</dd>` : ''}
          ${t.description ? `<dt>Descrição</dt><dd>${esc(t.description)}</dd>` : ''}
          ${t.desk_message ? `<dt>Nota</dt><dd>${esc(t.desk_message)}</dd>` : ''}
          ${t.registration_role ? `<dt>Perfil</dt><dd>${esc(roleLabel(t.registration_role))}</dd>` : ''}
          ${t.ativation_url ? `<dt>Ativar conta</dt><dd><a class="btn btn-primary btn-sm" href="${esc(t.ativation_url)}" target="_blank" rel="noopener">Abrir link de ativação</a></dd>` : ''}
          <dt>Sessão/Alert ID</dt><dd><code>${esc(t.session_id || '—')}</code></dd>
          <dt>Verificado</dt><dd>${t.account_verified ? 'Sim' : 'Não'}</dd>
          <dt>Revisão</dt><dd>${t.needs_review ? 'Necessária' : 'Não'}</dd>
          <dt>Criado</dt><dd>${fmtDate(t.created_at)}</dd>
        </dl>
        ${sessionId && t.source === 'vm112-onboard' ? assistActionsHtml(sessionId, {
          can_escalate: assistMeta?.can_escalate,
          assist_status: assistMeta?.ticket_status || assistMeta?.assist_status,
          assisted_by: assistMeta?.assisted_by,
          actions: assistMeta?.actions,
        }, assistMeta?._console || {}) : ''}
        ${carbonioBlock ? carbonioBlockPanelHtml(carbonioBlock) : ''}
        <div class="actions">
          ${canPatchTickets() ? (closeStatuses.includes(t.status)
            ? `<button type="button" class="btn btn-primary" data-action="close">Fechar ticket</button>`
            : `<button type="button" class="btn btn-ghost" data-action="open">Reabrir ticket</button>`) : ''}
        </div>
        ${timeline.length ? `${phaseTimingCardHtml(timing, timeline)}<h3 style="margin-top:1.25rem">Eventos</h3>${timelineHtml(timeline, timing, { compact: true })}` : ''}
        <h3 style="margin-top:1.25rem">Payload</h3>
        <pre class="raw">${esc(JSON.stringify(t.payload, null, 2))}</pre>
      </div>`;
    if (sessionId && t.source === 'vm112-onboard') {
      bindAssistActions(detailEl, sessionId);
    }
    bindCarbonioResolveForms(detailEl);
    bindLiveTimingClock(detailEl);
    detailEl.querySelector('[data-action="close"]')?.addEventListener('click', () => updateTicketStatus('closed'));
    detailEl.querySelector('[data-action="open"]')?.addEventListener('click', () => updateTicketStatus('open'));
  } catch (e) {
    detailEl.innerHTML = `<div class="card"><p class="loading">Erro: ${esc(e.message)}</p></div>`;
  }
}

async function updateTicketStatus(status) {
  await api(`/v1/desk/tickets/${state.selectedTicketId}`, {
    method: 'PATCH',
    body: JSON.stringify({ status }),
  });
  await renderTickets();
}

async function renderEvents() {
  syncEventsToolbar();
  if (state.eventsTab === 'purges') {
    await renderPurgeHistory();
    return;
  }
  if (state.eventsTab === 'security') {
    await renderSecurityEvents();
    return;
  }
  if (state.eventsTab === 'carbonio') {
    await renderCarbonioBlocks();
    return;
  }
  const el = document.getElementById('events-content');
  el.innerHTML = '<p class="loading">Carregando eventos…</p>';
  try {
    const srcQ = state.eventSourceFilter !== 'all' ? `?source=${state.eventSourceFilter}` : '';
    const data = await api(`/v1/webhooks/events${srcQ}`);
    const rows = (data.events || []).map((e) => {
      const p = e.payload || {};
      const dataObj = p.data || {};
      return `<tr>
        <td>${e.id}</td>
        <td>${sourceBadge(e.source)}</td>
        <td><span class="badge open">${esc(e.event_type)}</span> ${severityBadge(dataObj.level || e.severity)}</td>
        <td>${esc(p.domain || '—')}</td>
        <td><code>${esc((p.session_id || '').slice(0, 16))}</code></td>
        <td>${fmtDate(e.created_at)}</td>
      </tr>`;
    }).join('');
    el.innerHTML = `
      <div class="card table-wrap">
        <table>
          <thead><tr><th>ID</th><th>Origem</th><th>Evento</th><th>Agente/Domínio</th><th>Ref</th><th>Data</th></tr></thead>
          <tbody>${rows || '<tr><td colspan="6">Sem eventos</td></tr>'}</tbody>
        </table>
      </div>`;
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

function syncEventsToolbar() {
  const isPurges = state.eventsTab === 'purges';
  const isSecurity = state.eventsTab === 'security';
  const isCarbonio = state.eventsTab === 'carbonio';
  document.querySelectorAll('[data-events-tab]').forEach((btn) => {
    btn.classList.toggle('active', btn.dataset.eventsTab === state.eventsTab);
  });
  document.querySelectorAll('.events-webhooks-only').forEach((el) => {
    el.hidden = isPurges || isSecurity || isCarbonio;
  });
  document.querySelectorAll('.events-security-only').forEach((el) => {
    el.hidden = !isSecurity;
  });
  const title = document.getElementById('page-title');
  const sub = document.getElementById('page-subtitle');
  if (state.view === 'events' && title) {
    const titles = {
      purges: 'Histórico de purges',
      security: 'Eventos de segurança wizard',
      carbonio: 'Bloqueios Carbonio',
    };
    title.textContent = titles[state.eventsTab] || 'Eventos webhook';
    if (sub) {
      const subs = {
        purges: 'Purges VM112 persistidos no Desk — timeline, usuário e serviços removidos',
        security: 'CSP, inputs bloqueados e handoff — telemetria Spec 021',
        carbonio: 'ACCOUNT_EXISTS — remover conta órfã no Carbonio para o cliente repetir o passo',
      };
      sub.textContent = subs[state.eventsTab] || 'Operações Ligbox — onboarding, tickets e monitoramento';
    }
  }
}

function carbonioBlockStatusBadge(status) {
  const map = {
    pending: ['open', 'Pendente'],
    resolved: ['done', 'Resolvido'],
  };
  const [cls, label] = map[status] || ['open', status || '—'];
  return `<span class="badge ${cls}">${esc(label)}</span>`;
}

function carbonioReleaseGuideHtml() {
  return `
    <details class="wizard-sec-playbook" style="margin:0.75rem 0">
      <summary>Guia — libertar e-mail ACCOUNT_EXISTS</summary>
      <ol class="wizard-sec-steps">
        <li>O onboarding falhou porque o e-mail já existe no Carbonio (conta órfã de processo abandonado).</li>
        <li>Confirme o e-mail exacto e a sua <strong>senha Desk</strong> (não a do Carbonio nem root).</li>
        <li>A ação remove apenas a conta Carbonio (<code>zmprov da</code>) — domínio, DNS e portal mantêm-se.</li>
        <li>Peça ao cliente para repetir «Criar conta» no wizard com o mesmo e-mail.</li>
        <li>Dois técnicos a resolver em paralelo: só o primeiro consegue; o outro vê «já resolvido».</li>
      </ol>
    </details>`;
}

function carbonioResolveFormHtml(block) {
  if (block.status === 'resolved') {
    return `<p class="ticket-meta">Resolvido por <strong>${esc(block.resolved_by)}</strong> em ${fmtDate(block.resolved_at)}${block.resolution_note ? ` — ${esc(block.resolution_note)}` : ''}</p>`;
  }
  if (!canReadTickets()) return '';
  return `
    <form class="carbonio-resolve-form" data-carbonio-block="${block.id}" style="margin-top:0.75rem">
      <p class="ticket-meta">Confirme o e-mail e a sua senha Desk para executar <code>zmprov da</code> na VM112.</p>
      <div style="display:grid;gap:0.5rem;max-width:24rem">
        <label>E-mail a libertar
          <input type="email" name="confirm_email" required placeholder="${esc(block.email)}" autocomplete="off" class="input">
        </label>
        <label>Sua senha Desk
          <input type="password" name="password" required autocomplete="current-password" class="input">
        </label>
        <button type="submit" class="btn btn-primary btn-sm">Remover conta Carbonio</button>
      </div>
      <p class="carbonio-resolve-msg ticket-meta" hidden style="margin-top:0.5rem"></p>
    </form>`;
}

function carbonioBlockPanelHtml(block) {
  return `
    <div class="card carbonio-release-card" style="margin-top:1rem;border-left:4px solid #e67e22">
      <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem;flex-wrap:wrap">
        <h3 style="margin:0">Bloqueio Carbonio — ACCOUNT_EXISTS</h3>
        ${carbonioBlockStatusBadge(block.status)}
      </div>
      <p class="ticket-meta" style="margin:0.5rem 0">
        E-mail <code>${esc(block.email)}</code> · domínio ${esc(block.domain)}
        ${block.ticket_id ? ` · bloqueio #${block.id}` : ''}
      </p>
      ${block.error_message ? `<p class="ticket-meta" style="opacity:0.85">${esc(block.error_message.slice(0, 240))}</p>` : ''}
      ${carbonioReleaseGuideHtml()}
      ${carbonioResolveFormHtml(block)}
    </div>`;
}

async function resolveCarbonioBlock(blockId, confirmEmail, password) {
  return api(`/v1/carbonio-blocks/${blockId}/resolve`, {
    method: 'POST',
    body: JSON.stringify({ confirm_email: confirmEmail, password }),
  });
}

function bindCarbonioResolveForms(root) {
  root.querySelectorAll('.carbonio-resolve-form').forEach((form) => {
    if (form.dataset.bound) return;
    form.dataset.bound = '1';
    form.addEventListener('submit', async (ev) => {
      ev.preventDefault();
      const blockId = form.dataset.carbonioBlock;
      const fd = new FormData(form);
      const msgEl = form.querySelector('.carbonio-resolve-msg');
      const btn = form.querySelector('button[type="submit"]');
      btn.disabled = true;
      msgEl.hidden = true;
      try {
        const res = await resolveCarbonioBlock(blockId, fd.get('confirm_email'), fd.get('password'));
        msgEl.textContent = res.message || 'Conta removida do Carbonio.';
        msgEl.style.color = 'var(--ok, #2ecc71)';
        msgEl.hidden = false;
        setTimeout(async () => {
          if (state.view === 'events') await renderEvents();
          else if (state.selectedTicketId) await renderTicketDetail();
        }, 1200);
      } catch (e) {
        msgEl.textContent = e.message;
        msgEl.style.color = 'var(--danger, #e74c3c)';
        msgEl.hidden = false;
        btn.disabled = false;
      }
    });
  });
}

async function renderCarbonioBlocks() {
  syncEventsToolbar();
  const el = document.getElementById('events-content');
  if (!window.DeskModules?.isEnabled('carbonio-release')) {
    el.innerHTML = '<p class="loading">Módulo Bloqueios Carbonio desativado.</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando bloqueios Carbonio…</p>';
  try {
    const [pending, resolved] = await Promise.all([
      api('/v1/carbonio-blocks?status=pending&limit=100'),
      api('/v1/carbonio-blocks?status=resolved&limit=30'),
    ]);
    const pendingBlocks = pending.blocks || [];
    const resolvedBlocks = resolved.blocks || [];
    const pendingCards = pendingBlocks.length
      ? pendingBlocks.map((b) => carbonioBlockPanelHtml(b)).join('')
      : '<p class="ticket-meta">Nenhum bloqueio pendente — novos casos aparecem aqui via webhook <code>onboarding.failed</code> + ACCOUNT_EXISTS.</p>';
    const resolvedRows = resolvedBlocks.map((b) => `
      <tr>
        <td>#${b.id}</td>
        <td><code>${esc(b.email)}</code></td>
        <td>${esc(b.domain)}</td>
        <td>${esc(b.resolved_by || '—')}</td>
        <td>${fmtDate(b.resolved_at)}</td>
        <td>${b.ticket_id ? `#${b.ticket_id}` : '—'}</td>
      </tr>`).join('');
    el.innerHTML = `
      <div class="card" style="margin-bottom:1rem">
        <p class="ticket-meta" style="margin:0">
          <strong>${pending.total || pendingBlocks.length}</strong> pendente(s) ·
          <strong>${resolved.total || resolvedBlocks.length}</strong> resolvido(s) recentes
        </p>
      </div>
      ${pendingCards}
      <details class="card" style="margin-top:1rem">
        <summary style="cursor:pointer;font-weight:600">Histórico resolvido (${resolvedBlocks.length})</summary>
        <div class="table-wrap" style="margin-top:0.75rem">
          <table>
            <thead><tr><th>ID</th><th>E-mail</th><th>Domínio</th><th>Resolvido por</th><th>Quando</th><th>Ticket</th></tr></thead>
            <tbody>${resolvedRows || '<tr><td colspan="6">Nenhum</td></tr>'}</tbody>
          </table>
        </div>
      </details>`;
    bindCarbonioResolveForms(el);
    el.querySelectorAll('[data-goto-ticket]').forEach((link) => {
      link.addEventListener('click', (ev) => {
        ev.preventDefault();
        state.selectedTicketId = Number(link.dataset.gotoTicket);
        setView('tickets');
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function renderSecurityEvents() {
  syncEventsToolbar();
  const el = document.getElementById('events-content');
  if (!window.DeskModules?.isEnabled('wizard-security')) {
    el.innerHTML = '<p class="loading">Módulo Segurança Wizard desativado.</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando eventos de segurança…</p>';
  try {
    const [data, summary] = await Promise.all([
      api('/v1/security/events?limit=200&window_hours=168'),
      api('/v1/security/summary?window_hours=24').catch(() => ({})),
    ]);
    const rows = (data.events || []).map((ev) => `
      <tr class="wizard-sec-row" data-wizard-sec-session="${esc(ev.session_id || '')}" style="cursor:pointer">
        <td>${wizardSecuritySeverityBadge(ev.severity)}</td>
        <td>${esc(wizardSecurityEventLabel(ev.event_type))}</td>
        <td>${ev.session_id ? sessionHashHtml(ev.session_id) : '—'}</td>
        <td>${esc(ev.domain || '—')}</td>
        <td><code>${esc(ev.client_ip || '—')}</code></td>
        <td>${esc(ev.endpoint || ev.reason || '—')}</td>
        <td>${fmtDate(ev.created_at)}</td>
      </tr>`).join('');
    el.innerHTML = `
      <div class="card">
        <p class="ticket-meta" style="margin:0 0 0.75rem">
          Últimas 24h: <strong>${summary.csp_violations || 0}</strong> CSP ·
          <strong>${summary.inputs_blocked || 0}</strong> bloqueados ·
          <strong>${summary.handoffs_rejected || 0}</strong> handoffs rejeitados
        </p>
        <details class="wizard-sec-playbook"><summary>Guia rápido para técnicos</summary>
          <ol class="wizard-sec-steps">
            <li>Input bloqueado → anote hash + IP; se repetido, escale.</li>
            <li>Handoff rejeitado → cliente deve refazer login; ticket escalado automático.</li>
            <li>Clique na linha para abrir a sessão em Tickets.</li>
          </ol>
        </details>
        <div class="table-wrap" style="margin-top:0.75rem">
          <table class="wizard-sec-table">
            <thead><tr><th>Nível</th><th>Evento</th><th>Sessão</th><th>Domínio</th><th>IP</th><th>Detalhe</th><th>Quando</th></tr></thead>
            <tbody>${rows || '<tr><td colspan="7">Nenhum evento de segurança</td></tr>'}</tbody>
          </table>
        </div>
      </div>`;
    el.querySelectorAll('[data-wizard-sec-session]').forEach((row) => {
      const sid = row.dataset.wizardSecSession;
      if (!sid) return;
      row.addEventListener('click', () => {
        state.selectedSessionId = sid;
        setView('tickets');
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

function purgeStatusBadge(status) {
  const map = {
    done: ['done', 'Concluído'],
    error: ['closed', 'Erro'],
    running: ['open', 'Em execução'],
    queued: ['pending', 'Na fila'],
  };
  const [cls, label] = map[status] || ['open', status || '—'];
  return `<span class="badge ${cls}">${esc(label)}</span>`;
}

function deskRemovedSummary(desk) {
  if (!desk || typeof desk !== 'object') return '—';
  const labels = {
    webhook_events: 'webhooks',
    tickets: 'tickets',
    audit_domains: 'audit',
    assist_sessions: 'assist',
    audit_checks: 'checks',
  };
  const parts = Object.entries(desk)
    .filter(([, n]) => Number(n) > 0)
    .map(([k, n]) => `${labels[k] || k}: ${n}`);
  return parts.length ? parts.join(', ') : 'nenhum no Desk';
}

function vm112RemovedSummary(vm112) {
  if (!vm112 || !vm112.ok) return vm112?.error ? esc(vm112.error) : '—';
  const r = vm112.result || {};
  const parts = [];
  if (Array.isArray(r.carbonio_accounts) && r.carbonio_accounts.length) {
    parts.push(`Carbonio (${r.carbonio_accounts.length} contas)`);
  } else if (r.carbonio_domain) {
    parts.push('Carbonio');
  }
  if (Array.isArray(r.portal_users_removed) && r.portal_users_removed.length) {
    parts.push(`portal (${r.portal_users_removed.length})`);
  }
  if (r.site_folder_removed) parts.push('site');
  if (r.cloudflare) parts.push('Cloudflare');
  if (r.traefik_sni || r.traefik_routers) parts.push('Traefik');
  return parts.length ? esc(parts.join(', ')) : 'VM112 OK';
}

function renderPurgeTimelineHtml(steps) {
  return `<ul class="vm112-purge-timeline purge-history-timeline">${(steps || []).map((step) => {
    const status = step.status || 'pending';
    return `
      <li class="vm112-purge-step vm112-purge-step--${esc(status)}">
        <span class="vm112-purge-step-time">${esc(fmtDate(step.at))}</span>
        <div class="vm112-purge-step-body">
          <strong>${esc(step.label)}</strong>
          ${step.detail ? `<span>${esc(step.detail)}</span>` : ''}
        </div>
      </li>`;
  }).join('')}</ul>`;
}

function closePurgeHistoryModal() {
  const modal = document.getElementById('purge-history-modal');
  if (!modal) return;
  modal.classList.add('hidden');
  modal.setAttribute('aria-hidden', 'true');
}

function openPurgeHistoryModal(jobId) {
  const modal = document.getElementById('purge-history-modal');
  const title = document.getElementById('purge-history-modal-title');
  const sub = document.getElementById('purge-history-modal-sub');
  const body = document.getElementById('purge-history-modal-body');
  if (!modal || !body) return;
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
  title.textContent = 'Detalhe do purge';
  sub.textContent = `Job ${jobId}`;
  body.innerHTML = '<p class="loading">Carregando…</p>';
  api(`/v1/vm112/purge/jobs/${encodeURIComponent(jobId)}`)
    .then((job) => {
      title.textContent = job.domain || 'Purge';
      sub.innerHTML = `${purgeStatusBadge(job.status)} · ${esc(job.by || '—')} · ${fmtDate(job.created_at)} · job <code>${esc(job.id)}</code>`;
      const desk = job.desk || {};
      const vm112 = job.vm112 || {};
      const deskRows = Object.entries({
        webhook_events: 'Eventos webhook',
        tickets: 'Tickets',
        audit_domains: 'Domínios audit',
        assist_sessions: 'Sessões assist',
        audit_checks: 'Checks audit',
      }).map(([key, label]) => `
        <tr><td>${esc(label)}</td><td>${Number(desk[key] || 0)}</td></tr>`).join('');
      const vm112Steps = Array.isArray(vm112.steps) ? vm112.steps : [];
      const timeline = (job.timeline || []).length ? job.timeline : vm112Steps;
      body.innerHTML = `
        <div class="purge-history-grid">
          <div class="card">
            <h4>Removido no Desk (VM122)</h4>
            <table class="purge-history-kv">
              <tbody>${deskRows}</tbody>
              <tfoot><tr><td><strong>Total</strong></td><td><strong>${Object.values(desk).reduce((a, b) => a + Number(b || 0), 0)}</strong></td></tr></tfoot>
            </table>
          </div>
          <div class="card">
            <h4>Removido na VM112</h4>
            <p class="purge-history-vm112-sum">${vm112RemovedSummary(vm112)}</p>
            ${job.elapsed_vm112 ? `<p class="ticket-meta">Duração VM112: ${job.elapsed_vm112}s</p>` : ''}
            ${job.error ? `<p class="purge-history-error">${esc(job.error)}</p>` : ''}
          </div>
        </div>
        <div class="card" style="margin-top:1rem">
          <h4>Timeline completa</h4>
          ${timeline.length ? renderPurgeTimelineHtml(timeline) : '<p class="loading">Sem passos registados</p>'}
        </div>`;
    })
    .catch((e) => {
      body.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
    });
  document.querySelectorAll('[data-close-purge-history-modal]').forEach((el) => {
    el.onclick = closePurgeHistoryModal;
  });
}

async function renderPurgeHistory() {
  syncEventsToolbar();
  const el = document.getElementById('events-content');
  el.innerHTML = '<p class="loading">Carregando histórico de purges…</p>';
  try {
    const data = await api('/v1/vm112/purge/jobs?limit=200');
    const rows = (data.jobs || []).map((j) => `
      <tr class="purge-history-row" data-purge-job="${esc(j.id)}" tabindex="0" role="button">
        <td><code class="purge-history-link">${esc(j.id)}</code></td>
        <td><strong>${esc(j.domain)}</strong></td>
        <td>${purgeStatusBadge(j.status)}</td>
        <td>${esc(j.by || '—')}</td>
        <td class="purge-history-removed">${esc(deskRemovedSummary(j.desk))}</td>
        <td>${fmtDate(j.created_at)}</td>
        <td>${j.elapsed_vm112 ? `${j.elapsed_vm112}s` : '—'}</td>
      </tr>`).join('');
    el.innerHTML = `
      <div class="card table-wrap">
        <p class="ticket-meta" style="padding:0.75rem 1rem 0">Clique numa linha para ver a timeline completa e o que foi removido em cada serviço.</p>
        <table class="purge-history-table">
          <thead>
            <tr>
              <th>Job</th><th>Domínio</th><th>Status</th><th>Usuário</th>
              <th>Desk removido</th><th>Quando</th><th>VM112</th>
            </tr>
          </thead>
          <tbody>${rows || '<tr><td colspan="7">Nenhum purge registado</td></tr>'}</tbody>
        </table>
        ${data.total > (data.jobs || []).length ? `<p class="ticket-meta" style="padding:0.5rem 1rem">A mostrar ${(data.jobs || []).length} de ${data.total} purges.</p>` : ''}
      </div>`;
    el.querySelectorAll('[data-purge-job]').forEach((row) => {
      const open = () => openPurgeHistoryModal(row.dataset.purgeJob);
      row.addEventListener('click', open);
      row.addEventListener('keydown', (ev) => {
        if (ev.key === 'Enter' || ev.key === ' ') {
          ev.preventDefault();
          open();
        }
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function renderTenants() {
  const el = document.getElementById('tenants-content');
  el.innerHTML = '<p class="loading">Carregando…</p>';
  try {
    const data = await api('/v1/tenants');
    el.innerHTML = `
      <div class="card table-wrap">
        <table>
          <thead><tr><th>ID</th><th>Nome</th><th>IP</th><th>Papel</th><th>Desde</th></tr></thead>
          <tbody>${(data.tenants || []).map((t) => `
            <tr>
              <td>${t.id}</td>
              <td>${esc(t.name)}</td>
              <td><code>${esc(t.ip)}</code></td>
              <td>${esc(t.role)}</td>
              <td>${fmtDate(t.created_at)}</td>
            </tr>`).join('')}
          </tbody>
        </table>
      </div>`;
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

function fmtRelative(iso) {
  if (!iso) return 'nunca';
  const diff = Date.now() - new Date(iso).getTime();
  if (Number.isNaN(diff)) return '—';
  const mins = Math.floor(diff / 60000);
  if (mins < 1) return 'agora';
  if (mins < 60) return `há ${mins} min`;
  const hours = Math.floor(mins / 60);
  if (hours < 24) return `há ${hours}h`;
  const days = Math.floor(hours / 24);
  if (days === 1) return 'ontem';
  if (days < 7) return `há ${days} dias`;
  return fmtDate(iso);
}

function userInitials(displayName, username) {
  const src = (displayName || username || '?').trim();
  const parts = src.split(/\s+/).filter(Boolean);
  if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
  if (src.includes('@')) return src[0].toUpperCase();
  return src.slice(0, 2).toUpperCase();
}

function roleBadgeHtml(role) {
  const cls = {
    super_admin: 'role-super',
    ops_lead: 'role-lead',
    technician: 'role-tech',
    noc: 'role-noc',
    sales_admin: 'role-sales-admin',
    sales_support: 'role-sales-support',
    finance: 'role-finance',
    marketing: 'role-marketing',
    seo: 'role-seo',
    developer: 'role-developer',
    devops: 'role-devops',
    security_analyst: 'role-security',
    content_editor: 'role-content',
    agentic_operator: 'role-agentic',
  }[role] || 'role-default';
  return `<span class="role-badge ${cls}">${esc(roleLabel(role))}</span>`;
}

function mfaBadgeHtml(user) {
  if (user.totp_enabled) {
    const backups = Number(user.backup_codes_remaining || 0);
    const hint = backups > 0 ? ` · ${backups} backup` : '';
    return `<span class="badge ok">2FA${hint}</span>`;
  }
  return '<span class="badge review">sem 2FA</span>';
}

const ROLE_OPTIONS = [
  { value: 'super_admin', label: 'Super Admin', group: 'Ops' },
  { value: 'ops_lead', label: 'Chefe Ops', group: 'Ops' },
  { value: 'technician', label: 'Suporte', group: 'Ops' },
  { value: 'noc', label: 'NOC', group: 'Ops' },
  { value: 'sales_admin', label: 'Sales Admin', group: 'Comercial' },
  { value: 'sales_support', label: 'Sales Support', group: 'Comercial' },
  { value: 'finance', label: 'Financeiro', group: 'Negócio' },
  { value: 'marketing', label: 'Marketing', group: 'Negócio' },
  { value: 'seo', label: 'SEO', group: 'Negócio' },
  { value: 'developer', label: 'Developer', group: 'Plataforma' },
  { value: 'devops', label: 'DevOps', group: 'Plataforma' },
  { value: 'security_analyst', label: 'Segurança / SOC', group: 'Plataforma' },
  { value: 'content_editor', label: 'Conteúdo / CMS', group: 'Plataforma' },
  { value: 'agentic_operator', label: 'Operador Agentes IA', group: 'Plataforma' },
];

const ASSIGNABLE_ROLE_OPTIONS = ROLE_OPTIONS.filter((r) => r.value !== 'super_admin');

function registrationRoleSelectHtml(selected = 'technician') {
  const groups = [...new Set(ASSIGNABLE_ROLE_OPTIONS.map((r) => r.group))];
  return groups.map((group) => {
    const opts = ASSIGNABLE_ROLE_OPTIONS.filter((r) => r.group === group)
      .map((r) => `<option value="${r.value}" ${r.value === selected ? 'selected' : ''}>${esc(r.label)}</option>`)
      .join('');
    return `<optgroup label="${esc(group)}">${opts}</optgroup>`;
  }).join('');
}

function roleSelectHtml(username, current, assignableOnly = true) {
  const options = assignableOnly && current !== 'super_admin'
    ? ASSIGNABLE_ROLE_OPTIONS
    : ROLE_OPTIONS;
  const opts = options.map((r) =>
    `<option value="${r.value}" ${r.value === current ? 'selected' : ''}>${r.label}</option>`
  ).join('');
  return `<select class="admin-role" data-user="${esc(username)}">${opts}</select>`;
}

async function saveUser(username, payload, msgEl) {
  if (msgEl) msgEl.textContent = 'Salvando…';
  try {
    await api(`/v1/auth/users/${encodeURIComponent(username)}`, {
      method: 'PATCH',
      body: JSON.stringify(payload),
    });
    if (msgEl) {
      msgEl.textContent = 'Salvo';
      msgEl.className = 'admin-msg ok';
    }
    closeTeamDrawer();
    await renderAdmin();
  } catch (e) {
    if (msgEl) {
      msgEl.textContent = e.message;
      msgEl.className = 'admin-msg err';
    }
    throw e;
  }
}

function filterAdminUsers(users) {
  const { q, role, status, mfa } = state.adminFilter;
  const query = (q || '').trim().toLowerCase();
  return users.filter((u) => {
    if (role !== 'all' && u.role !== role) return false;
    if (status === 'active' && !u.active) return false;
    if (status === 'inactive' && u.active) return false;
    if (mfa === 'on' && !u.totp_enabled) return false;
    if (mfa === 'off' && u.totp_enabled) return false;
    if (!query) return true;
    const hay = [
      u.username,
      u.email,
      u.display_name,
      roleLabel(u.role),
    ].join(' ').toLowerCase();
    return hay.includes(query);
  });
}

function closeTeamDrawer() {
  const drawer = document.getElementById('team-drawer');
  if (!drawer) return;
  drawer.classList.add('hidden');
  drawer.setAttribute('aria-hidden', 'true');
  state.adminSelected = null;
}

function bindTeamDrawerClose() {
  document.querySelectorAll('[data-close-team-drawer]').forEach((el) => {
    el.onclick = closeTeamDrawer;
  });
}

function openTeamDrawer(username) {
  const user = state.adminUsers.find((u) => u.username === username);
  if (!user) return;
  state.adminSelected = username;
  const drawer = document.getElementById('team-drawer');
  const body = document.getElementById('team-drawer-body');
  const title = document.getElementById('team-drawer-title');
  if (!drawer || !body) return;

  const email = user.email || (user.username.includes('@') ? user.username : '—');
  const isRoot = user.username === 'root';
  title.textContent = user.display_name || user.username;

  body.innerHTML = `
    <div class="team-drawer-profile">
      <div class="team-avatar team-avatar-lg" aria-hidden="true">${esc(userInitials(user.display_name, user.username))}</div>
      <div>
        <p class="team-drawer-name">${esc(user.display_name || user.username)}</p>
        <p class="ticket-meta">${esc(email)}</p>
        <p class="ticket-meta"><code>${esc(user.username)}</code></p>
      </div>
    </div>
    <dl class="kv team-drawer-meta">
      <dt>Criado</dt><dd>${fmtDate(user.created_at)}</dd>
      <dt>Último login</dt><dd>${fmtRelative(user.last_login_at)}</dd>
      <dt>Segurança</dt><dd>${mfaBadgeHtml(user)}</dd>
    </dl>
    <form id="team-drawer-form" class="team-drawer-form" autocomplete="off">
      <label>Nome de exibição
        <input type="text" id="team-drawer-display" value="${esc(user.display_name || '')}" placeholder="Nome"/>
      </label>
      <label>Perfil
        ${roleSelectHtml(user.username, user.role, !isRoot)}
      </label>
      <label>Estado da conta
        <select id="team-drawer-active" ${isRoot ? 'disabled' : ''}>
          <option value="1" ${user.active ? 'selected' : ''}>ativo</option>
          <option value="0" ${!user.active ? 'selected' : ''}>inativo</option>
        </select>
      </label>
      <label>Nova senha
        <input type="password" id="team-drawer-password" placeholder="opcional (mín. 6 caracteres)" minlength="6"/>
      </label>
      ${user.totp_enabled ? `
      <div class="team-drawer-danger">
        <p class="ticket-meta">2FA ativo — o usuário pode recuperar no login ou você pode resetar aqui.</p>
        <button type="button" class="btn btn-ghost btn-sm" id="team-reset-2fa">Resetar 2FA</button>
      </div>` : ''}
      <p id="team-drawer-msg" class="admin-msg" hidden></p>
      <div class="team-drawer-actions">
        <button type="submit" class="btn btn-primary">Salvar alterações</button>
        <button type="button" class="btn btn-ghost" data-close-team-drawer>Cancelar</button>
      </div>
    </form>`;

  drawer.classList.remove('hidden');
  drawer.setAttribute('aria-hidden', 'false');
  bindTeamDrawerClose();

  body.querySelector('#team-drawer-form')?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const msgEl = body.querySelector('#team-drawer-msg');
    msgEl.hidden = false;
    const payload = {
      display_name: body.querySelector('#team-drawer-display')?.value?.trim() || null,
      role: body.querySelector('.admin-role')?.value,
      active: body.querySelector('#team-drawer-active')?.value === '1',
    };
    const pwd = body.querySelector('#team-drawer-password')?.value;
    if (pwd && pwd.length >= 6) payload.password = pwd;
    try {
      await saveUser(user.username, payload, msgEl);
    } catch {
      /* msg shown */
    }
  });

  body.querySelector('#team-reset-2fa')?.addEventListener('click', async () => {
    const msgEl = body.querySelector('#team-drawer-msg');
    if (!window.confirm(`Resetar 2FA de ${user.username}? O usuário entrará só com senha até reconfigurar.`)) return;
    msgEl.hidden = false;
    msgEl.textContent = 'Resetando…';
    msgEl.className = 'admin-msg';
    try {
      await api(`/v1/auth/users/${encodeURIComponent(user.username)}/reset-2fa`, { method: 'POST' });
      msgEl.textContent = '2FA resetado';
      msgEl.className = 'admin-msg ok';
      closeTeamDrawer();
      await renderAdmin();
    } catch (err) {
      msgEl.textContent = err.message;
      msgEl.className = 'admin-msg err';
    }
  });
}

async function renderAdmin() {
  const el = document.getElementById('admin-content');
  if (!canManageUsers()) {
    el.innerHTML = '<p class="loading">Sem permissão</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando equipe…</p>';
  try {
    const [usersData, regData] = await Promise.all([
      api('/v1/auth/users'),
      api('/v1/auth/registration-requests').catch(() => ({ pending_count: 0 })),
    ]);
    state.adminUsers = usersData.users || [];
    const users = state.adminUsers;
    const filtered = filterAdminUsers(users);
    const activeCount = users.filter((u) => u.active).length;
    const mfaCount = users.filter((u) => u.totp_enabled).length;
    const inactiveCount = users.length - activeCount;
    const pending = regData.pending_count || 0;
    const { q, role, status, mfa } = state.adminFilter;

    const rows = filtered.map((u) => `
      <tr class="team-row" data-user="${esc(u.username)}" tabindex="0">
        <td>
          <div class="team-user-cell">
            <div class="team-avatar" aria-hidden="true">${esc(userInitials(u.display_name, u.username))}</div>
            <div>
              <strong class="team-user-name">${esc(u.display_name || u.username)}</strong>
              <span class="team-user-email">${esc(u.email || u.username)}</span>
            </div>
          </div>
        </td>
        <td>${roleBadgeHtml(u.role)}</td>
        <td>${mfaBadgeHtml(u)}</td>
        <td class="team-muted">${fmtRelative(u.last_login_at)}</td>
        <td>${u.active ? '<span class="badge ok">ativo</span>' : '<span class="badge closed">inativo</span>'}</td>
        <td class="team-actions">
          <button type="button" class="btn btn-ghost btn-sm team-edit-btn" data-user="${esc(u.username)}">Editar</button>
        </td>
      </tr>`).join('');

    el.innerHTML = `
      <div class="team-admin">
        <header class="team-admin-head">
          <div>
            <h2 class="team-admin-title">Equipe Ligbox</h2>
            <p class="ticket-meta">Gestão de acessos ao Support Desk</p>
          </div>
          <button type="button" class="btn btn-ghost btn-sm" id="team-goto-messages">
            Pedidos de cadastro${pending ? ` <span class="badge review">${pending}</span>` : ''}
          </button>
        </header>

        <div class="team-kpi-grid">
          <div class="team-kpi card"><span class="team-kpi-val">${users.length}</span><span class="team-kpi-label">membros</span></div>
          <div class="team-kpi card"><span class="team-kpi-val">${activeCount}</span><span class="team-kpi-label">ativos</span></div>
          <div class="team-kpi card"><span class="team-kpi-val">${mfaCount}</span><span class="team-kpi-label">com 2FA</span></div>
          <div class="team-kpi card"><span class="team-kpi-val">${inactiveCount}</span><span class="team-kpi-label">inativos</span></div>
        </div>

        <div class="team-toolbar card">
          <label class="team-search">
            <span class="sr-only">Buscar</span>
            <input type="search" id="team-filter-q" placeholder="Buscar nome, e-mail ou perfil…" value="${esc(q)}"/>
          </label>
          <label>Perfil
            <select id="team-filter-role">
              <option value="all" ${role === 'all' ? 'selected' : ''}>Todos</option>
              ${ROLE_OPTIONS.map((r) => `<option value="${r.value}" ${role === r.value ? 'selected' : ''}>${r.label}</option>`).join('')}
            </select>
          </label>
          <label>Estado
            <select id="team-filter-status">
              <option value="all" ${status === 'all' ? 'selected' : ''}>Todos</option>
              <option value="active" ${status === 'active' ? 'selected' : ''}>Ativos</option>
              <option value="inactive" ${status === 'inactive' ? 'selected' : ''}>Inativos</option>
            </select>
          </label>
          <label>2FA
            <select id="team-filter-mfa">
              <option value="all" ${mfa === 'all' ? 'selected' : ''}>Todos</option>
              <option value="on" ${mfa === 'on' ? 'selected' : ''}>Com 2FA</option>
              <option value="off" ${mfa === 'off' ? 'selected' : ''}>Sem 2FA</option>
            </select>
          </label>
        </div>

        <div class="card team-table-wrap">
          <table class="data-table team-table">
            <thead>
              <tr>
                <th>Membro</th>
                <th>Perfil</th>
                <th>Segurança</th>
                <th>Último login</th>
                <th>Estado</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              ${rows || '<tr><td colspan="6" class="loading">Nenhum membro encontrado</td></tr>'}
            </tbody>
          </table>
          <p class="team-table-foot ticket-meta">${filtered.length} de ${users.length} membros</p>
        </div>
      </div>`;

    const applyFilters = () => {
      state.adminFilter = {
        q: document.getElementById('team-filter-q')?.value || '',
        role: document.getElementById('team-filter-role')?.value || 'all',
        status: document.getElementById('team-filter-status')?.value || 'all',
        mfa: document.getElementById('team-filter-mfa')?.value || 'all',
      };
      renderAdmin();
    };

    document.getElementById('team-filter-q')?.addEventListener('input', () => {
      clearTimeout(state._teamSearchTimer);
      state._teamSearchTimer = setTimeout(applyFilters, 200);
    });
    ['team-filter-role', 'team-filter-status', 'team-filter-mfa'].forEach((id) => {
      document.getElementById(id)?.addEventListener('change', applyFilters);
    });
    document.getElementById('team-goto-messages')?.addEventListener('click', () => setView('messages'));

    el.querySelectorAll('.team-edit-btn').forEach((btn) => {
      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        openTeamDrawer(btn.dataset.user);
      });
    });
    el.querySelectorAll('.team-row').forEach((row) => {
      row.addEventListener('click', (e) => {
        if (e.target.closest('button')) return;
        openTeamDrawer(row.dataset.user);
      });
      row.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          openTeamDrawer(row.dataset.user);
        }
      });
    });

    if (state.adminSelected) {
      openTeamDrawer(state.adminSelected);
    }
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function renderModules() {
  const el = document.getElementById('modules-content');
  if (!el) return;
  const user = getUser();
  if (user?.role !== 'super_admin') {
    el.innerHTML = '<p class="loading">Apenas Super Admin pode gerenciar módulos.</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando módulos…</p>';
  try {
    await DeskModules.load();
    const mods = DeskModules.list;
    el.innerHTML = `
      <div class="card modules-admin-card">
        <h3 style="margin-top:0">Módulos do Desk</h3>
        <p class="ticket-meta">Desativar um módulo remove-o do menu e desliga enriquecimentos na API — o núcleo continua estável.</p>
        <div class="modules-grid">
          ${mods.map((m) => `
            <label class="module-row${m.locked ? ' module-row--locked' : ''}">
              <div class="module-row-main">
                <strong>${esc(m.label)}</strong>
                <span class="ticket-meta">${esc(m.description)}</span>
                <code class="module-id">${esc(m.id)}</code>
                ${m.locked ? '<span class="badge open">núcleo</span>' : ''}
              </div>
              <input type="checkbox" data-module-toggle="${esc(m.id)}" ${m.enabled ? 'checked' : ''} ${m.locked ? 'disabled' : ''} aria-label="Activar ${esc(m.label)}" />
            </label>`).join('')}
        </div>
      </div>`;
    el.querySelectorAll('[data-module-toggle]').forEach((input) => {
      input.addEventListener('change', async () => {
        const id = input.dataset.moduleToggle;
        input.disabled = true;
        try {
          await api(`/v1/modules/${encodeURIComponent(id)}`, {
            method: 'PATCH',
            body: JSON.stringify({ enabled: input.checked }),
          });
          await DeskModules.load();
          applyRoleNav();
          DeskModules.applyVisibility();
          if (!DeskModules.isViewEnabled(state.view)) setView('dashboard');
          else refresh();
        } catch (e) {
          input.checked = !input.checked;
          alert(e.message || 'Falha ao actualizar módulo');
        } finally {
          input.disabled = false;
        }
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

const REG_ROLE_LABELS = ROLE_LABELS;

async function renderMessages() {
  const el = document.getElementById('messages-content');
  if (!canManageUsers()) {
    el.innerHTML = '<p class="loading">Sem permissão</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando pedidos…</p>';
  try {
    const data = await api('/v1/auth/registration-requests');
    const items = data.requests || [];
    const pending = items.filter((r) => r.status === 'pending');
    const history = items.filter((r) => r.status !== 'pending');
    const pendingCards = pending.map((r) => `
      <div class="card admin-user-card" data-req="${r.id}">
        <div class="health-card-head">
          <div>
            <h3 style="margin:0">${esc(r.email)}</h3>
            <p class="ticket-meta">${esc(r.display_name || '—')} · ${fmtDate(r.created_at)}</p>
          </div>
          <span class="badge review">pendente</span>
        </div>
        <label>Perfil a atribuir
          <select class="req-role">
            ${registrationRoleSelectHtml('technician')}
          </select>
        </label>
        <div class="actions" style="margin-top:0.75rem">
          <button type="button" class="btn btn-primary req-approve">Aprovar</button>
          <button type="button" class="btn btn-ghost req-reject">Rejeitar</button>
          <span class="admin-msg"></span>
        </div>
      </div>`).join('');
    const historyRows = history.map((r) => `
      <tr>
        <td>${esc(r.email)}</td>
        <td><span class="badge ${r.status === 'active' ? 'ok' : r.status === 'rejected' ? 'closed' : 'review'}">${esc(statusLabel(r.status))}</span></td>
        <td>${esc(r.role ? roleLabel(r.role) : '—')}</td>
        <td>${fmtDate(r.updated_at || r.created_at)}</td>
      </tr>`).join('');
    el.innerHTML = `
      <div class="admin-users">
        <h3 style="margin:0 0 0.5rem">Pedidos pendentes <span class="ticket-meta">(${pending.length})</span></h3>
        ${pendingCards || '<p class="loading">Nenhum pedido pendente</p>'}
        ${history.length ? `
          <h3 style="margin:1.5rem 0 0.5rem">Histórico</h3>
          <div class="card" style="overflow:auto">
            <table class="data-table">
              <thead><tr><th>E-mail</th><th>Estado</th><th>Perfil</th><th>Atualizado</th></tr></thead>
              <tbody>${historyRows}</tbody>
            </table>
          </div>` : ''}
      </div>`;
    el.querySelectorAll('[data-req]').forEach((card) => {
      const id = card.dataset.req;
      const msgEl = card.querySelector('.admin-msg');
      card.querySelector('.req-approve')?.addEventListener('click', async () => {
        msgEl.textContent = '…';
        try {
          const role = card.querySelector('.req-role')?.value;
          await api(`/v1/auth/registration-requests/${id}/approve`, { method: 'POST', body: JSON.stringify({ role }) });
          msgEl.textContent = 'Aprovado — email enviado';
          msgEl.className = 'admin-msg ok';
          await renderMessages();
        } catch (e) {
          msgEl.textContent = e.message;
          msgEl.className = 'admin-msg err';
        }
      });
      card.querySelector('.req-reject')?.addEventListener('click', async () => {
        const reason = window.prompt('Motivo da rejeição (opcional):') || '';
        msgEl.textContent = '…';
        try {
          await api(`/v1/auth/registration-requests/${id}/reject`, {
            method: 'POST',
            body: JSON.stringify({ reason: reason || null }),
          });
          msgEl.textContent = 'Rejeitado';
          msgEl.className = 'admin-msg ok';
          await renderMessages();
        } catch (e) {
          msgEl.textContent = e.message;
          msgEl.className = 'admin-msg err';
        }
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function renderAccount(force = false) {
  const el = document.getElementById('account-content');
  if (state.accountLoaded && !force) {
    return;
  }
  const saved = force ? null : readAccountPwdForm();
  el.innerHTML = '<p class="loading">Carregando…</p>';
  try {
    const me = await api('/v1/auth/me');
    const totpOn = Boolean(me.totp_enabled || me.mfa_enabled);
    el.innerHTML = `
      <div class="card account-card" style="max-width:480px">
        <h3 style="margin:0 0 0.75rem">Minha conta</h3>
        <dl class="kv account-kv">
          <dt>E-mail / login</dt><dd><code>${esc(me.email || me.username)}</code></dd>
          <dt>Perfil</dt><dd>${esc(roleLabel(me.role))}</dd>
          <dt>Nome</dt><dd>${esc(me.display_name || '—')}</dd>
          <dt>Último login</dt><dd>${fmtDate(me.last_login_at)}</dd>
          <dt>2FA (app)</dt><dd>${totpOn ? '<span class="badge ok">ativo</span>' : '<span class="badge review">não configurado</span>'}</dd>
          ${totpOn ? `<dt>Códigos backup</dt><dd>${Number(me.backup_codes_remaining || 0)} restante(s)</dd>` : ''}
        </dl>
        <hr style="border:none;border-top:1px solid var(--border);margin:1.25rem 0"/>
        <h4 style="margin:0 0 0.5rem">Alterar senha</h4>
        <p class="ticket-meta" style="margin:0 0 1rem">
          ${totpOn
            ? 'Por segurança, confirme a senha atual e o código do autenticador (sessão aberta). Se perdeu o app, use <a href="/login.html">recuperação no login</a> ou um código de backup.'
            : 'Informe a senha atual e escolha uma nova (mín. 8 caracteres).'}
        </p>
        <form id="account-pwd-form" class="account-pwd-form" autocomplete="off">
          <label for="acct-pwd-current">Senha atual
            <input type="password" id="acct-pwd-current" name="current_password" autocomplete="current-password" required/>
          </label>
          <label for="acct-pwd-new">Nova senha
            <input type="password" id="acct-pwd-new" name="new_password" autocomplete="new-password" required minlength="8"/>
          </label>
          <label for="acct-pwd-new2">Confirmar nova senha
            <input type="password" id="acct-pwd-new2" name="new_password2" autocomplete="off" required minlength="8"/>
          </label>
          ${totpOn ? `
          <label for="acct-pwd-totp">Código 2FA (app)
            <input type="text" id="acct-pwd-totp" name="totp_code" inputmode="numeric" maxlength="6" placeholder="000000" autocomplete="one-time-code" required/>
          </label>` : ''}
          <p id="account-pwd-error" class="login-error" hidden></p>
          <p id="account-pwd-ok" class="login-notice" hidden></p>
          <button type="submit" class="btn btn-primary" style="width:100%;margin-top:0.5rem">Salvar nova senha</button>
        </form>
      </div>`;
    restoreAccountPwdForm(saved);
    bindAccountPwdForm(totpOn);
    state.accountLoaded = true;
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
    state.accountLoaded = false;
  }
}

function readAccountPwdForm() {
  const form = document.getElementById('account-pwd-form');
  if (!form) return null;
  const get = (id) => document.getElementById(id)?.value ?? '';
  const hasValue = ['acct-pwd-current', 'acct-pwd-new', 'acct-pwd-new2', 'acct-pwd-totp']
    .some((id) => get(id));
  if (!hasValue) return null;
  return {
    current: get('acct-pwd-current'),
    neu: get('acct-pwd-new'),
    neu2: get('acct-pwd-new2'),
    totp: get('acct-pwd-totp'),
  };
}

function restoreAccountPwdForm(saved) {
  if (!saved) return;
  const set = (id, val) => {
    const el = document.getElementById(id);
    if (el && val) el.value = val;
  };
  set('acct-pwd-current', saved.current);
  set('acct-pwd-new', saved.neu);
  set('acct-pwd-new2', saved.neu2);
  set('acct-pwd-totp', saved.totp);
}

function bindAccountPwdForm(totpOn) {
  const form = document.getElementById('account-pwd-form');
  const errEl = document.getElementById('account-pwd-error');
  const okEl = document.getElementById('account-pwd-ok');
  if (!form || form.dataset.bound === '1') return;
  form.dataset.bound = '1';
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    errEl.hidden = true;
    okEl.hidden = true;
    const cur = document.getElementById('acct-pwd-current')?.value ?? '';
    const neu = document.getElementById('acct-pwd-new')?.value ?? '';
    const neu2 = document.getElementById('acct-pwd-new2')?.value ?? '';
    if (neu !== neu2) {
      errEl.textContent = 'As senhas não coincidem';
      errEl.hidden = false;
      return;
    }
    const payload = { current_password: cur, new_password: neu };
    if (totpOn) {
      payload.totp_code = (document.getElementById('acct-pwd-totp')?.value ?? '').trim();
    }
    const btn = form.querySelector('button[type="submit"]');
    btn.disabled = true;
    try {
      await api('/v1/auth/change-password', {
        method: 'POST',
        body: JSON.stringify(payload),
      });
      okEl.textContent = 'Senha alterada com sucesso.';
      okEl.hidden = false;
      form.reset();
    } catch (ex) {
      errEl.textContent = ex.message;
      errEl.hidden = false;
    } finally {
      btn.disabled = false;
    }
  });
}

const SOC_EVENT_LABELS = {
  'session.started': 'Sessão iniciada',
  'domain.validated': 'Domínio validado',
  'dns.applied': 'DNS aplicado',
  'onboarding.started': 'Onboarding iniciado',
  'account.created': 'Conta criada',
  'infra.synced': 'Infra sincronizada',
  'onboarding.completed': 'Onboarding concluído',
  'onboarding.failed': 'Onboarding falhou',
  'integration.test': 'Teste integração',
  ...SECURITY_EVENT_LABELS,
};

function socWindowHours() {
  return { '24h': 24, '48h': 48, '7d': 168 }[state.socWindow] || 24;
}

function socEventSeverity(eventType) {
  if (eventType?.startsWith('security.')) {
    if (eventType.includes('blocked') || eventType.includes('rejected') || eventType.includes('anomaly')) return 'high';
    if (eventType.includes('csp') || eventType.includes('rate')) return 'warn';
    return 'info';
  }
  if (eventType === 'onboarding.failed') return 'high';
  if (eventType === 'onboarding.started' || eventType === 'session.started') return 'warn';
  if (eventType === 'onboarding.completed' || eventType === 'account.created') return 'ok';
  return 'info';
}

function socAreaChartSvg(values, width = 320, height = 88) {
  const data = values?.length ? values : [0, 0, 0, 0, 0, 0];
  const max = Math.max(...data, 1);
  const padX = 4;
  const padY = 6;
  const innerW = width - padX * 2;
  const innerH = height - padY * 2;
  const pts = data.map((v, i) => {
    const x = padX + (i / Math.max(data.length - 1, 1)) * innerW;
    const y = padY + innerH - (v / max) * innerH;
    return [x, y];
  });
  const line = pts.map((p) => p.join(',')).join(' ');
  const area = `${padX},${padY + innerH} ${line} ${padX + innerW},${padY + innerH}`;
  return `
    <svg class="soc-area-chart" viewBox="0 0 ${width} ${height}" width="100%" height="${height}" aria-hidden="true">
      <defs>
        <linearGradient id="soc-area-grad" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#22d3ee" stop-opacity="0.35"/>
          <stop offset="100%" stop-color="#22d3ee" stop-opacity="0"/>
        </linearGradient>
      </defs>
      <polygon points="${area}" fill="url(#soc-area-grad)"/>
      <polyline fill="none" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="${line}"/>
    </svg>`;
}

function socPipelineHtml(stages, total) {
  const order = ['started', 'domain_validated', 'dns_applied', 'account_created', 'infra_synced', 'completed'];
  const max = Math.max(total || 1, ...order.map((k) => stages[k] || 0));
  return order.map((key) => {
    const n = stages[key] || 0;
    const pct = max ? Math.round((n / max) * 100) : 0;
    return `
      <div class="soc-pipe-row">
        <span class="soc-pipe-label">${esc(FUNNEL_LABELS[key] || key)}</span>
        <div class="soc-pipe-bar"><div class="soc-pipe-fill" style="width:${pct}%"></div></div>
        <span class="soc-pipe-count">${n}</span>
      </div>`;
  }).join('');
}

function socStatusKpiClass(status) {
  if (status === 'ok') return 'ok';
  if (status === 'critical') return 'critical';
  return 'warn';
}

function socSessionRingStage(stage) {
  if (stage === 'completed' || stage === 'failed') return stage;
  return 'active';
}

function closeSocTestModal() {
  const modal = document.getElementById('soc-test-modal');
  if (!modal) return;
  modal.classList.add('hidden');
  modal.setAttribute('aria-hidden', 'true');
}

function bindSocTestModal() {
  document.querySelectorAll('[data-close-soc-test-modal]').forEach((el) => {
    el.addEventListener('click', closeSocTestModal);
  });
}

function showSocWebhookTestResult(result) {
  const modal = document.getElementById('soc-test-modal');
  const title = document.getElementById('soc-test-modal-title');
  const sub = document.getElementById('soc-test-modal-sub');
  const body = document.getElementById('soc-test-modal-body');
  if (!modal || !body) return;

  const ok = result.accepted && result.status === 'accepted';
  const dup = result.duplicate === true;
  title.textContent = ok ? (dup ? 'Webhook OK (duplicado)' : 'Webhook OK') : 'Webhook com problema';
  sub.textContent = fmtDate(result.tested_at || new Date().toISOString());

  body.innerHTML = `
    <div class="soc-test-result">
      <div class="soc-test-status soc-test-status--${ok ? 'ok' : 'fail'}">
        <span class="soc-sev soc-sev--${ok ? 'ok' : 'high'}"></span>
        ${esc(result.message || (ok ? 'Integração VM112 → VM122 respondendo corretamente.' : 'Falha ao processar webhook.'))}
      </div>
      <dl class="soc-test-kv">
        <dt>Status</dt><dd>${esc(result.status || '—')}</dd>
        <dt>Evento</dt><dd>${esc(result.event || '—')}</dd>
        <dt>Origem</dt><dd>${esc(result.source || '—')}</dd>
        <dt>Domínio</dt><dd>${esc(result.domain || '—')}</dd>
        <dt>Sessão</dt><dd>${esc(result.session_id || '—')}</dd>
        <dt>Duplicado</dt><dd>${dup ? 'sim' : 'não'}</dd>
        <dt>Ticket criado</dt><dd>${result.ticket_created ? `sim (#${result.ticket_id})` : 'não'}</dd>
        <dt>Disparado por</dt><dd>${esc(result.triggered_by || '—')}</dd>
      </dl>
      <p class="soc-test-hint">
        Este teste simula um evento <code>integration.test</code> no endpoint
        <code>POST /api/v1/webhooks/onboard</code> — o mesmo caminho usado pela VM112.
        Não cria ticket de onboarding; apenas valida que a API grava o evento e o SOC consegue lê-lo.
      </p>
      <div class="soc-test-actions">
        <button type="button" class="soc-btn" data-soc-goto-events>Ver em Eventos</button>
        <button type="button" class="soc-btn soc-btn--ghost" data-close-soc-test-modal>Fechar</button>
      </div>
    </div>`;

  body.querySelector('[data-soc-goto-events]')?.addEventListener('click', () => {
    closeSocTestModal();
    state.eventSourceFilter = 'vm112-onboard';
    document.querySelectorAll('.filter-btn[data-kind="event"]').forEach((b) => {
      b.classList.toggle('active', b.dataset.source === 'vm112-onboard');
    });
    setView('events');
  });
  body.querySelector('[data-close-soc-test-modal]')?.addEventListener('click', closeSocTestModal);

  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

function showSocWebhookTestError(err) {
  const modal = document.getElementById('soc-test-modal');
  const title = document.getElementById('soc-test-modal-title');
  const sub = document.getElementById('soc-test-modal-sub');
  const body = document.getElementById('soc-test-modal-body');
  if (!modal || !body) return;

  const msg = err?.message || String(err);
  const is403 = /403|insufficient permissions|permiss/i.test(msg);
  title.textContent = 'Falha no teste';
  sub.textContent = 'Não foi possível completar o teste';

  body.innerHTML = `
    <div class="soc-test-result">
      <div class="soc-test-status soc-test-status--fail">
        <span class="soc-sev soc-sev--high"></span>
        ${esc(msg)}
      </div>
      ${is403 ? `<p class="soc-test-hint">Apenas perfis <strong>super_admin</strong> e <strong>admin</strong> podem executar o teste de webhook.</p>` : ''}
      <p class="soc-test-hint">Verifique se a API está online, se a sessão não expirou e se o usuário tem permissão.</p>
      <div class="soc-test-actions">
        <button type="button" class="soc-btn soc-btn--ghost" data-close-soc-test-modal>Fechar</button>
      </div>
    </div>`;
  body.querySelector('[data-close-soc-test-modal]')?.addEventListener('click', closeSocTestModal);
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

function showOpenPanelTestResult(result) {
  const modal = document.getElementById('soc-test-modal');
  const title = document.getElementById('soc-test-modal-title');
  const sub = document.getElementById('soc-test-modal-sub');
  const body = document.getElementById('soc-test-modal-body');
  if (!modal || !body) return;

  const ok = result.ok === true;
  title.textContent = ok ? 'OpenPanel API — confirmado' : 'OpenPanel API — falha';
  sub.textContent = `Spec 028 · ${result.steps_passed || 0}/${result.steps_total || 0} passos · ${result.duration_sec || '—'}s`;

  const steps = (result.steps || []).map((s) => `
    <li class="badge ${s.ok ? 'ok' : 'escalated'}">
      <strong>${esc(s.name)}</strong> — ${esc(s.detail || (s.ok ? 'OK' : 'FAIL'))}
    </li>`).join('');

  body.innerHTML = `
    <div class="soc-test-result">
      <div class="soc-test-status ${ok ? 'soc-test-status--ok' : 'soc-test-status--fail'}">
        <span class="soc-sev ${ok ? 'soc-sev--low' : 'soc-sev--high'}"></span>
        ${esc(result.message || (ok ? 'Multidomínio OK' : 'Falha'))}
      </div>
      <ul class="soc-alerts" style="list-style:none;padding:0;margin:0.75rem 0;display:flex;flex-direction:column;gap:0.35rem">${steps || '<li>—</li>'}</ul>
      <p class="soc-test-hint">
        Suite <code>openpanel-multidomain-api-confirm</code> — usa os nomes que indicou nos campos
        (ou gera automaticamente). Aguarde até 3 minutos sem sair da página.
        Script CLI: <code>scripts/test-openpanel-multidomain-api.sh</code>
      </p>
      <div class="soc-test-actions">
        <button type="button" class="soc-btn soc-btn--ghost" data-close-soc-test-modal>Fechar</button>
      </div>
    </div>`;
  body.querySelector('[data-close-soc-test-modal]')?.addEventListener('click', closeSocTestModal);
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

function showOpenPanelTestError(err) {
  const modal = document.getElementById('soc-test-modal');
  const title = document.getElementById('soc-test-modal-title');
  const sub = document.getElementById('soc-test-modal-sub');
  const body = document.getElementById('soc-test-modal-body');
  if (!modal || !body) return;

  const msg = err?.message || String(err);
  const is403 = /403|permiss/i.test(msg);
  title.textContent = 'OpenPanel API — erro';
  sub.textContent = 'Teste não concluído';
  body.innerHTML = `
    <div class="soc-test-result">
      <div class="soc-test-status soc-test-status--fail">
        <span class="soc-sev soc-sev--high"></span>
        ${esc(msg)}
      </div>
      ${is403 ? '<p class="soc-test-hint">Perfis: <strong>super_admin</strong>, <strong>devops</strong>, <strong>developer</strong>.</p>' : ''}
      <div class="soc-test-actions">
        <button type="button" class="soc-btn soc-btn--ghost" data-close-soc-test-modal>Fechar</button>
      </div>
    </div>`;
  body.querySelector('[data-close-soc-test-modal]')?.addEventListener('click', closeSocTestModal);
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

async function runOpenPanelApiTest() {
  const btn = document.getElementById('btn-test-openpanel-api');
  const prevLabel = btn?.textContent;
  const user1 = (document.getElementById('op-test-user1')?.value || '').trim().toLowerCase();
  const domain1 = (document.getElementById('op-test-domain1')?.value || '').trim().toLowerCase();
  const user2 = (document.getElementById('op-test-user2')?.value || '').trim().toLowerCase();
  const domain2 = (document.getElementById('op-test-domain2')?.value || '').trim().toLowerCase();
  const password = (document.getElementById('op-test-password')?.value || 'LbOpenTest805353').trim();
  const cleanup = document.getElementById('op-test-cleanup')?.checked !== false;
  const autoNames = document.getElementById('op-test-auto-names')?.checked !== false;

  const accounts = [];
  if (user1 || domain1) accounts.push({ username: user1, domain: domain1 });
  if (user2 || domain2) accounts.push({ username: user2, domain: domain2 });

  state.openPanelTestRunning = true;
  if (pollTimer) clearInterval(pollTimer);

  if (btn) {
    btn.disabled = true;
    btn.textContent = 'Simulando… aguarde (~2 min)';
  }
  try {
    const r = await apiLongRunning('/v1/vm123/openpanel/test-confirm', {
      method: 'POST',
      body: JSON.stringify({
        accounts,
        password,
        cleanup,
        auto_names: autoNames && accounts.length === 0,
        check_reference: true,
      }),
    });
    showOpenPanelTestResult(r);
  } catch (ex) {
    const msg = ex?.name === 'AbortError' || /aborted/i.test(ex?.message || '')
      ? 'Requisição interrompida — aguarde até 3 minutos sem mudar de página.'
      : (ex?.message || String(ex));
    showOpenPanelTestError({ message: msg });
  } finally {
    state.openPanelTestRunning = false;
    reschedulePoll();
    if (btn) {
      btn.disabled = false;
      btn.textContent = prevLabel || 'Executar simulação';
    }
  }
}

async function runWebhookIntegrationTest(refreshView) {
  const btn = document.getElementById('soc-btn-test') || document.getElementById('btn-test-webhook');
  const prevLabel = btn?.textContent;
  if (btn) {
    btn.disabled = true;
    btn.textContent = 'Testando…';
  }
  try {
    const r = await api('/v1/integrations/onboard/test', { method: 'POST' });
    showSocWebhookTestResult(r);
    if (refreshView === 'infra2') await renderInfra2();
    else if (refreshView === 'infra') await renderInfra();
  } catch (ex) {
    showSocWebhookTestError(ex);
  } finally {
    if (btn) {
      btn.disabled = false;
      btn.textContent = prevLabel || 'Testar webhook';
    }
  }
}

async function renderInfra2() {
  const el = document.getElementById('infra2-content');
  if (!el) return;
  if (window.DeskModules?.loaded && !DeskModules.isEnabled('infra2-soc')) {
    el.innerHTML = '<p class="loading">Módulo Infra 2 SOC desativado. Active em <strong>Módulos</strong>.</p>';
    return;
  }
  el.innerHTML = '<p class="loading">Carregando SOC…</p>';
  const windowHours = socWindowHours();
  try {
    const [health, vm112, wazuh, funnel, eventsRes, secRes, summary] = await Promise.all([
      api('/v1/integrations/health').catch(() => ({ status: 'unknown', alerts: [], vm112_onboard: {} })),
      api('/v1/infra/vm112/status').catch(() => ({ error: 'indisponível' })),
      api('/v1/infra/wazuh/status').catch(() => ({ error: 'indisponível' })),
      api(`/v1/onboard/funnel?window_hours=${windowHours}`).catch(() => ({ stages: {}, active_sessions: [], sessions_total: 0 })),
      api('/v1/webhooks/events?source=vm112-onboard').catch(() => ({ events: [] })),
      window.DeskModules?.isEnabled('wizard-security')
        ? api('/v1/security/summary?window_hours=24').catch(() => ({}))
        : Promise.resolve({}),
      api('/v1/desk/summary').catch(() => ({ tickets_open: 0, recent_tickets: [] })),
    ]);

    const onboard = health.vm112_onboard || {};
    const lastWh = onboard.last_webhook || {};
    const gapMin = onboard.gap_minutes != null ? Math.round(onboard.gap_minutes) : null;
    const alerts = health.alerts || [];
    const vmOk = vm112.vm112?.status === 'ok';
    const wazuhOk = wazuh.api_online === true || wazuh.http_status === 401 || wazuh.http_status === 200;
    const intStatus = health.status || 'unknown';
    const liveCls = intStatus === 'ok' ? '' : intStatus === 'critical' ? 'critical' : 'warn';

    const secSummary = secRes || {};
    const secRecent = (secSummary.recent || []).map((ev) => ({
      id: `sec-${ev.id}`,
      event_type: ev.event_type,
      created_at: ev.created_at,
      payload: { domain: ev.domain, session_id: ev.session_id },
      domain: ev.domain,
      _security: true,
    }));

    const allEvents = (eventsRes.events || []).map((ev) => ({
      ...ev,
      payload: typeof ev.payload === 'object' ? ev.payload : {},
    }));
    const windowEvents = allEvents.filter((ev) => isInWindow(ev.created_at, windowHours));
    const chartBuckets = bucketEvents(windowEvents, windowHours, 24);
    const eventsPerHour = windowHours ? Math.round((windowEvents.length / windowHours) * 10) / 10 : 0;

    const feedEvents = [...allEvents, ...secRecent]
      .sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0))
      .slice(0, 18);

    const sessions = (funnel.active_sessions || [])
      .filter((s) => s.domain || s.session_id)
      .sort((a, b) => new Date(b.last_event_at || 0) - new Date(a.last_event_at || 0));

    const sessionTimings = {};
    if (window.DeskModules?.isEnabled('funnel-timing')) {
      const tops = sessions.slice(0, 8).filter((s) => s.session_id);
      const timingResults = await Promise.all(
        tops.map((s) => api(`/v1/onboard/sessions/${encodeURIComponent(s.session_id)}/timeline`).catch(() => null))
      );
      tops.forEach((s, i) => {
        if (timingResults[i]?.timing) sessionTimings[s.session_id] = timingResults[i].timing;
      });
    }

    const newestId = feedEvents[0]?.id;
    const flashNew = state.socLastEventId && newestId && newestId > state.socLastEventId;
    state.socLastEventId = newestId || state.socLastEventId;

    const onboardTicketsOpen = (summary.recent_tickets || []).filter(
      (t) => (t.source === 'vm112-onboard' || String(t.subject || '').includes('[onboarding]')) && t.status !== 'closed'
    ).length;

    const nowLabel = new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });

    el.innerHTML = `
      <div class="soc-console" id="soc-console-root">
        <header class="soc-header">
          <div class="soc-header-left">
            <span class="soc-live-dot ${liveCls}" aria-hidden="true"></span>
            <h3>SOC Operations Center</h3>
            <span class="soc-meta">VM112 → VM122 · atualizado ${esc(nowLabel)} · refresh 15s</span>
          </div>
          <div class="soc-header-actions">
            <select class="soc-select" id="soc-window-select" aria-label="Janela de tempo">
              <option value="24h" ${state.socWindow === '24h' ? 'selected' : ''}>24 horas</option>
              <option value="48h" ${state.socWindow === '48h' ? 'selected' : ''}>48 horas</option>
              <option value="7d" ${state.socWindow === '7d' ? 'selected' : ''}>7 dias</option>
            </select>
            <button type="button" class="soc-btn" id="soc-btn-test">Testar webhook</button>
            <button type="button" class="soc-btn soc-btn--ghost" id="soc-btn-refresh">Atualizar</button>
          </div>
        </header>

        <div class="soc-kpi-grid">
          <div class="soc-kpi soc-kpi--${socStatusKpiClass(intStatus)}">
            <span class="soc-kpi-label">Integração</span>
            <span class="soc-kpi-value">${esc(intStatus)}</span>
            <span class="soc-kpi-sub">VM112 onboard</span>
          </div>
          <div class="soc-kpi soc-kpi--${gapMin != null && gapMin > (health.webhook_gap_alert_minutes || 15) ? 'critical' : 'ok'}">
            <span class="soc-kpi-label">Gap webhook</span>
            <span class="soc-kpi-value">${gapMin != null ? `${gapMin}m` : '—'}</span>
            <span class="soc-kpi-sub">limite ${health.webhook_gap_alert_minutes || 15} min</span>
          </div>
          <div class="soc-kpi soc-kpi--info">
            <span class="soc-kpi-label">Eventos</span>
            <span class="soc-kpi-value">${windowEvents.length}</span>
            <span class="soc-kpi-sub">~${eventsPerHour}/h · ${state.socWindow}</span>
          </div>
          <div class="soc-kpi soc-kpi--info">
            <span class="soc-kpi-label">Sessões</span>
            <span class="soc-kpi-value">${funnel.sessions_total || sessions.length}</span>
            <span class="soc-kpi-sub">funil ativo</span>
          </div>
          <div class="soc-kpi soc-kpi--${onboardTicketsOpen > 0 ? 'warn' : 'ok'}">
            <span class="soc-kpi-label">Tickets onboard</span>
            <span class="soc-kpi-value">${onboardTicketsOpen}</span>
            <span class="soc-kpi-sub">abertos agora</span>
          </div>
          <div class="soc-kpi soc-kpi--${alerts.length ? 'warn' : 'ok'}">
            <span class="soc-kpi-label">Alertas</span>
            <span class="soc-kpi-value">${alerts.length}</span>
            <span class="soc-kpi-sub">${lastWh.event ? esc(lastWh.event) : 'sem eventos'}</span>
          </div>
          ${window.DeskModules?.isEnabled('wizard-security') ? `
          <div class="soc-kpi soc-kpi--${(secSummary.inputs_blocked || 0) + (secSummary.handoffs_rejected || 0) > 0 ? 'critical' : (secSummary.total || 0) > 0 ? 'warn' : 'ok'}">
            <span class="soc-kpi-label">Segurança wizard</span>
            <span class="soc-kpi-value">${secSummary.total || 0}</span>
            <span class="soc-kpi-sub">CSP ${secSummary.csp_violations || 0} · bloq ${secSummary.inputs_blocked || 0}</span>
          </div>` : ''}
        </div>

        <div class="soc-topology" aria-label="Topologia de integração">
          <div class="soc-node">
            <span class="soc-node-dot ${vmOk ? 'ok' : 'bad'}"></span>
            VM112 Wizard
          </div>
          <span class="soc-flow"><strong>webhook</strong> POST /onboard →</span>
          <div class="soc-node">
            <span class="soc-node-dot ok"></span>
            VM122 Desk
          </div>
          <span class="soc-flow">←</span>
          <div class="soc-node">
            <span class="soc-node-dot ${wazuhOk ? 'ok' : 'bad'}"></span>
            VM104 Wazuh
          </div>
          <span class="soc-flow"><strong>alertas</strong> level ≥10</span>
        </div>

        <div class="soc-main-grid">
          <div class="soc-panel">
            <div class="soc-panel-head">
              <h4>Feed ao vivo — VM112 + Segurança</h4>
              <span class="soc-meta">${feedEvents.length} recentes</span>
            </div>
            <div class="soc-panel-body">
              ${feedEvents.length ? `
              <table class="soc-feed">
                <thead><tr><th></th><th>Evento</th><th>Domínio</th><th>Hora</th></tr></thead>
                <tbody>
                  ${feedEvents.map((ev, i) => {
                    const p = ev.payload || {};
                    const sev = socEventSeverity(ev.event_type);
                    const isNew = flashNew && i === 0;
                    return `
                    <tr class="soc-feed-row${isNew ? ' soc-feed-row--new' : ''}${ev._security ? ' soc-feed-row--security' : ''}">
                      <td><span class="soc-sev soc-sev--${sev}" title="${sev}"></span></td>
                      <td class="soc-event-name">${esc(SOC_EVENT_LABELS[ev.event_type] || ev.event_type)}</td>
                      <td class="soc-event-domain">${esc(p.domain || ev.domain || '—')}</td>
                      <td class="soc-event-time">${relativeTimeAgo(ev.created_at)}</td>
                    </tr>`;
                  }).join('')}
                </tbody>
              </table>` : '<p class="soc-empty">Nenhum evento VM112 registrado</p>'}
            </div>
          </div>

          <div class="soc-panel">
            <div class="soc-panel-head">
              <h4>Volume &amp; funil</h4>
              <span class="soc-meta">${state.socWindow}</span>
            </div>
            <div class="soc-panel-body">
              <div class="soc-chart-wrap">
                <div class="soc-chart-legend">
                  <span>Eventos VM112</span>
                  <span>máx ${Math.max(...chartBuckets, 0)}</span>
                </div>
                ${socAreaChartSvg(chartBuckets)}
              </div>
              <div class="soc-pipeline">
                ${socPipelineHtml(funnel.stages || {}, funnel.sessions_total || 0)}
              </div>
            </div>
          </div>

          <div class="soc-panel">
            <div class="soc-panel-head">
              <h4>Sessões VM112</h4>
              <span class="soc-meta">${sessions.length} ativas</span>
            </div>
            <div class="soc-panel-body">
              <div class="soc-session-list">
                ${sessions.length ? sessions.slice(0, 10).map((s) => {
                  const stage = s.current_stage || 'started';
                  const ringCls = socSessionRingStage(stage);
                  const initials = (s.domain || '??').slice(0, 2).toUpperCase();
                  const tmeta = sessionTimings[s.session_id];
                  const timingBadge = tmeta
                    ? `<span class="timing-badge soc-timing-badge" title="Duração total">Σ ${esc(tmeta.total_duration_label)}</span>`
                    : '';
                  const idleHint = tmeta && !tmeta.is_completed
                    ? ` · parado ${esc(tmeta.idle_since_label)}`
                    : '';
                  return `
                  <button type="button" class="soc-session-card${s.stale ? ' stale' : ''}" data-soc-session="${esc(s.session_id || '')}" data-soc-ticket="${s.ticket_id || ''}">
                    <span class="soc-session-ring ${ringCls}">${esc(initials)}</span>
                    <span class="soc-session-main">
                      <strong>${esc(s.domain || 'sem domínio')}</strong>
                      <span>${esc(FUNNEL_LABELS[stage] || stage)} · onboarding · ${relativeTimeAgo(s.last_event_at)}${idleHint}</span>
                      ${s.session_id ? `<span class="soc-session-hash">${sessionHashHtml(s.session_id)}</span>` : ''}
                    </span>
                    ${timingBadge}
                    ${s.ticket_id ? `<span class="badge ok">#${s.ticket_id}</span>` : '<span class="badge review">—</span>'}
                  </button>`;
                }).join('') : '<p class="soc-empty">Sem sessões no período</p>'}
              </div>
            </div>
          </div>
        </div>

        <div class="soc-bottom-grid">
          <div class="soc-panel" style="min-height:160px">
            <div class="soc-panel-head"><h4>Alertas SOC</h4></div>
            <div class="soc-panel-body">
              <ul class="soc-alert-list">
                ${alerts.length ? alerts.map((a) => `
                  <li class="soc-alert-item soc-alert-item--${a.level === 'critical' ? 'critical' : 'warn'}">
                    <span class="soc-sev soc-sev--${a.level === 'critical' ? 'high' : 'warn'}"></span>
                    ${esc(a.message)}
                  </li>`).join('') : `
                  <li class="soc-alert-item soc-alert-item--ok">
                    <span class="soc-sev soc-sev--ok"></span>
                    Integração saudável — sem alertas ativos
                  </li>`}
                ${lastWh.domain ? `
                  <li class="soc-alert-item">
                    <span class="soc-sev soc-sev--info"></span>
                    Último: <strong>${esc(lastWh.event)}</strong> · ${esc(lastWh.domain)} · ${relativeTimeAgo(lastWh.created_at)}
                  </li>` : ''}
              </ul>
            </div>
          </div>

          <div class="soc-panel" style="min-height:160px">
            <div class="soc-panel-head"><h4>Health dos nós</h4></div>
            <div class="soc-panel-body">
              <div class="soc-health-grid">
                <div class="soc-health-card">
                  <h5>VM112 Portal</h5>
                  <dl>
                    <dt>HTTP</dt><dd>${vm112.http_status ?? '—'}</dd>
                    <dt>Service</dt><dd>${esc(vm112.vm112?.service || vm112.error || '—')}</dd>
                    <dt>API</dt><dd>${onboard.vm112_api?.reachable ? 'OK' : 'offline'}</dd>
                  </dl>
                </div>
                <div class="soc-health-card">
                  <h5>VM122 Desk</h5>
                  <dl>
                    <dt>Integração</dt><dd>${esc(intStatus)}</dd>
                    <dt>Gap</dt><dd>${gapMin != null ? `${gapMin} min` : '—'}</dd>
                    <dt>Webhook</dt><dd>${esc(lastWh.event || '—')}</dd>
                  </dl>
                </div>
                <div class="soc-health-card">
                  <h5>VM104 Wazuh</h5>
                  <dl>
                    <dt>API</dt><dd>${wazuh.http_status ?? '—'}</dd>
                    <dt>Regra</dt><dd>level ≥ 10</dd>
                    <dt>Status</dt><dd>${wazuhOk ? 'online' : 'check'}</dd>
                  </dl>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>`;

    document.getElementById('soc-window-select')?.addEventListener('change', (e) => {
      state.socWindow = e.target.value;
      renderInfra2();
    });
    document.getElementById('soc-btn-refresh')?.addEventListener('click', () => renderInfra2());
    document.getElementById('soc-btn-test')?.addEventListener('click', () => runWebhookIntegrationTest('infra2'));
    el.querySelectorAll('[data-soc-session]').forEach((btn) => {
      btn.addEventListener('click', () => {
        state.selectedSessionId = btn.dataset.socSession;
        const tid = btn.dataset.socTicket;
        state.selectedTicketId = tid ? Number(tid) : null;
        setView('tickets');
      });
    });
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro SOC: ${esc(e.message)}</p>`;
  }
}

async function renderInfra() {
  const el = document.getElementById('infra-content');
  el.innerHTML = '<p class="loading">Verificando…</p>';
  try {
    const [vm112, wazuh, integrations, health] = await Promise.all([
      api('/v1/infra/vm112/status'),
      api('/v1/infra/wazuh/status'),
      api('/v1/integrations'),
      api('/v1/integrations/health'),
    ]);
    const onboard = health.vm112_onboard || {};
    const last = onboard.last_webhook;
    const gap = onboard.gap_minutes != null ? `${Math.round(onboard.gap_minutes)} min` : '—';
    const statusCls = health.status === 'ok' ? 'ok' : health.status === 'critical' ? 'escalated' : 'assisting';
    const alerts = (health.alerts || []).map((a) =>
      `<li class="badge ${a.level === 'critical' ? 'escalated' : 'assisting'}">${esc(a.message)}</li>`
    ).join('') || '<li class="muted">Nenhum alerta</li>';
    el.innerHTML = `
      <div class="card soc-panel">
        <div style="display:flex;justify-content:space-between;align-items:center;gap:0.75rem;flex-wrap:wrap">
          <h3>SOC — Integração VM112</h3>
          <span class="badge ${statusCls}">${esc(health.status || '—')}</span>
        </div>
        <dl class="kv">
          <dt>Último webhook</dt><dd>${last ? esc(last.event) : '—'}</dd>
          <dt>Domínio</dt><dd>${last?.domain ? esc(last.domain) : '—'}</dd>
          <dt>Há quanto tempo</dt><dd>${gap}</dd>
          <dt>VM112 API</dt><dd>${onboard.vm112_api?.reachable ? 'OK' : esc(onboard.vm112_api?.error || 'offline')}</dd>
        </dl>
        <ul class="soc-alerts" style="list-style:none;padding:0;margin:0.5rem 0;display:flex;flex-direction:column;gap:0.35rem">${alerts}</ul>
        <div class="actions">
          <button type="button" class="btn secondary" id="btn-test-webhook">Testar webhook</button>
          <button type="button" class="btn secondary" id="btn-refresh-health">Atualizar</button>
        </div>
        <p class="health-card-hint">Alerta se gap &gt; ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>
      </div>
      <div class="card">
        <h3>OpenPanel API — Re-engenharia Ligbox</h3>
        <p class="health-card-hint">Spec 028 · simule contas/domínios · duração ~1–3 min por conta.</p>
        <div class="openpanel-test-form" style="display:grid;gap:0.65rem;max-width:32rem;margin:0.75rem 0">
          <label><strong>Conta 1</strong> (username OpenPanel, ex: <code>meuteste1</code>)</label>
          <input type="text" id="op-test-user1" placeholder="meuteste1" pattern="[a-z][a-z0-9]{2,15}" autocomplete="off" style="padding:0.45rem 0.6rem;border-radius:6px;border:1px solid var(--border,#444);background:var(--bg-input,#1a1a22);color:inherit;width:100%"/>
          <label><strong>Domínio 1</strong> (ex: <code>meuteste1.ligbox.com.br</code>)</label>
          <input type="text" id="op-test-domain1" placeholder="meuteste1.ligbox.com.br" autocomplete="off" style="padding:0.45rem 0.6rem;border-radius:6px;border:1px solid var(--border,#444);background:var(--bg-input,#1a1a22);color:inherit;width:100%"/>
          <label><strong>Conta 2</strong> (opcional — 2º domínio na plataforma)</label>
          <input type="text" id="op-test-user2" placeholder="meuteste2" autocomplete="off" style="padding:0.45rem 0.6rem;border-radius:6px;border:1px solid var(--border,#444);background:var(--bg-input,#1a1a22);color:inherit;width:100%"/>
          <label><strong>Domínio 2</strong></label>
          <input type="text" id="op-test-domain2" placeholder="meuteste2.ligbox.com.br" autocomplete="off" style="padding:0.45rem 0.6rem;border-radius:6px;border:1px solid var(--border,#444);background:var(--bg-input,#1a1a22);color:inherit;width:100%"/>
          <label><strong>Senha hosting</strong></label>
          <input type="text" id="op-test-password" value="LbOpenTest805353" autocomplete="off" style="padding:0.45rem 0.6rem;border-radius:6px;border:1px solid var(--border,#444);background:var(--bg-input,#1a1a22);color:inherit;width:100%"/>
          <label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="op-test-auto-names" checked/> Gerar nomes automaticamente se campos vazios</label>
          <label style="display:flex;align-items:center;gap:0.4rem"><input type="checkbox" id="op-test-cleanup" checked/> Apagar contas após teste</label>
        </div>
        <div class="actions">
          <button type="button" class="btn secondary" id="btn-test-openpanel-api">Executar simulação</button>
        </div>
      </div>
      <div class="card">
        <h3>VM112 — Portal Onboard</h3>
        <dl class="kv">
          <dt>HTTP</dt><dd>${vm112.http_status ?? '—'}</dd>
          <dt>Service</dt><dd>${esc(vm112.vm112?.service || vm112.error || '—')}</dd>
        </dl>
      </div>
      <div class="card">
        <h3>VM104 — Wazuh SOC</h3>
        <dl class="kv">
          <dt>API</dt><dd>${wazuh.http_status ?? '—'}</dd>
          <dt>Integração</dt><dd>webhook level ≥ 10 → VM122</dd>
        </dl>
      </div>
      <div class="card">
        <h3>Integrações ativas</h3>
        <pre class="raw">${esc(JSON.stringify(integrations, null, 2))}</pre>
      </div>`;
    document.getElementById('btn-refresh-health')?.addEventListener('click', () => renderInfra());
    document.getElementById('btn-test-webhook')?.addEventListener('click', () => runWebhookIntegrationTest('infra'));
    document.getElementById('btn-test-openpanel-api')?.addEventListener('click', () => runOpenPanelApiTest());
  } catch (e) {
    el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
  }
}

async function refresh(options = {}) {
  const { poll = false } = options;
  await loadHealth();
  if (poll && state.view === 'account') {
    return;
  }
  if (state.view === 'dashboard') await renderDashboard();
  if (state.view === 'email-migration' && window.DeskEmailMigration?.renderPage) await window.DeskEmailMigration.renderPage();
  if (state.view === 'overview') await renderOverview();
  if (state.view === 'overview-home') await renderOverviewHome({ poll });
  if (state.view === 'leads') await renderLeads();
  if (state.view === 'tickets') {
    if (poll && window.TicketsWorkspace?._pageReady) await TicketsWorkspace.softRefresh();
    else await renderTickets({ poll: false });
  }
  if (state.view === 'events') await renderEvents();
  if (state.view === 'tenants') await renderTenants();
  if (state.view === 'infra' && !state.openPanelTestRunning) await renderInfra();
  if (state.view === 'infra2') await renderInfra2();
  if (state.view === 'messages') await renderMessages();
  if (state.view === 'admin') await renderAdmin();
  if (state.view === 'modules') await renderModules();
  if (state.view === 'account') await renderAccount();
}

document.querySelectorAll('.nav button').forEach((btn) => {
  btn.addEventListener('click', () => setView(btn.dataset.view));
});

document.querySelectorAll('.filter-btn[data-filter]').forEach((btn) => {
  btn.addEventListener('click', () => {
    state.ticketFilter = btn.dataset.filter;
    document.querySelectorAll('.filter-btn[data-filter]').forEach((b) => b.classList.toggle('active', b === btn));
    renderTickets();
  });
});

document.querySelectorAll('.filter-btn[data-source]').forEach((btn) => {
  btn.addEventListener('click', () => {
    const kind = btn.dataset.kind || 'ticket';
    if (kind === 'event') {
      state.eventSourceFilter = btn.dataset.source;
      document.querySelectorAll('.filter-btn[data-kind="event"]').forEach((b) => b.classList.toggle('active', b === btn));
      renderEvents();
    } else {
      state.sourceFilter = btn.dataset.source;
      document.querySelectorAll('.filter-btn[data-kind="ticket"]').forEach((b) => b.classList.toggle('active', b === btn));
      renderTickets();
    }
  });
});

document.querySelectorAll('[data-events-tab]').forEach((btn) => {
  btn.addEventListener('click', () => {
    state.eventsTab = btn.dataset.eventsTab || 'webhooks';
    document.querySelectorAll('[data-events-tab]').forEach((b) => b.classList.toggle('active', b === btn));
    renderEvents();
  });
});

document.querySelectorAll('[data-close-purge-history-modal]').forEach((el) => {
  el.addEventListener('click', closePurgeHistoryModal);
});

document.getElementById('btn-refresh')?.addEventListener('click', () => {
  if (state.view === 'account') {
    state.accountLoaded = false;
  }
  refresh();
});

(async function boot() {
  const dash = document.getElementById('dashboard-content');
  try {
    if (!getToken()) {
      window.location.replace('/login.html');
      return;
    }
    setupSidebarUser();
    await DeskModules.load();
    applyRoleNav();
    DeskModules.applyVisibility();
    bindOverviewModal();
    bindTeamDrawerClose();
    bindSocTestModal();
    setView('dashboard');

    ensureValidSession().then((valid) => {
      if (!valid) window.location.replace('/login.html');
      else setupSidebarUser();
    });

    reschedulePoll();
  } catch (err) {
    console.error('boot failed', err);
    if (dash) {
      dash.innerHTML = `<p class="loading">Erro ao iniciar (${esc(err.message)}). <a href="/login.html">Voltar ao login</a></p>`;
    }
  }
})();
