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:
parent
48b0dc2fe3
commit
1640a93ae1
4 changed files with 123 additions and 37 deletions
|
|
@ -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));
|
||||||
patchHtml(el.querySelector('#ao-subnav'), subnavHtml());
|
|
||||||
patchHtml(el.querySelector('#ao-squad-grid'), squadHtml(data, view));
|
const incFp = incidentsFingerprint(data);
|
||||||
patchHtml(el.querySelector('#ao-kanban'), kanbanHtml(view));
|
const tickFp = timelineFingerprint(data.timeline);
|
||||||
patchHtml(el.querySelector('#ao-timeline'), timelineHtml(data.timeline));
|
const boardChanged = !soft || incFp !== state._aoIncidentsFp;
|
||||||
patchHtml(el.querySelector('#ao-task-board'), taskBoardHtml(data));
|
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');
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
el.className = 'status-pill ok';
|
const html = '<span class="dot"></span> API online';
|
||||||
el.innerHTML = '<span class="dot"></span> API online';
|
if (!el.classList.contains('ok') || el.innerHTML !== html) {
|
||||||
return h;
|
el.className = 'status-pill ok';
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
el.className = 'status-pill err';
|
const html = '<span class="dot"></span> API offline';
|
||||||
el.innerHTML = '<span class="dot"></span> API offline';
|
if (!el.classList.contains('err') || el.innerHTML !== html) {
|
||||||
|
el.className = 'status-pill err';
|
||||||
|
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');
|
||||||
box.innerHTML = '<p class="loading">Carregando…</p>';
|
if (!viewHasContent(box, '.dashboard-top')) {
|
||||||
|
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');
|
||||||
el.innerHTML = '<p class="loading">Carregando overview…</p>';
|
if (!viewHasContent(el, '.health-grid')) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
el.innerHTML = '<p class="loading">Carregando leads…</p>';
|
if (!viewHasContent(el, '.lead-grid')) {
|
||||||
|
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');
|
||||||
el.innerHTML = '<p class="loading">Carregando eventos…</p>';
|
if (!viewHasContent(el, '.table-wrap')) {
|
||||||
|
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');
|
||||||
el.innerHTML = '<p class="loading">Verificando stack…</p>';
|
if (!viewHasContent(el, '.infra-vm-section')) {
|
||||||
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue