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 };
}
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) {
if (openAgents.has(id)) return { cls: 'ao-squad-status--alert', label: 'alerta' };
if (state.selectedAgent === id) return { cls: 'ao-squad-status--active', label: 'activo' };
@ -691,18 +701,36 @@
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 open = data.overview.incidents_open || {};
const dot = el.querySelector('#ao-live-dot');
if (dot) dot.className = `ao-live-dot ${liveDotClass(open)}`.trim();
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-squad-grid'), squadHtml(data, view));
patchHtml(el.querySelector('#ao-kanban'), kanbanHtml(view));
state._aoIncidentsFp = incFp;
}
if (timelineChanged) {
patchHtml(el.querySelector('#ao-timeline'), timelineHtml(data.timeline));
state._aoTimelineFp = tickFp;
}
if (!soft) {
patchHtml(el.querySelector('#ao-task-board'), taskBoardHtml(data));
patchSubViewPanes(el, data);
}
const boardLabel = el.querySelector('#ao-board-label');
if (boardLabel) {
@ -711,8 +739,9 @@
: 'Mission Board';
}
patchSubViewPanes(el, data);
if (!soft) {
updateSubViews(el);
}
updateMainTabs(el);
updateCommsChrome(el, view.sel);
updateMobileTabs(el);
@ -1215,7 +1244,7 @@
bindEventsOnce(el);
}
const view = patchDashboard(el, data);
const view = patchDashboard(el, data, { soft });
if (saved) restoreInputs(el, saved);
if (state.threadId && state.chatModalOpen) {

View file

@ -286,21 +286,38 @@ function reschedulePoll() {
async function loadHealth() {
const el = document.getElementById('global-health');
if (!el) return null;
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.innerHTML = '<span class="dot"></span> API online';
return h;
el.innerHTML = html;
}
return true;
} catch {
const html = '<span class="dot"></span> API offline';
if (!el.classList.contains('err') || el.innerHTML !== html) {
el.className = 'status-pill err';
el.innerHTML = '<span class="dot"></span> API offline';
el.innerHTML = html;
}
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');
if (!viewHasContent(box, '.dashboard-top')) {
box.innerHTML = '<p class="loading">Carregando…</p>';
} else if (poll) {
box.classList.add('view--refreshing');
}
try {
const leadsPromise = canReadLeads()
? api('/v1/crm/leads').catch(() => ({ leads: [], total: 0 }))
@ -425,10 +442,10 @@ async function renderDashboard() {
});
} catch (e) {
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 === 'wazuh') return '<span class="badge wazuh">wazuh</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');
if (!viewHasContent(el, '.health-grid')) {
el.innerHTML = '<p class="loading">Carregando overview…</p>';
} 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)
@ -1766,6 +1788,8 @@ async function renderOverview() {
});
} catch (e) {
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>';
}
async function renderLeads() {
async function renderLeads(options = {}) {
const { poll = false } = options;
const el = document.getElementById('leads-content');
if (!canReadLeads()) {
el.innerHTML = '<p class="loading">Sem permissão para ver leads</p>';
return;
}
if (!viewHasContent(el, '.lead-grid')) {
el.innerHTML = '<p class="loading">Carregando leads…</p>';
} else if (poll) {
el.classList.add('view--refreshing');
}
try {
const data = await api('/v1/crm/leads');
const leads = data.leads || [];
@ -1916,6 +1945,8 @@ async function renderLeads() {
});
} catch (e) {
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();
}
async function renderEvents() {
async function renderEvents(options = {}) {
const { poll = false } = options;
syncEventsToolbar();
if (state.eventsTab === 'purges') {
await renderPurgeHistory();
@ -2126,7 +2158,11 @@ async function renderEvents() {
return;
}
const el = document.getElementById('events-content');
if (!viewHasContent(el, '.table-wrap')) {
el.innerHTML = '<p class="loading">Carregando eventos…</p>';
} 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}`);
@ -2151,6 +2187,8 @@ async function renderEvents() {
</div>`;
} catch (e) {
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');
if (!viewHasContent(el, '.infra-vm-section')) {
el.innerHTML = '<p class="loading">Verificando stack…</p>';
} else if (poll) {
el.classList.add('view--refreshing');
}
try {
const [stack, integrations, health, vm123Health, purgeMeta] = await Promise.all([
api('/v1/infra/stack/status'),
@ -4169,10 +4212,12 @@ async function renderInfra() {
${sections}
${integrationsCard ? `<section class="infra-vm-section"><h3 class="infra-vm-head">Integrações · Desk API</h3><div class="proc-grid">${integrationsCard}</div></section>` : ''}
</div>`;
document.getElementById('btn-infra-refresh-stack')?.addEventListener('click', () => renderInfra());
document.getElementById('btn-infra-refresh-stack')?.addEventListener('click', () => renderInfra({ force: true }));
bindStackCardActions(el);
} catch (e) {
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') {
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 === 'overview') await renderOverview();
if (state.view === 'overview') await renderOverview(p);
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 (poll && window.TicketsWorkspace?._pageReady) await TicketsWorkspace.softRefresh();
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 === 'infra') await renderInfra();
if (state.view === 'infra') await renderInfra(p);
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 === 'admin') await renderAdmin();
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; }
.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 {
display: flex;
align-items: center;

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=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/agentic-ops.css?v=20260620v10"/>
<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-detail-panel.js?v=20260619tickets2"></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/app.js?v=20260625socfix"></script>
<script src="/assets/app.js?v=20260625pollfix"></script>
</body>
</html>