fix(desk): eliminar flash "Carregando…" no poll global Ligbox Ops

Refresh suave em todas as vistas, Agentic Ops com poll próprio e
patch incremental só quando dados mudam — sem apagar o ecrã a cada 30s.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ligbox Spec Hub 2026-06-25 15:36:35 +00:00
parent 48b0dc2fe3
commit 1640a93ae1
4 changed files with 123 additions and 37 deletions

View file

@ -273,6 +273,16 @@
return { list, openAgents, counts, filtered, bySev, sel }; return { list, openAgents, counts, filtered, bySev, sel };
} }
function incidentsFingerprint(data) {
return (data.incidents?.incidents || [])
.map((i) => `${i.id}:${i.status}:${i.severity}:${i.last_seen_at || ''}`)
.join('|');
}
function timelineFingerprint(timeline) {
return JSON.stringify(timeline?.ticks || []);
}
function agentStatus(id, openAgents) { function agentStatus(id, openAgents) {
if (openAgents.has(id)) return { cls: 'ao-squad-status--alert', label: 'alerta' }; if (openAgents.has(id)) return { cls: 'ao-squad-status--alert', label: 'alerta' };
if (state.selectedAgent === id) return { cls: 'ao-squad-status--active', label: 'activo' }; if (state.selectedAgent === id) return { cls: 'ao-squad-status--active', label: 'activo' };
@ -691,18 +701,36 @@
patchHtml(el.querySelector('#ao-pane-activity'), activityFeedHtml()); patchHtml(el.querySelector('#ao-pane-activity'), activityFeedHtml());
} }
function patchDashboard(el, data) { function patchDashboard(el, data, opts = {}) {
const { soft = false } = opts;
const view = deriveView(data); const view = deriveView(data);
const open = data.overview.incidents_open || {}; const open = data.overview.incidents_open || {};
const dot = el.querySelector('#ao-live-dot'); const dot = el.querySelector('#ao-live-dot');
if (dot) dot.className = `ao-live-dot ${liveDotClass(open)}`.trim(); if (dot) dot.className = `ao-live-dot ${liveDotClass(open)}`.trim();
patchHtml(el.querySelector('#ao-stat-chips'), chipsHtml(data.overview, open)); patchHtml(el.querySelector('#ao-stat-chips'), chipsHtml(data.overview, open));
const incFp = incidentsFingerprint(data);
const tickFp = timelineFingerprint(data.timeline);
const boardChanged = !soft || incFp !== state._aoIncidentsFp;
const timelineChanged = !soft || tickFp !== state._aoTimelineFp;
if (boardChanged) {
patchHtml(el.querySelector('#ao-subnav'), subnavHtml()); patchHtml(el.querySelector('#ao-subnav'), subnavHtml());
patchHtml(el.querySelector('#ao-squad-grid'), squadHtml(data, view)); patchHtml(el.querySelector('#ao-squad-grid'), squadHtml(data, view));
patchHtml(el.querySelector('#ao-kanban'), kanbanHtml(view)); patchHtml(el.querySelector('#ao-kanban'), kanbanHtml(view));
state._aoIncidentsFp = incFp;
}
if (timelineChanged) {
patchHtml(el.querySelector('#ao-timeline'), timelineHtml(data.timeline)); patchHtml(el.querySelector('#ao-timeline'), timelineHtml(data.timeline));
state._aoTimelineFp = tickFp;
}
if (!soft) {
patchHtml(el.querySelector('#ao-task-board'), taskBoardHtml(data)); patchHtml(el.querySelector('#ao-task-board'), taskBoardHtml(data));
patchSubViewPanes(el, data);
}
const boardLabel = el.querySelector('#ao-board-label'); const boardLabel = el.querySelector('#ao-board-label');
if (boardLabel) { if (boardLabel) {
@ -711,8 +739,9 @@
: 'Mission Board'; : 'Mission Board';
} }
patchSubViewPanes(el, data); if (!soft) {
updateSubViews(el); updateSubViews(el);
}
updateMainTabs(el); updateMainTabs(el);
updateCommsChrome(el, view.sel); updateCommsChrome(el, view.sel);
updateMobileTabs(el); updateMobileTabs(el);
@ -1215,7 +1244,7 @@
bindEventsOnce(el); bindEventsOnce(el);
} }
const view = patchDashboard(el, data); const view = patchDashboard(el, data, { soft });
if (saved) restoreInputs(el, saved); if (saved) restoreInputs(el, saved);
if (state.threadId && state.chatModalOpen) { if (state.threadId && state.chatModalOpen) {

View file

@ -286,21 +286,38 @@ function reschedulePoll() {
async function loadHealth() { async function loadHealth() {
const el = document.getElementById('global-health'); const el = document.getElementById('global-health');
if (!el) return null;
try { try {
const h = await api('/health'); await api('/health');
const html = '<span class="dot"></span> API online';
if (!el.classList.contains('ok') || el.innerHTML !== html) {
el.className = 'status-pill ok'; el.className = 'status-pill ok';
el.innerHTML = '<span class="dot"></span> API online'; el.innerHTML = html;
return h; }
return true;
} catch { } catch {
const html = '<span class="dot"></span> API offline';
if (!el.classList.contains('err') || el.innerHTML !== html) {
el.className = 'status-pill err'; el.className = 'status-pill err';
el.innerHTML = '<span class="dot"></span> API offline'; el.innerHTML = html;
}
return null; return null;
} }
} }
async function renderDashboard() { /** 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'); const box = document.getElementById('dashboard-content');
if (!viewHasContent(box, '.dashboard-top')) {
box.innerHTML = '<p class="loading">Carregando…</p>'; box.innerHTML = '<p class="loading">Carregando…</p>';
} else if (poll) {
box.classList.add('view--refreshing');
}
try { try {
const leadsPromise = canReadLeads() const leadsPromise = canReadLeads()
? api('/v1/crm/leads').catch(() => ({ leads: [], total: 0 })) ? api('/v1/crm/leads').catch(() => ({ leads: [], total: 0 }))
@ -425,10 +442,10 @@ async function renderDashboard() {
}); });
} catch (e) { } catch (e) {
box.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`; box.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
} finally {
box?.classList.remove('view--refreshing');
} }
} }
function sourceBadge(src) {
if (src === 'desk-registration') return '<span class="badge onboard">desk</span>'; if (src === 'desk-registration') return '<span class="badge onboard">desk</span>';
if (src === 'wazuh') return '<span class="badge wazuh">wazuh</span>'; if (src === 'wazuh') return '<span class="badge wazuh">wazuh</span>';
if (src === 'vm112-onboard') return '<span class="badge onboard">onboard</span>'; if (src === 'vm112-onboard') return '<span class="badge onboard">onboard</span>';
@ -1713,9 +1730,14 @@ async function openUserAccessModal() {
} }
} }
async function renderOverview() { async function renderOverview(options = {}) {
const { poll = false } = options;
const el = document.getElementById('overview-content'); const el = document.getElementById('overview-content');
if (!viewHasContent(el, '.health-grid')) {
el.innerHTML = '<p class="loading">Carregando overview…</p>'; el.innerHTML = '<p class="loading">Carregando overview…</p>';
} else if (poll) {
el.classList.add('view--refreshing');
}
try { try {
const secPromise = window.DeskModules?.isEnabled('wizard-security') const secPromise = window.DeskModules?.isEnabled('wizard-security')
? api('/v1/security/summary?window_hours=24').catch(() => null) ? api('/v1/security/summary?window_hours=24').catch(() => null)
@ -1766,6 +1788,8 @@ async function renderOverview() {
}); });
} catch (e) { } catch (e) {
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`; el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
} finally {
el?.classList.remove('view--refreshing');
} }
} }
@ -1883,13 +1907,18 @@ async function renderOverviewHome(options = {}) {
el.innerHTML = '<p class="loading">Módulo Serviços não carregado.</p>'; el.innerHTML = '<p class="loading">Módulo Serviços não carregado.</p>';
} }
async function renderLeads() { async function renderLeads(options = {}) {
const { poll = false } = options;
const el = document.getElementById('leads-content'); const el = document.getElementById('leads-content');
if (!canReadLeads()) { if (!canReadLeads()) {
el.innerHTML = '<p class="loading">Sem permissão para ver leads</p>'; el.innerHTML = '<p class="loading">Sem permissão para ver leads</p>';
return; return;
} }
if (!viewHasContent(el, '.lead-grid')) {
el.innerHTML = '<p class="loading">Carregando leads…</p>'; el.innerHTML = '<p class="loading">Carregando leads…</p>';
} else if (poll) {
el.classList.add('view--refreshing');
}
try { try {
const data = await api('/v1/crm/leads'); const data = await api('/v1/crm/leads');
const leads = data.leads || []; const leads = data.leads || [];
@ -1916,6 +1945,8 @@ async function renderLeads() {
}); });
} catch (e) { } catch (e) {
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`; el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
} finally {
el?.classList.remove('view--refreshing');
} }
} }
@ -2111,7 +2142,8 @@ async function updateTicketStatus(status) {
await renderTickets(); await renderTickets();
} }
async function renderEvents() { async function renderEvents(options = {}) {
const { poll = false } = options;
syncEventsToolbar(); syncEventsToolbar();
if (state.eventsTab === 'purges') { if (state.eventsTab === 'purges') {
await renderPurgeHistory(); await renderPurgeHistory();
@ -2126,7 +2158,11 @@ async function renderEvents() {
return; return;
} }
const el = document.getElementById('events-content'); const el = document.getElementById('events-content');
if (!viewHasContent(el, '.table-wrap')) {
el.innerHTML = '<p class="loading">Carregando eventos…</p>'; el.innerHTML = '<p class="loading">Carregando eventos…</p>';
} else if (poll) {
el.classList.add('view--refreshing');
}
try { try {
const srcQ = state.eventSourceFilter !== 'all' ? `?source=${state.eventSourceFilter}` : ''; const srcQ = state.eventSourceFilter !== 'all' ? `?source=${state.eventSourceFilter}` : '';
const data = await api(`/v1/webhooks/events${srcQ}`); const data = await api(`/v1/webhooks/events${srcQ}`);
@ -2151,6 +2187,8 @@ async function renderEvents() {
</div>`; </div>`;
} catch (e) { } catch (e) {
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`; el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
} finally {
el?.classList.remove('view--refreshing');
} }
} }
@ -4110,9 +4148,14 @@ function openInfraProcessDetail(procId) {
} }
} }
async function renderInfra() { async function renderInfra(options = {}) {
const { poll = false } = options;
const el = document.getElementById('infra-content'); const el = document.getElementById('infra-content');
if (!viewHasContent(el, '.infra-vm-section')) {
el.innerHTML = '<p class="loading">Verificando stack…</p>'; el.innerHTML = '<p class="loading">Verificando stack…</p>';
} else if (poll) {
el.classList.add('view--refreshing');
}
try { try {
const [stack, integrations, health, vm123Health, purgeMeta] = await Promise.all([ const [stack, integrations, health, vm123Health, purgeMeta] = await Promise.all([
api('/v1/infra/stack/status'), api('/v1/infra/stack/status'),
@ -4169,10 +4212,12 @@ async function renderInfra() {
${sections} ${sections}
${integrationsCard ? `<section class="infra-vm-section"><h3 class="infra-vm-head">Integrações · Desk API</h3><div class="proc-grid">${integrationsCard}</div></section>` : ''} ${integrationsCard ? `<section class="infra-vm-section"><h3 class="infra-vm-head">Integrações · Desk API</h3><div class="proc-grid">${integrationsCard}</div></section>` : ''}
</div>`; </div>`;
document.getElementById('btn-infra-refresh-stack')?.addEventListener('click', () => renderInfra()); document.getElementById('btn-infra-refresh-stack')?.addEventListener('click', () => renderInfra({ force: true }));
bindStackCardActions(el); bindStackCardActions(el);
} catch (e) { } catch (e) {
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`; el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
} finally {
el?.classList.remove('view--refreshing');
} }
} }
@ -4284,20 +4329,25 @@ async function refresh(options = {}) {
if (poll && state.view === 'account') { if (poll && state.view === 'account') {
return; return;
} }
if (state.view === 'dashboard') await renderDashboard(); // 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 === 'email-migration' && window.DeskEmailMigration?.renderPage) await window.DeskEmailMigration.renderPage();
if (state.view === 'overview') await renderOverview(); if (state.view === 'overview') await renderOverview(p);
if (state.view === 'overview-home') await renderOverviewHome({ poll }); if (state.view === 'overview-home') await renderOverviewHome({ poll });
if (state.view === 'leads') await renderLeads(); if (state.view === 'leads') await renderLeads(p);
if (state.view === 'tickets') { if (state.view === 'tickets') {
if (poll && window.TicketsWorkspace?._pageReady) await TicketsWorkspace.softRefresh(); if (poll && window.TicketsWorkspace?._pageReady) await TicketsWorkspace.softRefresh();
else await renderTickets({ poll: false }); else await renderTickets({ poll: false });
} }
if (state.view === 'events') await renderEvents(); if (state.view === 'events') await renderEvents(p);
if (state.view === 'tenants') await renderTenants(); if (state.view === 'tenants') await renderTenants();
if (state.view === 'infra') await renderInfra(); if (state.view === 'infra') await renderInfra(p);
if (state.view === 'infra2') await renderInfra2({ poll }); if (state.view === 'infra2') await renderInfra2({ poll });
if (state.view === 'agentic-ops' && window.renderAgenticOps) await window.renderAgenticOps(); if (state.view === 'agentic-ops' && window.renderAgenticOps) await window.renderAgenticOps(p);
if (state.view === 'messages') await renderMessages(); if (state.view === 'messages') await renderMessages();
if (state.view === 'admin') await renderAdmin(); if (state.view === 'admin') await renderAdmin();
if (state.view === 'access-matrix' && window.renderAccessMatrix) await window.renderAccessMatrix(); if (state.view === 'access-matrix' && window.renderAccessMatrix) await window.renderAccessMatrix();

View file

@ -1302,6 +1302,13 @@ th { color: var(--muted); font-weight: 600; font-size: 0.75rem; text-transform:
.hidden { display: none !important; } .hidden { display: none !important; }
.loading { color: var(--muted); font-size: 0.9rem; padding: 1rem; } .loading { color: var(--muted); font-size: 0.9rem; padding: 1rem; }
/* Poll suave — sem flash "Carregando…" */
.view--refreshing {
opacity: 0.94;
transition: opacity 0.25s ease;
pointer-events: none;
}
.infra-card { .infra-card {
display: flex; display: flex;
align-items: center; align-items: center;

View file

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