diff --git a/projects/ops-desk/frontend/assets/app.js b/projects/ops-desk/frontend/assets/app.js index 9ed948a..5e0d8f1 100644 --- a/projects/ops-desk/frontend/assets/app.js +++ b/projects/ops-desk/frontend/assets/app.js @@ -277,6 +277,7 @@ function setView(name) { } let pollTimer = null; +let socRenderInFlight = false; function reschedulePoll() { if (pollTimer) clearInterval(pollTimer); const ms = state.view === 'infra2' ? 15000 : 30000; @@ -3529,7 +3530,7 @@ async function runWebhookIntegrationTest(refreshView) { try { const r = await api('/v1/integrations/onboard/test', { method: 'POST' }); showSocWebhookTestResult(r); - if (refreshView === 'infra2') await renderInfra2(); + if (refreshView === 'infra2') await renderInfra2({ force: true }); else if (refreshView === 'infra') await renderInfra(); } catch (ex) { showSocWebhookTestError(ex); @@ -3541,14 +3542,64 @@ async function runWebhookIntegrationTest(refreshView) { } } -async function renderInfra2() { +function bindSocEventsOnce() { + if (state._socEventsBound) return; + const host = document.getElementById('infra2-content'); + if (!host) return; + host.addEventListener('change', (e) => { + if (e.target.id !== 'soc-window-select') return; + state.socWindow = e.target.value; + renderInfra2({ force: true }); + }); + host.addEventListener('click', (e) => { + if (e.target.closest('#soc-btn-refresh')) { + e.preventDefault(); + renderInfra2({ force: true }); + return; + } + if (e.target.closest('#soc-btn-test')) { + e.preventDefault(); + runWebhookIntegrationTest('infra2'); + return; + } + const card = e.target.closest('[data-soc-session]'); + if (card) { + state.selectedSessionId = card.dataset.socSession; + const tid = card.dataset.socTicket; + state.selectedTicketId = tid ? Number(tid) : null; + setView('tickets'); + } + }); + state._socEventsBound = true; +} + +async function renderInfra2(options = {}) { + const { poll = false, force = false } = options; const el = document.getElementById('infra2-content'); if (!el) return; if (window.DeskModules?.loaded && !DeskModules.isEnabled('infra2-soc')) { el.innerHTML = '

Módulo Infra 2 SOC desativado. Active em Módulos.

'; return; } - el.innerHTML = '

Carregando SOC…

'; + + 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([ @@ -3610,8 +3661,9 @@ async function renderInfra2() { } const newestId = feedEvents[0]?.id; - const flashNew = state.socLastEventId && newestId && newestId > state.socLastEventId; - state.socLastEventId = newestId || state.socLastEventId; + 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' @@ -3834,22 +3886,12 @@ async function renderInfra2() { `; - 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'); - }); - }); + bindSocEventsOnce(); } catch (e) { el.innerHTML = `

Erro SOC: ${esc(e.message)}

`; + } finally { + socRenderInFlight = false; + document.getElementById('soc-console-root')?.classList.remove('soc-console--refreshing'); } } @@ -4254,7 +4296,7 @@ async function refresh(options = {}) { if (state.view === 'events') await renderEvents(); if (state.view === 'tenants') await renderTenants(); if (state.view === 'infra') await renderInfra(); - if (state.view === 'infra2') await renderInfra2(); + if (state.view === 'infra2') await renderInfra2({ poll }); if (state.view === 'agentic-ops' && window.renderAgenticOps) await window.renderAgenticOps(); if (state.view === 'messages') await renderMessages(); if (state.view === 'admin') await renderAdmin(); diff --git a/projects/ops-desk/frontend/assets/styles.css b/projects/ops-desk/frontend/assets/styles.css index 252c94a..d7bb23c 100644 --- a/projects/ops-desk/frontend/assets/styles.css +++ b/projects/ops-desk/frontend/assets/styles.css @@ -2535,6 +2535,12 @@ button.health-card { box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.04); } +.soc-console--refreshing { + opacity: 0.94; + transition: opacity 0.25s ease; + pointer-events: none; +} + .soc-header { display: flex; flex-wrap: wrap; diff --git a/projects/ops-desk/frontend/index.html b/projects/ops-desk/frontend/index.html index 75afb98..5b7fbe0 100644 --- a/projects/ops-desk/frontend/index.html +++ b/projects/ops-desk/frontend/index.html @@ -4,7 +4,7 @@ Ligbox Ops — Support Desk - + @@ -476,6 +476,6 @@ - +