const API = '/api';
const CONSOLE_BASE_URL = 'https://console.ligbox.com.br';
function consoleHomeUrl(base) {
return `${String(base || CONSOLE_BASE_URL).replace(/\/$/, '')}/`;
}
/** SSO/handoff Console → Desk (?desk_handoff= ou cookie). */
async function consumeSsoFromUrl() {
const params = new URLSearchParams(window.location.search);
const handoff = params.get('desk_handoff');
if (!handoff) return false;
try {
const res = await fetchWithTimeout('/api/v1/auth/console-handoff/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ handoff_token: handoff }),
});
const data = await res.json().catch(() => ({}));
if (res.ok && data.access_token) {
setSession(data.access_token, {
username: data.username,
role: data.role,
display_name: data.display_name,
}, { maxAgeSec: data.expires_in });
params.delete('desk_handoff');
const qs = params.toString();
window.history.replaceState({}, '', `${window.location.pathname}${qs ? `?${qs}` : ''}`);
return true;
}
} catch (err) {
console.warn('consumeSsoFromUrl:', err?.name || err);
}
return false;
}
async function setupConsoleReturnLink() {
const link = document.getElementById('nav-console-return');
if (!link) return;
let consoleUrl = CONSOLE_BASE_URL;
try {
const f = await fetch(`${API}/v1/desk/features`);
if (f.ok) {
const data = await f.json();
if (data.console_url) consoleUrl = data.console_url;
}
} catch {
/* fallback */
}
link.href = consoleHomeUrl(consoleUrl);
link.addEventListener('click', (e) => {
e.preventDefault();
window.location.href = consoleHomeUrl(consoleUrl);
});
}
function setupDeskNavBreadcrumb() {
const params = new URLSearchParams(window.location.search);
if (params.get('from') !== 'console' && !params.get('desk_handoff')) return;
const toolbar = document.getElementById('page-toolbar');
if (!toolbar || document.getElementById('desk-console-crumb')) return;
const crumb = document.createElement('a');
crumb.id = 'desk-console-crumb';
crumb.className = 'btn btn-ghost btn-sm desk-console-crumb';
crumb.textContent = '← Console';
crumb.href = '#';
crumb.addEventListener('click', (e) => {
e.preventDefault();
const link = document.getElementById('nav-console-return');
if (link?.href) window.location.href = link.href;
});
toolbar.insertBefore(crumb, toolbar.firstChild);
}
async function maybeConsoleCutover() {
const params = new URLSearchParams(window.location.search);
if (params.get('desk') === '1') return false;
if (/login|register|activate|matriz/.test(window.location.pathname)) return false;
try {
const res = await fetch(`${API}/v1/desk/features`);
if (!res.ok) return false;
const f = await res.json();
if (f.console_cutover && f.console_url) {
window.location.replace(f.console_url);
return true;
}
} catch (err) {
console.warn('console cutover', err);
}
return false;
}
function resolveBootView() {
const params = new URLSearchParams(window.location.search);
const requested = params.get('view');
const fallback = window.DeskModules?.isViewEnabled?.('agentic-ops') ? 'agentic-ops' : resolveDefaultView();
if (!requested || requested === 'tenants') {
if (requested === 'tenants') {
window.open(`${CONSOLE_BASE_URL}/#infra-nodes`, '_blank', 'noopener');
}
return fallback;
}
if (window.DeskModules?.isViewEnabled?.(requested)) return requested;
return fallback;
}
async function api(path, options = {}) {
const res = await fetchWithTimeout(`${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, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function sessionHashHtml(sessionId, { full = true } = {}) {
const id = (sessionId || '').trim();
if (!id) return '';
const shown = full ? id : `${id.slice(0, 8)}…${id.slice(-4)}`;
return `${esc(shown)}`;
}
const DEFAULT_VIEW = 'agentic-ops';
let state = {
view: DEFAULT_VIEW,
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,
adminPanel: 'team',
features: { access_matrix_ui: false },
socWindow: '24h',
socLastEventId: null,
};
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'),
'agentic-ops': document.getElementById('view-agentic-ops'),
messages: document.getElementById('view-messages'),
admin: document.getElementById('view-admin'),
'access-matrix': document.getElementById('view-access-matrix'),
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 assistStatusLabel(status) {
return {
observing: 'observando',
escalated: 'escalado',
assisting: 'assistindo',
}[status] || status || 'observando';
}
function assistBadge(status) {
if (!status || status === 'observing') {
return 'observando';
}
const cls = status === 'assisting' ? 'assisting' : status === 'escalated' ? 'escalated' : 'open';
return `${esc(assistStatusLabel(status))}`;
}
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 = `
${esc(user.display_name || user.username)}
${esc(user.username)} · ${esc(label)}`;
}
if (header) {
header.hidden = false;
header.innerHTML = `${esc(user.display_name || user.username)}${esc(label)}`;
}
if (logoutBtn) {
logoutBtn.hidden = false;
logoutBtn.onclick = logout;
}
}
function renderAdminTabs() {
if (!state.features?.access_matrix_ui || getUser()?.role !== 'super_admin') return '';
const teamActive = state.adminPanel !== 'matrix';
return `
`;
}
function adminTabsInContent() {
return document.querySelector('.shell--v2') ? '' : renderAdminTabs();
}
function bindAdminTabs() {
document.querySelectorAll('[data-admin-tab]').forEach((btn) => {
btn.onclick = () => {
state.adminPanel = btn.dataset.adminTab === 'matrix' ? 'matrix' : 'team';
renderAdmin();
window.DeskTopnav?.updateContextSidebar?.('admin');
};
});
}
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');
}
}
async function loadDeskFeatures() {
try {
const data = await api('/v1/config/features');
state.features = { ...state.features, ...data };
} catch {
state.features = { access_matrix_ui: false };
}
}
function applyAccessMatrixNav() {
const nav = document.getElementById('nav-access-matrix');
if (!nav) return;
const user = getUser();
const show = !!state.features?.access_matrix_ui && user?.role === 'super_admin';
if (show) nav.removeAttribute('hidden');
else nav.setAttribute('hidden', '');
}
function resolveDefaultView() {
if (window.DeskModules?.loaded && DeskModules.isViewEnabled(DEFAULT_VIEW)) return DEFAULT_VIEW;
return 'dashboard';
}
function setView(name) {
if (name === 'access-matrix' && !state.features?.access_matrix_ui) {
name = resolveDefaultView();
} else if (window.DeskModules?.loaded && !DeskModules.isViewEnabled(name)) {
name = resolveDefaultView();
}
if (state.view === 'account' && name !== 'account') {
state.accountLoaded = false;
}
state.view = name;
if (name === 'admin') state.adminPanel = 'team';
const titles = {
dashboard: 'Dashboard',
overview: 'Audit Overview',
'overview-home': 'Serviços IaaS',
tickets: 'Tickets',
events: 'Eventos webhook',
tenants: 'Tenants',
infra: 'INFRA CODE',
infra2: 'SOC — Infra 2',
'agentic-ops': 'Agentic Ops',
messages: 'Mensagens — pedidos de cadastro',
admin: 'Administradores',
'access-matrix': 'Matriz de Acessos',
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': 'Orquestração MOSP · Infra as Code',
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: 'Infrastructure as Code — stack VMs 112, 114, 122, 123, 130',
infra2: 'Centro de operações — monitoramento visual VM112 → VM122 em tempo quase real',
'agentic-ops': 'Vigilância 24/7, findings, advisor IA e copiloto ops (Spec 029)',
messages: 'Operações Ligbox — onboarding, tickets e monitoramento',
admin: 'Operações Ligbox — onboarding, tickets e monitoramento',
'access-matrix': 'Preview read-only — Spec 027 · funções × VM112/122/123 (estilo Odoo groups)',
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, [data-desk-nav]').forEach((b) => {
b.classList.toggle('active', b.dataset.view === name);
});
document.querySelectorAll('[data-desk-nav]').forEach((b) => {
b.classList.toggle('active', b.dataset.view === name);
if (b.dataset.view === name) b.setAttribute('aria-current', 'page');
else b.removeAttribute('aria-current');
});
window.DeskTopnav?.updateTopnavActive?.(name);
window.DeskTopnav?.updateContextSidebar?.(name);
Object.entries(views).forEach(([k, el]) => el?.classList.toggle('active', k === name));
reschedulePoll();
refresh();
}
/** Navegação cross-módulo (Spec 018 — cards Escopo OPS). */
window.DeskNavigate = {
go(view, opts = {}) {
if (opts.eventsTab) state.eventsTab = opts.eventsTab;
if (opts.ticketFilter) state.ticketFilter = opts.ticketFilter;
setView(view);
if (view === 'events' && opts.eventsTab) {
document.querySelectorAll('[data-events-tab]').forEach((b) => {
b.classList.toggle('active', b.dataset.eventsTab === opts.eventsTab);
});
renderEvents();
}
if (view === 'tickets' && opts.ticketFilter) {
document.querySelectorAll('.filter-btn[data-filter]').forEach((b) => {
b.classList.toggle('active', b.dataset.filter === opts.ticketFilter);
});
renderTickets();
}
},
};
let pollTimer = null;
let socRenderInFlight = false;
function reschedulePoll() {
if (pollTimer) clearInterval(pollTimer);
let ms = 30000;
if (state.view === 'infra2') ms = 15000;
// VM112 /admin/domains ~10–15s — poll menos agressivo (Spec 018 Serviços IaaS)
if (state.view === 'overview-home') ms = 90000;
pollTimer = setInterval(() => refresh({ poll: true }), ms);
}
async function loadHealth() {
const el = document.getElementById('global-health');
if (!el) return null;
try {
await api('/health');
const html = ' API online';
if (!el.classList.contains('ok') || el.innerHTML !== html) {
el.className = 'status-pill ok';
el.innerHTML = html;
}
return true;
} catch {
const html = ' API offline';
if (!el.classList.contains('err') || el.innerHTML !== html) {
el.className = 'status-pill err';
el.innerHTML = html;
}
return null;
}
}
/** Evita flash "Carregando…" quando o poll refresca uma vista já montada */
function viewHasContent(el, selector) {
return Boolean(el?.querySelector(selector));
}
async function renderDashboard(options = {}) {
const { poll = false } = options;
const box = document.getElementById('dashboard-content');
if (!viewHasContent(box, '.dashboard-top')) {
box.innerHTML = '
Carregando…
'; } else if (poll) { box.classList.add('view--refreshing'); } 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 ` `; }).join(''); box.innerHTML = `Sem sessões recentes
'}Nenhum lead — sessões stale viram lead após ${summary.onboard_stale_hours ?? 24}h
'}Sem tickets
'}Erro: ${esc(e.message)}
`; } finally { box?.classList.remove('view--refreshing'); } } function sourceBadge(src) { if (src === 'desk-registration') return 'desk'; if (src === 'wazuh') return 'wazuh'; if (src === 'vm112-onboard') return 'onboard'; return src ? `${esc(src)}` : ''; } 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 `L${n}`; } 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 `| Fase | Registado | Δ fase | Acumulado |
|---|
${esc(a.action)} · ${esc(a.actor)} · ${fmtDate(a.created_at)}| # | Técnico | Assumidos | Escalados | Acções | Score |
|---|---|---|---|---|---|
| ${i + 1} | ${esc(r.username)} | ${r.assumidos} | ${r.escalados} | ${r.acoes} | ${r.score} |
Carregando detalhes de ${esc(domain)}…
`; 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}${window.DnsViewer?.renderPanel ? window.DnsViewer.renderPanel(dns, { compact: true }) : htmlCloudflareDnsCardInline(dns)}`; } function htmlCloudflareDnsCardInline(dns) { if (!dns) { return 'Dados DNS indisponíveis.
'; } if (dns.error && !dns.records?.length) { return `${esc(dns.error)}
${dns.email_service ? '' : ''}`; } const rows = (dns.records || []).map((r) => `${esc(r.name)}| Função | Nome | Tipo | Conteúdo |
|---|---|---|---|
| Sem registos para este domínio. | |||
Dados DNS indisponíveis.
${esc(r.name)}| Função | Nome | Tipo | Conteúdo | Estado |
|---|---|---|---|---|
| Sem registos DNS para este domínio na zona Cloudflare. | ||||
${esc(i.domain)} · ${esc(i.check_id)} — ${esc(i.message || i.status)}| Agente | IP | Alertas | Máx | Último |
|---|
Nenhum agente com alertas registados.
'}| Nível | Agente | Descrição | Src IP | Agent IP | Hora |
|---|
Sem alertas.
'}Sem dados
'; 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 `Sem dados
'; const max = Math.max(...items.map((i) => i.value), 1); return items.map((item) => ` `).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) => `${esc((ev.client_ip || '—'))} · ${esc(wizardSecurityEventLabel(ev.event_type))} — ${ev.session_id ? sessionHashHtml(ev.session_id, { full: false }) : 'sem sessão'}${esc(ev.client_ip || '—')}${esc(ev.client_ip || '—')} · ${esc(wizardSecurityEventLabel(ev.event_type))} — ${ev.session_id ? `${esc(ev.session_id.slice(0, 14))}…` : 'sem sessão'}${esc(ip)}
${n} evt
Nenhum IP registado
'}| Ameaça | Nível | Sessão | IP | Hora |
|---|---|---|---|---|
| Sem ameaças nas últimas 24h | ||||
Este painel cobre apenas o comportamento de quem acede ao sistema — visitantes, clientes no portal e tentativas de abuso em formulários públicos.
${standalone ? 'Domínios, DNS e Carbonio estão no card VM112 Ligbox Onboard — área separada.' : '≠ Saúde do wizard VM112 (domínios, e-mail, certificados) — ver secção Onboard abaixo.'}
${esc(i.check_id)} — ${esc(i.message || i.status)}Nenhum domínio auditado neste tenant.
'}Carregando detalhes…
'; 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) => `Sem eventos webhook para este domínio.
'; const ips = (d.client_ips || []).filter(Boolean); body.innerHTML = `${esc(d.client_ip || (ips[0] || '—'))}| Check | Status | Mensagem | Verificado |
|---|---|---|---|
| Sem checks | |||
Carregando detalhes…
'; 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 = `Erro: ${esc(e.message)}
`; 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 = 'Carregando segurança de acesso…
'; 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 = `Erro ao carregar segurança de acesso: ${esc(e.message)}
`; body.querySelector('[data-retry-user-access]')?.addEventListener('click', () => openUserAccessModal()); } } async function renderOverview(options = {}) { const { poll = false } = options; const el = document.getElementById('overview-content'); if (!viewHasContent(el, '.health-grid')) { el.innerHTML = 'Carregando overview…
'; } else if (poll) { el.classList.add('view--refreshing'); } 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) => `${esc(i.domain)} · ${esc(i.check_id)} — ${esc(i.message || i.status)}Nenhum tenant auditado. Complete onboarding ou POST /audit/cycle.
'; 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 = `Erro: ${esc(e.message)}
`; } finally { el?.classList.remove('view--refreshing'); } } 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 ``; } 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 = 'Módulo Serviços não carregado.
'; } async function renderLeads(options = {}) { const { poll = false } = options; const el = document.getElementById('leads-content'); if (!canReadLeads()) { el.innerHTML = 'Sem permissão para ver leads
'; return; } if (!viewHasContent(el, '.lead-grid')) { el.innerHTML = 'Carregando leads…
'; } else if (poll) { el.classList.add('view--refreshing'); } try { const data = await api('/v1/crm/leads'); const leads = data.leads || []; el.innerHTML = `Nenhum lead no momento
'}Erro: ${esc(e.message)}
`; } finally { el?.classList.remove('view--refreshing'); } } 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 = 'Carregando tickets…
'; 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('') : 'Nenhum ticket neste filtro
'; 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 = 'Selecione um ticket ou sessão do funil
Erro: ${esc(e.message)}
`; } } async function renderSessionDetail() { const detailEl = document.getElementById('ticket-detail'); const sessionId = state.selectedSessionId; if (!sessionId) return; detailEl.innerHTML = 'Carregando sessão…
${esc(meta.session_id)}Erro: ${esc(e.message)}
Carregando…
${esc(t.session_id || '—')}${esc(JSON.stringify(t.payload, null, 2))}
Erro: ${esc(e.message)}
Carregando eventos…
'; } else if (poll) { el.classList.add('view--refreshing'); } 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 || {}; const domain = p.domain || e.domain || '—'; const ref = (p.session_id || p.job_id || '').slice(0, 16); return `${esc(ref || '—')}| ID | Origem | Evento | Agente/Domínio | Ref | Data |
|---|---|---|---|---|---|
| Sem eventos | |||||
Erro: ${esc(e.message)}
`; } finally { el?.classList.remove('view--refreshing'); } } 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 `${esc(label)}`; } function carbonioReleaseGuideHtml() { return `zmprov da) — domínio, DNS e portal mantêm-se.Módulo Bloqueios Carbonio desativado.
'; return; } el.innerHTML = 'Carregando bloqueios Carbonio…
'; 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('') : ''; const resolvedRows = resolvedBlocks.map((b) => `${esc(b.email)}| ID | Domínio | Resolvido por | Quando | Ticket | |
|---|---|---|---|---|---|
| Nenhum | |||||
Erro: ${esc(e.message)}
`; } } async function renderSecurityEvents() { syncEventsToolbar(); const el = document.getElementById('events-content'); if (!window.DeskModules?.isEnabled('wizard-security')) { el.innerHTML = 'Módulo Segurança Wizard desativado.
'; return; } el.innerHTML = 'Carregando eventos de segurança…
'; 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) => `${esc(ev.client_ip || '—')}| Nível | Evento | Sessão | Domínio | IP | Detalhe | Quando |
|---|---|---|---|---|---|---|
| Nenhum evento de segurança | ||||||
Erro: ${esc(e.message)}
`; } } 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 `${esc(label)}`; } 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 `${esc(JSON.stringify(p, null, 2))}`;
}
function openEventAuditorModal(eventId) {
const modal = document.getElementById('event-auditor-modal');
const title = document.getElementById('event-auditor-modal-title');
const sub = document.getElementById('event-auditor-modal-sub');
const body = document.getElementById('event-auditor-modal-body');
if (!modal || !body) return;
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
title.textContent = 'Auditor de Eventos';
sub.textContent = `webhook · ${eventId}`;
body.innerHTML = 'Carregando…
'; api(`/v1/webhooks/events/${encodeURIComponent(eventId)}`) .then(async (data) => { const ev = data.event || {}; sub.textContent = `${esc(ev.source || 'webhook')} · ${ev.id}`; if (ev.event_type === 'domain.purged') { body.innerHTML = renderDomainPurgedAuditor(ev); body.querySelector('[data-open-purge-job]')?.addEventListener('click', (btn) => { closeEventAuditorModal(); openPurgeHistoryModal(btn.currentTarget.dataset.openPurgeJob); }); return; } const p = ev.payload || {}; const sid = (p.session_id || ev.session_id || '').trim(); let timelineBlock = ''; if (sid && ev.source === 'vm112-onboard') { try { const tl = await api(`/v1/onboard/sessions/${encodeURIComponent(sid)}/timeline`); const events = tl.events || tl.timeline || []; if (events.length) { timelineBlock = `${esc(sid || '—')}${esc(JSON.stringify(p, null, 2))}`;
})
.catch((e) => {
body.innerHTML = `Erro: ${esc(e.message)}
`; }); document.querySelectorAll('[data-close-event-auditor-modal]').forEach((el) => { el.onclick = closeEventAuditorModal; }); } 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 = 'Carregando…
'; 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${esc(job.id)}`;
const desk = job.desk || {};
const vm112 = job.vm112 || {};
const { rows: deskRows, total: deskTotal } = purgeDeskRowsHtml(desk);
const vm112Steps = Array.isArray(vm112.steps) ? vm112.steps : [];
const timeline = (job.timeline || []).length ? job.timeline : vm112Steps;
const recoverBtn = job.status === 'error'
? ``
: '';
body.innerHTML = `
| Nenhum registo Desk removido | |
| Total | ${deskTotal} |
${vm112RemovedSummary(vm112)}
${job.elapsed_vm112 ? `` : ''} ${job.error ? `${esc(job.error)}
` : ''} ${recoverBtn}Sem passos registados
'}Erro: ${esc(e.message)}
`; }); document.querySelectorAll('[data-close-purge-history-modal]').forEach((el) => { el.onclick = closePurgeHistoryModal; }); } async function renderPurgeHistory() { syncEventsToolbar(); const el = document.getElementById('events-content'); el.innerHTML = 'Carregando histórico de purges…
'; try { const data = await api('/v1/vm112/purge/jobs?limit=200'); const rows = (data.jobs || []).map((j) => `${esc(j.id)}| Job | Domínio | Status | Usuário | Desk removido | Quando | VM112 |
|---|---|---|---|---|---|---|
| Nenhum purge registado | ||||||
Erro: ${esc(e.message)}
`; } } async function renderTenants() { const el = document.getElementById('tenants-content'); el.innerHTML = 'Carregando…
'; try { const data = await api('/v1/tenants'); el.innerHTML = `| ID | Nome | IP | Papel | Desde |
|---|---|---|---|---|
| ${t.id} | ${esc(t.name)} | ${esc(t.ip)} |
${esc(t.role)} | ${fmtDate(t.created_at)} |
Erro: ${esc(e.message)}
`; } } 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 `${esc(roleLabel(role))}`; } function mfaBadgeHtml(user) { if (user.totp_enabled) { const backups = Number(user.backup_codes_remaining || 0); const hint = backups > 0 ? ` · ${backups} backup` : ''; return `2FA${hint}`; } return 'sem 2FA'; } 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) => ``) .join(''); return ``; }).join(''); } function roleSelectHtml(username, current, assignableOnly = true) { const options = assignableOnly && current !== 'super_admin' ? ASSIGNABLE_ROLE_OPTIONS : ROLE_OPTIONS; const opts = options.map((r) => `` ).join(''); return ``; } 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 = `${esc(user.display_name || user.username)}
Sem permissão
'; return; } const showMatrixTab = state.features?.access_matrix_ui && getUser()?.role === 'super_admin'; if (showMatrixTab && state.adminPanel === 'matrix') { el.innerHTML = `${adminTabsInContent()}Carregando matriz…
Carregando equipe…
'; 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) => `| Membro | Perfil | Segurança | Último login | Estado | |
|---|---|---|---|---|---|
| Nenhum membro encontrado | |||||
Erro: ${esc(e.message)}
`; } } async function renderModules() { const el = document.getElementById('modules-content'); if (!el) return; const user = getUser(); if (user?.role !== 'super_admin') { el.innerHTML = 'Apenas Super Admin pode gerenciar módulos.
'; return; } el.innerHTML = 'Carregando módulos…
'; try { await DeskModules.load(); const mods = DeskModules.list; el.innerHTML = `Erro: ${esc(e.message)}
`; } } const REG_ROLE_LABELS = ROLE_LABELS; async function renderMessages() { const el = document.getElementById('messages-content'); if (!canManageUsers()) { el.innerHTML = 'Sem permissão
'; return; } el.innerHTML = 'Carregando pedidos…
'; 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) => `Nenhum pedido pendente
'} ${history.length ? `| Estado | Perfil | Atualizado |
|---|
Erro: ${esc(e.message)}
`; } } async function renderAccount(force = false) { const el = document.getElementById('account-content'); if (state.accountLoaded && !force) { return; } const saved = force ? null : readAccountPwdForm(); el.innerHTML = 'Carregando…
'; try { const me = await api('/v1/auth/me'); const totpOn = Boolean(me.totp_enabled || me.mfa_enabled); el.innerHTML = `${esc(me.email || me.username)}Erro: ${esc(e.message)}
`; 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 ` `; } 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 `
Este teste simula um evento integration.test no endpoint
POST /api/v1/webhooks/onboard — 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.
Apenas perfis super_admin e admin podem executar o teste de webhook.
` : ''}Verifique se a API está online, se a sessão não expirou e se o usuário tem permissão.
Suite openpanel-multidomain-api-confirm — provisiona 2 contas temporárias
(2 domínios na plataforma), valida listagem e remove. Pode executar quantas vezes quiser.
Script CLI: scripts/test-openpanel-multidomain-api.sh
Perfis: super_admin, devops, developer.
' : ''}Módulo Infra 2 SOC desativado. Active em Módulos.
'; return; } const hasShell = !!document.getElementById('soc-console-root'); const soft = poll && hasShell && !force; if (socRenderInFlight) { if (poll) return; while (socRenderInFlight) { await new Promise((r) => setTimeout(r, 50)); } } socRenderInFlight = true; const root = document.getElementById('soc-console-root'); if (soft && root) { root.classList.add('soc-console--refreshing'); } else if (!hasShell || force) { el.innerHTML = 'Carregando SOC…
'; } 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 newestKey = newestId != null ? String(newestId) : null; const flashNew = newestKey != null && newestKey !== state.socLastEventId; if (newestKey != null) state.socLastEventId = newestKey; 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 = `| Evento | Domínio | Hora | |
|---|---|---|---|
| ${esc(SOC_EVENT_LABELS[ev.event_type] || ev.event_type)} | ${esc(p.domain || ev.domain || '—')} | ${relativeTimeAgo(ev.created_at)} |
Nenhum evento VM112 registrado
'}Sem sessões no período
'}Erro SOC: ${esc(e.message)}
`; } finally { socRenderInFlight = false; document.getElementById('soc-console-root')?.classList.remove('soc-console--refreshing'); } } function infraKvHtml(items) { return `${desc}
${esc(svc.url || '—')}`],
['Status', esc(svc.status || '—')],
['HTTP', svc.http_status != null ? String(svc.http_status) : '—'],
['Detalhe', esc(svc.detail || '—')],
])}
Alerta se gap > ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.
` ); document.getElementById('btn-test-webhook-modal')?.addEventListener('click', () => runWebhookIntegrationTest('infra')); document.getElementById('btn-refresh-health-modal')?.addEventListener('click', () => { closeInfraProcessModal(); renderInfra(); }); return; } if (procId === 'openpanel' || procId === 'vm123-openpanel-bridge') { const vm123Health = snap.vm123Health; const op = vm123Health?.openpanel || findStackService('vm123-openpanel-bridge')?.svc || {}; const opOk = Boolean(op.ok); const bridgeOk = Boolean(op.bridge); openInfraProcessModal( 'OpenPanel API — Re-engenharia Ligbox', 'Spec 028 · VM123 bridge :18087', `Multidomínio · conta temporária com cleanup automático.
${infraKvHtml([ ['OpenPanel', opOk ? 'OK' : esc(op.error || op.detail || 'offline')], ['Bridge API', bridgeOk ? 'OK' : 'offline'], ['Bridge URL', esc(op.bridge_url || op.url || '—')], ])}Suite openpanel-multidomain-api-confirm
A carregar…
${esc(JSON.stringify(integrations || {}, null, 2))}Verificando stack…
'; } else if (poll) { el.classList.add('view--refreshing'); } try { const [stack, integrations, health, vm123Health, purgeMeta] = await Promise.all([ api('/v1/infra/stack/status'), api('/v1/integrations').catch(() => null), api('/v1/integrations/health').catch(() => ({})), api('/v1/vm123/health').catch(() => null), api('/v1/infra/purge-auth-domains').catch(() => ({ domains: [], can_generate: false })), ]); state.infraSnapshot = { stack, integrations, health, vm123Health, purgeMeta }; const summary = stack.summary || {}; const okCls = summary.ok === summary.total ? 'ok' : summary.ok > 0 ? 'assisting' : 'escalated'; let sections = (stack.vms || []).map((vm) => { const cards = (vm.services || []).map((svc) => procCardHtml({ id: svc.id, icon: svc.icon || '⚙️', accent: svc.accent || 'teal', title: svc.title, spec: svc.spec && svc.spec !== '—' ? `Spec ${svc.spec}` : String(svc.kind || 'stack').toUpperCase(), desc: esc(svc.detail || svc.url || ''), statusLabel: svc.status || (svc.ok ? 'online' : 'down'), statusCls: stackServiceStatusCls(svc), actions: stackServiceActions(svc.id), }) ).join(''); return `Erro: ${esc(e.message)}
`; } finally { el?.classList.remove('view--refreshing'); } } async function renderPurgeAuthPanel(panel) { if (!panel) return; try { const meta = await api('/v1/infra/purge-auth-domains'); const domainChips = (meta.domains || []).map((d) => `${esc(d)}` ).join('') || ''; const canGen = meta.can_generate && typeof canManageUsers === 'function' && canManageUsers(); let codesHtml = ''; if (canGen) { const data = await api('/v1/infra/purge-auth-codes?limit=20'); const rows = (data.codes || []).map((c) => `${esc(c.domain)}Gere código com senha Root — use na conferência antes do purge em Serviços.
| Domínio | Nota | Expira | Por |
|---|---|---|---|
| Nenhum código activo | |||
Apenas super_admin (root) gera códigos. Peça o código ao root antes do purge em Serviços.
`; } panel.innerHTML = codesHtml; const form = panel.querySelector('#purge-auth-generate-form'); if (form) { form.addEventListener('submit', async (ev) => { ev.preventDefault(); const msg = panel.querySelector('#purge-auth-gen-msg'); const out = panel.querySelector('#purge-auth-generated'); const domain = panel.querySelector('#purge-auth-domain')?.value?.trim() || ''; const note = panel.querySelector('#purge-auth-note')?.value?.trim() || ''; const ttl = parseInt(panel.querySelector('#purge-auth-ttl')?.value || '24', 10); const rootPwd = panel.querySelector('#purge-auth-root-pwd')?.value || ''; if (!domain || !rootPwd) { if (msg) msg.textContent = 'Preencha domínio e senha Root.'; return; } if (msg) msg.textContent = 'A gerar…'; try { const res = await api('/v1/infra/purge-auth-codes', { method: 'POST', body: JSON.stringify({ domain, root_password: rootPwd, note, ttl_hours: ttl, }), }); if (msg) msg.textContent = 'Código gerado — copie agora (não será mostrado de novo).'; if (out) { out.classList.remove('hidden'); out.innerHTML = `Código: ${esc(res.code)}
Erro: ${esc(e.message)}
`; } } async function renderPurgeAuthInfraPanel() { await renderPurgeAuthPanel(document.getElementById('purge-auth-infra-panel')); } async function refresh(options = {}) { const { poll = false } = options; void loadHealth(); if (poll && state.view === 'account') { return; } // Agentic Ops tem poll próprio — evita duplo refresh e flash "Carregando…" if (poll && state.view === 'agentic-ops') { return; } const p = poll ? { poll: true } : {}; if (state.view === 'dashboard') await renderDashboard(p); if (state.view === 'email-migration' && window.DeskEmailMigration?.renderPage) await window.DeskEmailMigration.renderPage(); if (state.view === 'overview') await renderOverview(p); if (state.view === 'overview-home') await renderOverviewHome({ poll }); if (state.view === 'leads') await renderLeads(p); if (state.view === 'tickets') { if (poll && window.TicketsWorkspace?._pageReady) await TicketsWorkspace.softRefresh(); else await renderTickets({ poll: false }); } if (state.view === 'events') await renderEvents(p); if (state.view === 'tenants') await renderTenants(); if (state.view === 'infra') await renderInfra(p); if (state.view === 'infra2') await renderInfra2({ poll }); if (state.view === 'agentic-ops' && window.renderAgenticOps) await window.renderAgenticOps(p); if (state.view === 'messages') await renderMessages(); if (state.view === 'admin') await renderAdmin(); if (state.view === 'access-matrix' && window.renderAccessMatrix) await window.renderAccessMatrix(); 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('[data-desk-nav]').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.querySelectorAll('[data-close-event-auditor-modal]').forEach((el) => { el.addEventListener('click', closeEventAuditorModal); }); document.getElementById('btn-refresh')?.addEventListener('click', () => { if (state.view === 'account') { state.accountLoaded = false; } refresh(); }); document.addEventListener('click', (ev) => { const btn = ev.target.closest('.js-console-dns'); if (!btn || typeof openConsoleDnsViewer !== 'function') return; ev.preventDefault(); openConsoleDnsViewer(btn.dataset.domain || ''); }); document.getElementById('nav-console')?.addEventListener('click', (ev) => { if (typeof openConsolePath !== 'function') return; ev.preventDefault(); openConsolePath('/admin/dominio'); }); (async function boot() { const dash = document.getElementById('dashboard-content'); try { if (await maybeConsoleCutover()) return; if (!getToken()) { window.location.replace('/login.html'); return; } await consumeSsoFromUrl(); setupSidebarUser(); window.DeskTopnav?.initTopnavDropdowns?.(); await setupConsoleReturnLink(); setupDeskNavBreadcrumb(); await loadDeskFeatures(); await DeskModules.load(); applyRoleNav(); applyAccessMatrixNav(); DeskModules.applyVisibility(); bindOverviewModal(); bindInfraProcessModal(); bindTeamDrawerClose(); bindSocTestModal(); setView(resolveBootView()); ensureValidSession().then((valid) => { if (!valid) window.location.replace('/login.html'); else setupSidebarUser(); }); reschedulePoll(); } catch (err) { console.error('boot failed', err); if (dash) { dash.innerHTML = `Erro ao iniciar (${esc(err.message)}). Voltar ao login
`; } } })();