/**
* 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 `
${items.map((it) => {
const h = it.count > 0 ? Math.max(16, Math.round((it.count / maxVal) * 100)) : 8;
return `
`;
}).join('')}
`;
},
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 `
`;
},
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 `
| Priority Score |
Tenant & SLAs |
Details |
Idle (last event) |
Total Age |
Responsável |
${rows}
`;
},
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;