Commita governance, user-wizard, operational-feed e catálogo RBAC; adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial; documenta credencial VM112 @betinplace. Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
5.5 KiB
JavaScript
162 lines
5.5 KiB
JavaScript
/**
|
||
* Tickets SLA + Priority Score v0 — port do ResultsDesk (React)
|
||
* Spec negócio Roger: SLA_SETTINGS + fórmula composta origem × idle
|
||
*/
|
||
const TicketsSla = {
|
||
SETTINGS: {
|
||
ONBOARD: { response: 2, resolution: 72 },
|
||
DESK: { response: 1, resolution: 8 },
|
||
SECURITY: { response: 0.5, resolution: 4 },
|
||
AGENTIC_OPS: { response: 0.5, resolution: 4 },
|
||
},
|
||
|
||
PRIORITY_CRITICAL: 7,
|
||
PRIORITY_HIGH: 4,
|
||
PRIORITY_WARN: 2,
|
||
|
||
mapTenant(t) {
|
||
const src = t.source || '';
|
||
const ev = t.event || '';
|
||
if (src === 'wazuh' || ev === 'wazuh.alert' || src === 'vm112-security' || (t.subject || '').startsWith('[security]')) {
|
||
return 'SECURITY';
|
||
}
|
||
if (src === 'vm112-onboard' || ev.startsWith('onboarding') || ev.startsWith('domain.') || ev.startsWith('dns.')) {
|
||
return 'ONBOARD';
|
||
}
|
||
if (src === 'agentic-ops' || (t.subject || '').toLowerCase().includes('[agentic')) {
|
||
return 'AGENTIC_OPS';
|
||
}
|
||
return 'DESK';
|
||
},
|
||
|
||
hoursSince(iso) {
|
||
if (!iso) return 0;
|
||
try {
|
||
return Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 3600000));
|
||
} catch {
|
||
return 0;
|
||
}
|
||
},
|
||
|
||
isInternalDomain(domain) {
|
||
const d = String(domain || '').toLowerCase();
|
||
return d.includes('itecnologys.com') || d.includes('ligbox.com.br');
|
||
},
|
||
|
||
computePriority(t) {
|
||
const tenant = this.mapTenant(t);
|
||
const sla = this.SETTINGS[tenant] || { response: 2, resolution: 24 };
|
||
const totalAgeHours = this.hoursSince(t.created_at);
|
||
const lastEvent = t._lastEventAt || null;
|
||
const idleHours = t._idleHours != null
|
||
? t._idleHours
|
||
: this.hoursSince(lastEvent || t.created_at);
|
||
|
||
const wazuhLevel = t.severity != null ? Number(t.severity) : null;
|
||
const isLead = t.crm_track === 'lead' || t.is_lead;
|
||
|
||
let originScore = 1.0;
|
||
if (tenant === 'SECURITY' && wazuhLevel != null && wazuhLevel >= 1) originScore = 4.0;
|
||
else if (this.isInternalDomain(t.domain || t.email)) originScore = 3.0;
|
||
else if (isLead) originScore = 2.0;
|
||
|
||
let idleScore = 0.5;
|
||
if (idleHours >= sla.resolution * 2) idleScore = 2.5;
|
||
else if (idleHours >= sla.resolution) idleScore = 2.0;
|
||
else if (idleHours >= sla.response * 2) idleScore = 1.5;
|
||
else if (idleHours >= sla.response) idleScore = 1.0;
|
||
|
||
const priorityScore = parseFloat((originScore * idleScore).toFixed(1));
|
||
|
||
let responseSla = 'OK';
|
||
let resolutionSla = 'OK';
|
||
if (idleHours > sla.response) responseSla = 'WARNING';
|
||
if (idleHours > sla.resolution) resolutionSla = `STALE (${sla.resolution}h+)`;
|
||
|
||
let priorityBand = 'info';
|
||
if (priorityScore >= this.PRIORITY_CRITICAL) priorityBand = 'critical';
|
||
else if (priorityScore >= this.PRIORITY_HIGH) priorityBand = 'high';
|
||
else if (priorityScore >= this.PRIORITY_WARN) priorityBand = 'warn';
|
||
|
||
return {
|
||
...t,
|
||
_tenant: tenant,
|
||
_priorityScore: priorityScore,
|
||
_priorityBand: priorityBand,
|
||
_totalAgeHours: totalAgeHours,
|
||
_idleHours: idleHours,
|
||
_responseSla: responseSla,
|
||
_resolutionSla: resolutionSla,
|
||
_slaLimits: sla,
|
||
};
|
||
},
|
||
|
||
processList(tickets) {
|
||
return tickets
|
||
.map((t) => this.computePriority(t))
|
||
.sort((a, b) => b._priorityScore - a._priorityScore);
|
||
},
|
||
|
||
computeOverview(tickets) {
|
||
const rows = tickets.map((t) => this.computePriority(t));
|
||
const active = rows.filter((t) => !['closed'].includes(t.status));
|
||
const resolved = rows.filter((t) => ['resolved', 'closed'].includes(t.status));
|
||
|
||
const slaOk = active.filter((t) => t._responseSla === 'OK' && t._resolutionSla === 'OK').length;
|
||
const slaPct = active.length ? Math.round((slaOk / active.length) * 100) : 100;
|
||
|
||
const mttrSamples = resolved.map((t) => t._totalAgeHours).filter((h) => h > 0);
|
||
const mttr = mttrSamples.length
|
||
? (mttrSamples.reduce((a, b) => a + b, 0) / mttrSamples.length).toFixed(1)
|
||
: '—';
|
||
|
||
const responseSamples = rows
|
||
.filter((t) => t.assigned_at || t.assisted_at)
|
||
.map((t) => {
|
||
try {
|
||
const start = new Date(t.created_at).getTime();
|
||
const end = new Date(t.assigned_at || t.assisted_at).getTime();
|
||
return Math.max(0, Math.round((end - start) / 3600000));
|
||
} catch {
|
||
return null;
|
||
}
|
||
})
|
||
.filter((h) => h != null);
|
||
const avgResponse = responseSamples.length
|
||
? (responseSamples.reduce((a, b) => a + b, 0) / responseSamples.length).toFixed(1)
|
||
: (active.length
|
||
? (active.reduce((s, t) => s + t._idleHours, 0) / active.length).toFixed(1)
|
||
: '—');
|
||
|
||
const buckets = { critical: 0, high: 0, warn: 0, info: 0 };
|
||
for (const t of rows) buckets[t._priorityBand] = (buckets[t._priorityBand] || 0) + 1;
|
||
|
||
const criticalWaiting = active.filter(
|
||
(t) => t._priorityBand === 'critical' && ['open', 'escalated', 'assisting'].includes(t.status),
|
||
).length;
|
||
|
||
return {
|
||
slaPct,
|
||
mttr,
|
||
avgResponse,
|
||
buckets,
|
||
criticalWaiting,
|
||
total: rows.length,
|
||
};
|
||
},
|
||
|
||
getInitials(name) {
|
||
if (!name) return '—';
|
||
const parts = String(name).trim().split(/\s+/);
|
||
return ((parts[0]?.[0] || '') + (parts[1]?.[0] || parts[0]?.[1] || '')).toUpperCase() || '—';
|
||
},
|
||
|
||
assigneeLabel(t, userMap = {}) {
|
||
const raw = t.assisted_by || t.assigned_to || '';
|
||
if (!raw) return { label: 'Sem atribuição', initials: '—' };
|
||
const label = userMap[raw] || raw.replace(/[._]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||
return { label, initials: this.getInitials(label) };
|
||
},
|
||
};
|
||
|
||
window.TicketsSla = TicketsSla;
|