/** * Results and Works Desk — UI aprovada (Spec 032 / Roger 2026-06-28) * KPI Overview + tabela operacional + alertas agentic. */ const TicketsWorkspace = { context: null, queueFilter: null, priorityFilter: null, _pageReady: false, _listFingerprint: '', async loadContext() { const [presence, funnel, summary] = await Promise.all([ window.DeskLive?.enabled() ? api('/v1/live/presence').catch(() => ({ sessions: [] })) : Promise.resolve({ sessions: [] }), api('/v1/onboard/funnel?window_hours=48').catch(() => ({ active_sessions: [] })), api('/v1/desk/summary').catch(() => ({})), ]); const liveBySession = {}; for (const s of presence.sessions || []) { if (s.session_id) liveBySession[s.session_id] = s; } const funnelBySession = {}; for (const s of funnel.active_sessions || []) { if (s.session_id) funnelBySession[s.session_id] = s; } this.context = { liveBySession, funnelBySession, staleHours: summary.onboard_stale_hours ?? 24, summary, }; return this.context; }, stripEnrichment(t) { const out = { ...t }; for (const k of Object.keys(out)) { if (k.startsWith('_')) delete out[k]; } return out; }, enrichTicket(t) { const ctx = this.context || { liveBySession: {}, funnelBySession: {}, staleHours: 24 }; const sid = (t.session_id || '').trim(); const funnel = sid ? ctx.funnelBySession[sid] : null; const isActive = ['open', 'escalated', 'assisting', 'resolved'].includes(t.status); const lastAt = funnel?.last_event_at || t.assisted_at || t.created_at; let idleHours = 0; if (lastAt) idleHours = (Date.now() - new Date(lastAt).getTime()) / 3600000; const ageHours = t.created_at ? (Date.now() - new Date(t.created_at).getTime()) / 3600000 : 0; const stale = funnel?.stale || (isActive && idleHours >= ctx.staleHours); const sev = Number(t.severity); const isWazuh = t.source === 'wazuh' || t.event === 'wazuh.alert'; let priority = 'info'; if (sev >= 12 || (isWazuh && isActive && sev >= 10)) priority = 'critical'; else if (sev >= 10 || t.status === 'escalated') priority = 'high'; else if (sev >= 7 || stale) priority = 'warn'; let voScore = 3; if (sev >= 12) voScore = 10; else if (sev >= 10) voScore = 8; else if (sev >= 7) voScore = 6; else if (stale) voScore = 5; else if (t.status === 'escalated') voScore = 7; else if (t.status === 'open') voScore = 4; const live = sid ? !!ctx.liveBySession[sid] : false; let base = { ...t, _idleHours: Math.round(idleHours), _ageHours: Math.round(ageHours), _stale: stale && isActive, _live: live, _lastEventAt: funnel?.last_event_at || null, _unassigned: isActive && !t.assigned_to && !t.assisted_by, _wazuh: isWazuh, _security: isWazuh || (t.subject || '').toLowerCase().includes('security'), _priority: priority, _voScore: voScore, _stageLabel: typeof FUNNEL_LABELS !== 'undefined' ? (FUNNEL_LABELS[funnel?.current_stage] || funnel?.current_stage || '') : '', }; if (window.TicketsSla?.computePriority) { base = TicketsSla.computePriority(base); } return base; }, formatMetric(val) { if (val === '—' || val == null || val === '') return '—'; const n = parseFloat(val); return Number.isFinite(n) ? `${n.toFixed(1)}h` : String(val); }, processTickets(tickets) { const enriched = tickets.map((t) => this.enrichTicket(this.stripEnrichment(t))); if (window.TicketsSla?.processList) return TicketsSla.processList(enriched); return enriched.sort((a, b) => (b._priorityScore || b._voScore || 0) - (a._priorityScore || a._voScore || 0)); }, computeMetrics(tickets) { const enriched = this.processTickets(tickets); let overview = null; if (window.TicketsSla?.computeOverview) { overview = TicketsSla.computeOverview(enriched); } const priorities = overview?.buckets || { critical: 0, high: 0, warn: 0, info: 0 }; const mttrNum = overview ? (overview.mttr === '—' ? 0 : parseFloat(overview.mttr) || 0) : 0; const respNum = overview ? (overview.avgResponse === '—' ? 0 : parseFloat(overview.avgResponse) || 0) : 0; if (!overview) { for (const t of enriched) priorities[t._priorityBand || t._priority] = (priorities[t._priorityBand || t._priority] || 0) + 1; } return { enriched, slaPct: overview?.slaPct ?? 0, mttr: overview?.mttr ?? '0', resp: overview?.avgResponse ?? '0', mttrNum, respNum, priorities, criticalWaiting: overview?.criticalWaiting ?? priorities.critical ?? 0, maxPriority: Math.max(priorities.critical || 0, priorities.high || 0, priorities.warn || 0, priorities.info || 0, 1), }; }, gaugeHtml(pct) { const p = Math.max(0, Math.min(100, Number(pct) || 0)); const r = 42; const circ = Math.PI * r; const filled = ((p / 100) * circ).toFixed(2); const uid = `rwdG${Math.random().toString(36).slice(2, 8)}`; return ` `; }, priorityBarsHtml(priorities, max) { const items = [ { key: 'critical', label: 'Critical', count: priorities.critical || 0 }, { key: 'high', label: 'High', count: priorities.high || 0 }, { key: 'warn', label: 'Warn', count: priorities.warn || 0 }, { key: 'info', label: 'Info', count: priorities.info || 0 }, ]; const maxVal = Math.max(15, max, ...items.map((i) => i.count), 1); return ` `; }, kpiOverviewHtml(m) { return `

KPI Overview

% SLA Compliant (General)
${this.gaugeHtml(m.slaPct)}
Avg. Resolution Time (MTTR)
${this.formatMetric(m.mttr)}
Avg. Response Time
${this.formatMetric(m.resp)}
Tickets by Priority
${this.priorityBarsHtml(m.priorities, m.maxPriority)}
Critical Waiting
${m.criticalWaiting}
`; }, headerHtml() { return `

Sessão Tickets

Results and Works Desk

Operações Ligbox — onboarding, tickets e monitoramento

`; }, tenantCell(t) { const name = t.domain || t.agent || (t.subject || '').slice(0, 32) || `Ticket #${t.id}`; const tags = [ t._tenant === 'SECURITY' || t._security ? 'SECURITY' : '', t._wazuh ? 'WAZUH' : '', t._tenant === 'ONBOARD' || t.source === 'vm112-onboard' ? 'ONBOARD' : '', ].filter(Boolean).join(''); const resp = t._responseSla || (t.assisted_at ? 'OK' : (t._stale ? 'WARNING' : 'PENDING')); const res = t._resolutionSla || (t.status === 'closed' || t.status === 'resolved' ? 'OK' : (t._stale ? 'STALE (4h+)' : 'OPEN')); return `
${tags}${esc(name)}
Resp: ${resp} · Res: ${res}
`; }, detailsCell(t) { const parts = []; if (t.agent) parts.push(esc(t.agent)); parts.push(`ID: ${t.id}`); if (t.severity != null) parts.push(`Wazuh Lvl ${t.severity}`); else if (t._stageLabel) parts.push(esc(t._stageLabel)); else if (t.event) parts.push(esc(String(t.event).replace('onboarding.', ''))); return `
${esc(t.description || t.subject || parts[0] || '—')}
${parts.join(' · ')}
`; }, scoreCell(t) { const stale = t._stale ? `PARADO ${t._idleHours}H` : ''; const st = t.status || 'open'; const live = t._live ? 'LIVE' : 'OFF'; const score = t._priorityScore ?? t._voScore ?? '—'; return `
${esc(statusLabel(st))} ${live} ${stale} ${score} V0 SCORE
`; }, tableHtml(tickets) { if (!tickets.length) { return '

Nenhum ticket neste filtro

'; } const rows = tickets.map((t) => { const sel = state.selectedTicketId === t.id ? ' selected' : ''; return ` ${this.scoreCell(t)} ${this.tenantCell(t)} ${this.detailsCell(t)} ${t._idleHours}h
Inativo ${t._ageHours}h
Desde a abertura ${t.assisted_by || t.assigned_to ? `${esc(t.assisted_by || t.assigned_to)}` : 'Sem atribuição
AGENT'} `; }).join(''); return `
${rows}
Priority Score Tenant & SLAs Details Idle (last event) Total Age Responsável
`; }, applyFilters(tickets) { let list = tickets; const qf = state.ticketFilter || this.queueFilter || 'all'; if (this.priorityFilter) { list = list.filter((t) => (t._priorityBand || t._priority) === this.priorityFilter); } if (qf === 'active') { list = list.filter((t) => ['open', 'escalated', 'assisting', 'resolved'].includes(t.status)); } else if (qf && qf !== 'all') { list = list.filter((t) => t.status === qf); } if (state.sourceFilter && state.sourceFilter !== 'all') { list = list.filter((t) => t.source === state.sourceFilter); } return list; }, async fetchAgenticAlerts() { try { const data = await api('/v1/agents/findings?open_only=true&limit=6'); return (data.findings || []).map((f) => ({ title: f.title || 'Finding', sub: `${f.severity || 'warn'} · ${f.category || 'agent'}`, id: f.id, })); } catch { return []; } }, alertsHtml(items) { if (!items.length) return ''; return ` `; }, bindTable(root, tickets) { root.querySelectorAll('.rwd-row').forEach((row) => { row.addEventListener('click', async () => { state.selectedTicketId = Number(row.dataset.id); state.selectedSessionId = row.dataset.session || null; root.querySelectorAll('.rwd-row').forEach((r) => r.classList.remove('selected')); row.classList.add('selected'); if (window.TicketsDetailPanel?.openDrawer) { await TicketsDetailPanel.openDrawer(state.selectedTicketId); } else if (typeof renderTicketDetail === 'function') { await renderTicketDetail(); } }); }); }, bindPage(root) { root.querySelector('#rwd-refresh')?.addEventListener('click', () => { this._pageReady = false; if (typeof refresh === 'function') refresh(); }); root.querySelector('#rwd-filter-toggle')?.addEventListener('click', () => { document.getElementById('view-tickets')?.classList.toggle('rwd-filters-open'); }); root.querySelector('[data-goto-agentic]')?.addEventListener('click', () => setView('agentic-ops')); root.querySelectorAll('[data-goto-agentic]').forEach((el) => { el.addEventListener('click', () => setView('agentic-ops')); }); }, syncPageTitle() { document.querySelector('.main')?.classList.add('main--rwd-tickets'); }, listFingerprint(tickets) { return tickets.map((t) => [t.id, t.status, t._idleHours, t._priority].join(':')).join('|'); }, renderShell(metrics, filtered, alerts) { return `
${this.headerHtml()} ${this.kpiOverviewHtml(metrics)} ${this.tableHtml(filtered)} ${this.alertsHtml(alerts)}
`; }, mountRoot() { let root = document.getElementById('tickets-rwd-root'); if (!root) { const view = document.getElementById('view-tickets'); if (!view) return null; root = document.createElement('div'); root.id = 'tickets-rwd-root'; view.insertBefore(root, view.firstChild); } return root; }, async renderListOnly() { const root = document.getElementById('tickets-rwd-root'); if (!root || !state.tickets?.length) return; const metrics = this.computeMetrics(state.tickets.map((t) => this.stripEnrichment(t))); state.tickets = metrics.enriched; const filtered = this.applyFilters(metrics.enriched); const fp = this.listFingerprint(filtered); if (fp === this._listFingerprint && root.querySelector('.rwd-table')) return; this._listFingerprint = fp; const alerts = await this.fetchAgenticAlerts(); root.innerHTML = this.renderShell({ ...metrics, enriched: metrics.enriched }, filtered, alerts); this.bindTable(root, filtered); this.bindPage(root); }, async softRefresh() { if (!this._pageReady || state.view !== 'tickets') return; try { await this.loadContext(); await this.renderListOnly(); } catch { /* poll */ } }, async renderPage({ listEl, detailEl, tickets }) { this.syncPageTitle(); await this.loadContext(); const metrics = this.computeMetrics(tickets); state.tickets = metrics.enriched; const filtered = this.applyFilters(metrics.enriched); this._listFingerprint = this.listFingerprint(filtered); const alerts = await this.fetchAgenticAlerts(); const root = this.mountRoot(); if (!root) return; root.innerHTML = this.renderShell(metrics, filtered, alerts); this.bindTable(root, filtered); this.bindPage(root); this._pageReady = true; if (listEl) listEl.innerHTML = ''; if (detailEl && !state.selectedTicketId) { detailEl.innerHTML = ''; } }, }; window.TicketsWorkspace = TicketsWorkspace;