fix(desk): parar piscar da página SOC Infra 2 no poll 15s

Refresh suave sem ecrã "Carregando SOC", guard de render concorrente,
flash de linha só em evento novo e event delegation nos controlos.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ligbox Spec Hub 2026-06-25 13:11:33 +00:00
parent af58565999
commit 48b0dc2fe3
3 changed files with 70 additions and 22 deletions

View file

@ -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 = '<p class="loading">Módulo Infra 2 SOC desativado. Active em <strong>Módulos</strong>.</p>';
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 = '<p class="loading">Carregando SOC…</p>';
}
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() {
</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');
});
});
bindSocEventsOnce();
} catch (e) {
el.innerHTML = `<p class="loading">Erro SOC: ${esc(e.message)}</p>`;
} 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();

View file

@ -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;

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Ligbox Ops — Support Desk</title>
<link rel="stylesheet" href="/assets/styles.css?v=20260619tickets2"/>
<link rel="stylesheet" href="/assets/styles.css?v=20260625socfix"/>
<link rel="stylesheet" href="/assets/tickets-workspace.css?v=20260619tickets2"/>
<link rel="stylesheet" href="/assets/agentic-ops.css?v=20260620v10"/>
<link rel="stylesheet" href="/assets/access-matrix.css?v=20260620am7"/>
@ -476,6 +476,6 @@
<script src="/assets/servicos.js?v=20260620agentic"></script>
<script src="/assets/agentic-ops.js?v=20260620v10"></script>
<script src="/assets/access-matrix.js?v=20260620am7"></script>
<script src="/assets/app.js?v=20260620v4"></script>
<script src="/assets/app.js?v=20260625socfix"></script>
</body>
</html>