diff --git a/projects/ops-desk/frontend/assets/app.js b/projects/ops-desk/frontend/assets/app.js
index 0af3104..71a6fe4 100644
--- a/projects/ops-desk/frontend/assets/app.js
+++ b/projects/ops-desk/frontend/assets/app.js
@@ -608,6 +608,28 @@ function billingTicketIcon(t) {
return '';
}
+function normalizeAssistStatus(status) {
+ if (status === 'active') return 'assisting';
+ return status;
+}
+
+function ticketFunnelKvHtml(t) {
+ const latest = t.latest_funnel_event || t.event;
+ const opened = t.event_opened || t.event;
+ const showOpened = opened && latest && opened !== latest;
+ const outcome = t.onboarding_outcome;
+ const outcomeBadge = outcome === 'completed'
+ ? 'concluído'
+ : outcome === 'failed'
+ ? 'falhou'
+ : '';
+ const label = latest ? (SOC_EVENT_LABELS[latest] || latest) : '—';
+ const sev = latest && typeof socEventSeverity === 'function' ? socEventSeverity(latest) : 'open';
+ return `
+
Estado funil${esc(label)} ${outcomeBadge}
+ ${showOpened ? `Abertura${esc(SOC_EVENT_LABELS[opened] || opened)}` : ''}`;
+}
+
function ticketRowHtml(t) {
const review = t.needs_review ? 'revisão' : '';
const verified = t.account_verified ? 'verificado' : '';
@@ -1845,10 +1867,15 @@ async function renderLeads() {
}
}
-async function renderTickets() {
+async function renderTickets(options = {}) {
+ const { poll = false } = options;
stopLiveTimingClock();
const listEl = document.getElementById('ticket-list');
const detailEl = document.getElementById('ticket-detail');
+ if (poll && window.TicketsWorkspace?._pageReady) {
+ await TicketsWorkspace.softRefresh();
+ return;
+ }
listEl.innerHTML = 'Carregando tickets…
';
try {
let tickets = [];
@@ -1864,32 +1891,43 @@ async function renderTickets() {
source: 'vm112-onboard',
crm_track: 'lead',
assigned_to: l.assigned_to,
+ session_id: l.session_id,
+ lead_funnel_stage: l.funnel_stage,
}));
} else {
let q = '';
const params = [];
- if (state.ticketFilter !== 'all') params.push(`status=${state.ticketFilter}`);
+ if (state.ticketFilter !== 'all' && state.ticketFilter !== 'active') {
+ params.push(`status=${state.ticketFilter}`);
+ }
if (state.sourceFilter !== 'all') params.push(`source=${state.sourceFilter}`);
if (params.length) q = '?' + params.join('&');
const data = await api(`/v1/desk/tickets${q}`);
tickets = data.tickets || [];
+ if (state.ticketFilter === 'active') {
+ tickets = tickets.filter((t) => ['open', 'escalated', 'assisting', 'resolved'].includes(t.status));
+ }
}
- state.tickets = tickets;
- listEl.innerHTML = state.tickets.length
- ? state.tickets.map(ticketRowHtml).join('')
- : 'Nenhum ticket neste filtro
';
- listEl.querySelectorAll('.ticket-row').forEach((btn) => {
- btn.addEventListener('click', () => {
- state.selectedTicketId = Number(btn.dataset.id);
- state.selectedSessionId = null;
- renderTicketDetail();
- listEl.querySelectorAll('.ticket-row').forEach((r) => r.classList.remove('selected'));
- btn.classList.add('selected');
+ if (window.TicketsWorkspace) {
+ await TicketsWorkspace.renderPage({ listEl, detailEl, tickets });
+ } else {
+ state.tickets = tickets;
+ listEl.innerHTML = state.tickets.length
+ ? state.tickets.map(ticketRowHtml).join('')
+ : 'Nenhum ticket nesto filtro
';
+ listEl.querySelectorAll('.ticket-row').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ state.selectedTicketId = Number(btn.dataset.id);
+ state.selectedSessionId = null;
+ renderTicketDetail();
+ listEl.querySelectorAll('.ticket-row').forEach((r) => r.classList.remove('selected'));
+ btn.classList.add('selected');
+ });
});
- });
- if (state.selectedTicketId) await renderTicketDetail();
- else if (state.selectedSessionId) await renderSessionDetail();
- else detailEl.innerHTML = 'Selecione um ticket ou sessão do funil
';
+ if (state.selectedTicketId) await renderTicketDetail();
+ else if (state.selectedSessionId) await renderSessionDetail();
+ else detailEl.innerHTML = 'Selecione um ticket ou sessão do funil
';
+ }
} catch (e) {
listEl.innerHTML = `Erro: ${esc(e.message)}
`;
}
@@ -1929,6 +1967,10 @@ async function renderSessionDetail() {
async function renderTicketDetail() {
const detailEl = document.getElementById('ticket-detail');
if (!state.selectedTicketId) return;
+ if (window.TicketsDetailPanel) {
+ await TicketsDetailPanel.render(state.selectedTicketId, detailEl);
+ return;
+ }
detailEl.innerHTML = '';
try {
const t = await api(`/v1/desk/tickets/${state.selectedTicketId}`);
@@ -3720,7 +3762,10 @@ async function refresh(options = {}) {
if (state.view === 'overview') await renderOverview();
if (state.view === 'overview-home') await renderOverviewHome({ poll });
if (state.view === 'leads') await renderLeads();
- if (state.view === 'tickets') await renderTickets();
+ 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 === 'tenants') await renderTenants();
if (state.view === 'infra') await renderInfra();
diff --git a/projects/ops-desk/frontend/assets/desk-live-stub.js b/projects/ops-desk/frontend/assets/desk-live-stub.js
new file mode 100644
index 0000000..42c41a7
--- /dev/null
+++ b/projects/ops-desk/frontend/assets/desk-live-stub.js
@@ -0,0 +1,8 @@
+/** DeskLive stub — tickets funcionam sem módulo wizard-live activo */
+window.DeskLive = window.DeskLive || {
+ enabled() { return false; },
+ openTrail() {},
+ renderNavigationTab(_sid, el) {
+ if (el) el.innerHTML = 'Live Presence indisponível (módulo wizard-live).
';
+ },
+};
diff --git a/projects/ops-desk/frontend/assets/styles.css b/projects/ops-desk/frontend/assets/styles.css
index 5dc10f0..1a4b8f7 100644
--- a/projects/ops-desk/frontend/assets/styles.css
+++ b/projects/ops-desk/frontend/assets/styles.css
@@ -4226,3 +4226,126 @@ button.health-card {
.migration-gate-warning { color: #f59e0b; }
.billing-modal-backdrop { position: fixed; inset: 0; background: #0009; z-index: 900; display: flex; align-items: center; justify-content: center; }
.billing-modal { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; max-width: 480px; width: 90%; }
+.ticket-card-aside {
+ display: flex;
+ align-items: flex-start;
+ padding: 0.65rem 0.75rem 0.65rem 0;
+}
+.tickets-kpi {
+ cursor: pointer;
+ font: inherit;
+ color: inherit;
+ transition: border-color 0.15s, box-shadow 0.15s;
+}
+.tickets-kpi:hover { border-color: var(--accent); }
+.tickets-kpi.active {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 2px var(--accent-soft);
+ background: var(--accent-soft);
+}
+.tickets-queue-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.4rem;
+ margin-bottom: 0.65rem;
+}
+.tickets-queue-label {
+ font-size: 0.72rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--muted);
+ margin-right: 0.25rem;
+}
+.tickets-queue-chip {
+ border: 1px solid var(--border);
+ background: #fff;
+ border-radius: 999px;
+ padding: 0.25rem 0.65rem;
+ font-size: 0.74rem;
+ cursor: pointer;
+ font: inherit;
+ color: inherit;
+}
+.tickets-queue-chip:hover { border-color: var(--accent); }
+.tickets-queue-chip.active {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+}
+.tickets-queue-clear {
+ border: none;
+ background: transparent;
+ color: var(--muted);
+ font-size: 0.72rem;
+ cursor: pointer;
+ text-decoration: underline;
+}
+.ticket-detail-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ margin-bottom: 0.75rem;
+}
+.ticket-detail-tabs {
+ display: flex;
+ gap: 0.35rem;
+ border-bottom: 1px solid var(--border);
+ margin: 0.75rem 0 1rem;
+ padding-bottom: 0;
+}
+.ticket-detail-tab {
+ border: none;
+ background: transparent;
+ padding: 0.45rem 0.75rem;
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: var(--muted);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+}
+.ticket-detail-tab:hover { color: #2a2520; }
+.ticket-detail-tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+.ticket-detail-pane[hidden] { display: none !important; }
+.ticket-next-action {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ padding: 0.65rem 0.75rem;
+ border-radius: 8px;
+ margin-bottom: 0.5rem;
+ border: 1px solid var(--border);
+}
+.ticket-next-action--live { background: #ecfdf5; border-color: rgba(52, 211, 153, 0.4); }
+.ticket-next-action--stale { background: #fef3e8; border-color: rgba(234, 179, 8, 0.35); }
+.ticket-next-action--warn { background: #fef3e8; }
+.ticket-next-action--danger { background: #fde8e8; }
+.ticket-next-action--billing { background: #eff6ff; }
+.ticket-next-action--info { background: #f5f2ed; }
+.ticket-next-action-icon { font-size: 1.25rem; line-height: 1; }
+.ticket-next-action-body { flex: 1; min-width: 0; }
+.ticket-next-action-body strong { display: block; font-size: 0.85rem; }
+.ticket-next-action-body span { display: block; font-size: 0.76rem; color: var(--muted); }
+.ticket-live-presence-card {
+ padding: 0.75rem;
+ border: 1px solid rgba(52, 211, 153, 0.35);
+ border-radius: 8px;
+ background: #f0fdf8;
+ margin-bottom: 0.75rem;
+}
+.kv--compact dt { font-size: 0.72rem; }
+.kv--compact dd { font-size: 0.82rem; }
+.ticket-payload-details { margin-top: 1rem; }
+.ticket-payload-details summary {
+ cursor: pointer;
+ font-size: 0.8rem;
+ color: var(--muted);
+}
+@media (max-width: 1200px) {
\ No newline at end of file
diff --git a/projects/ops-desk/frontend/assets/tickets-detail-panel.js b/projects/ops-desk/frontend/assets/tickets-detail-panel.js
new file mode 100644
index 0000000..9fbaacc
--- /dev/null
+++ b/projects/ops-desk/frontend/assets/tickets-detail-panel.js
@@ -0,0 +1,467 @@
+/**
+ * Tickets Detail Panel — abas Resumo | Ao vivo | Funil + próxima acção contextual (P1)
+ */
+const TicketsDetailPanel = {
+ activeTab: 'resumo',
+ lastTicketId: null,
+ mirrorTimer: null,
+ mirrorSessionId: null,
+
+ stopMirrorPoll() {
+ if (this.mirrorTimer) {
+ clearInterval(this.mirrorTimer);
+ this.mirrorTimer = null;
+ }
+ this.mirrorSessionId = null;
+ },
+
+ parseUiState(presence, uiState) {
+ if (uiState && typeof uiState === 'object' && Object.keys(uiState).length) return uiState;
+ if (presence?.ui_state) {
+ try { return JSON.parse(presence.ui_state); } catch { return {}; }
+ }
+ return {};
+ },
+
+ mirrorHtml(observer) {
+ if (!observer) {
+ return 'Carregando espelho do wizard…
';
+ }
+ if (observer.is_assisting) {
+ return 'ASM ativo — o cliente está pausado. Use Reabrir wizard ASM para continuar no modo técnico.
';
+ }
+ const ui = this.parseUiState(observer.presence, observer.ui_state);
+ const step = observer.step_label || ui.step_label || observer.presence?.wizard_step || observer.funnel_stage || '—';
+ const domain = observer.domain || ui.domain || '—';
+ const errRaw = observer.client_error || ui.error;
+ const err = errRaw && (observer.is_assisting || !String(errRaw).includes('controle interno do suporte'))
+ ? errRaw
+ : null;
+ const modals = observer.modals || ui.modals || {};
+ const modalLines = [];
+ if (modals.support) modalLines.push('Modal Ajuda do Suporte aberto');
+ if (modals.dns) modalLines.push('Modal DNS aberto');
+ if (modals.dns_advanced) modalLines.push('Modal DNS avançado aberto');
+ const activity = (observer.activity_log || []).slice(-12).map((e) => {
+ const lvl = (e.level || 'info').toLowerCase();
+ return ` ${esc(e.message || e.msg || '')}`;
+ }).join('');
+ const infraPending = (observer.infra_status?.steps || []).filter((s) => !s.ok).slice(0, 4)
+ .map((s) => esc(s.label || s.id)).join(' · ');
+ return `
+
+
O técnico lê aqui o mesmo contexto do cliente antes de assumir a sessão.
+
+
+ Cliente no wizard
+ ${observer.presence?.wizard_step || step !== '—' ? `${esc(step)}` : ''}
+
+
+ - Passo
- ${esc(step)}
+ - Domínio
- ${esc(domain)}
+ ${observer.wizard_ticket_id ? `- Chamado wizard
${esc(observer.wizard_ticket_id)} ` : ''}
+ ${observer.client_note ? `- Nota do cliente
- ${esc(observer.client_note)}
` : ''}
+
+ ${err ? `
Erro na tela do cliente${esc(err)}
` : ''}
+ ${modalLines.length ? `
${modalLines.join(' · ')}
` : ''}
+ ${infraPending ? `
Infra pendente: ${infraPending}
` : ''}
+
+
Terminal / activity log (cliente)
+
${activity || '- Sem linhas recentes
'}
+
+
+
Atualizado ${fmtDate(observer.presence?.last_seen_at || new Date().toISOString())}
+
`;
+ },
+
+ async fetchObserverView(sessionId) {
+ return api(`/v1/assist/sessions/${encodeURIComponent(sessionId)}/observer-view`);
+ },
+
+ async refreshMirrorPane(detailEl, sessionId) {
+ const pane = detailEl.querySelector('[data-ticket-pane="espelho"]');
+ if (!pane || pane.hidden) return;
+ try {
+ const observer = await this.fetchObserverView(sessionId);
+ pane.innerHTML = this.mirrorHtml(observer);
+ } catch (e) {
+ pane.innerHTML = `Espelho indisponível: ${esc(e.message)}
`;
+ }
+ },
+
+ startMirrorPoll(detailEl, sessionId) {
+ this.stopMirrorPoll();
+ if (!sessionId) return;
+ this.mirrorSessionId = sessionId;
+ const tick = () => this.refreshMirrorPane(detailEl, sessionId);
+ tick();
+ this.mirrorTimer = setInterval(tick, 4000);
+ },
+
+ espelhoPaneHtml() {
+ return `
+
+
Carregando espelho do wizard…
+
`;
+ },
+
+ computeNextAction(t, assistMeta, carbonioBlock) {
+ const enriched = window.TicketsWorkspace?.enrichTicket
+ ? TicketsWorkspace.enrichTicket(t)
+ : t;
+ if (carbonioBlock?.status === 'pending') {
+ return {
+ tone: 'warn',
+ icon: '🔒',
+ title: 'Bloqueio Carbonio',
+ text: 'Remover conta órfã para o cliente repetir o passo',
+ cta: 'scroll-carbonio',
+ };
+ }
+ const assistStatus = normalizeAssistStatus(assistMeta?.assist_status || assistMeta?.ticket_status || t.status);
+ if (assistStatus === 'assisting' && typeof canAssist === 'function' && canAssist()) {
+ return {
+ tone: 'brand',
+ icon: '🖥️',
+ title: 'ASM ativo',
+ text: 'Fechou o wizard por engano? Reabra a sessão sem devolver ao cliente',
+ cta: 'resume-wizard',
+ };
+ }
+ if (enriched._live && typeof canAssist === 'function' && canAssist()
+ && assistMeta?.can_escalate && assistStatus !== 'assisting') {
+ return {
+ tone: 'live',
+ icon: '🟢',
+ title: 'Cliente online',
+ text: enriched._livePath
+ ? `Em ${enriched._livePath} — abra Espelho cliente e depois assuma`
+ : 'Abra a aba Espelho cliente para ver a mesma tela antes de assumir',
+ cta: 'takeover',
+ };
+ }
+ if (enriched._stale) {
+ return {
+ tone: 'stale',
+ icon: '⏸',
+ title: 'Sessão parada',
+ text: `Sem progresso há ~${enriched._idleHours}h — escalar ou contactar`,
+ cta: 'escalate',
+ };
+ }
+ if (enriched._unassigned && enriched.status === 'open') {
+ return {
+ tone: 'info',
+ icon: '👤',
+ title: 'Sem dono',
+ text: 'Ticket aberto sem técnico atribuído',
+ cta: 'takeover',
+ };
+ }
+ if (enriched._billing) {
+ return {
+ tone: 'billing',
+ icon: '💳',
+ title: 'Billing pendente',
+ text: 'Validar pagamento antes de prosseguir onboarding',
+ cta: null,
+ };
+ }
+ if (enriched._wazuh && (enriched.severity == null || enriched.severity >= 10)) {
+ return {
+ tone: 'danger',
+ icon: '⚠️',
+ title: 'Alerta Wazuh',
+ text: 'Investigar evento de segurança',
+ cta: null,
+ };
+ }
+ if (enriched.status === 'escalated') {
+ return {
+ tone: 'danger',
+ icon: '🚨',
+ title: 'Escalado',
+ text: 'Prioridade operacional — assumir ou resolver',
+ cta: 'takeover',
+ };
+ }
+ return null;
+ },
+
+ nextActionHtml(action) {
+ if (!action) return '';
+ const cta = action.cta === 'takeover'
+ ? ''
+ : action.cta === 'resume-wizard'
+ ? ''
+ : action.cta === 'escalate'
+ ? ''
+ : action.cta === 'scroll-carbonio'
+ ? ''
+ : '';
+ return `
+
+
${action.icon}
+
+ ${esc(action.title)}
+ ${esc(action.text)}
+
+ ${cta}
+
`;
+ },
+
+ tabsHtml({ t, sessionId, hasLive, hasFunil, hasEspelho }) {
+ const tabs = [
+ { id: 'resumo', label: 'Resumo' },
+ ];
+ if (hasEspelho) tabs.push({ id: 'espelho', label: 'Espelho cliente' });
+ if (hasLive) tabs.push({ id: 'live', label: 'Ao vivo' });
+ if (hasFunil) tabs.push({ id: 'funil', label: 'Funil' });
+ if (!tabs.some((x) => x.id === this.activeTab)) this.activeTab = 'resumo';
+ return `
+ `;
+ },
+
+ resumoHtml(t, { sessionId, assistMeta, carbonioBlock, timeline, timing }) {
+ const closeStatuses = ['open', 'escalated', 'assisting', 'resolved'];
+ return `
+
+
+ - Origem
- ${sourceBadge(t.source)}
+ - Domínio/Agente
- ${esc(t.domain || t.agent || '—')}
+ - Email
- ${esc(t.email || '—')}
+ ${typeof ticketFunnelKvHtml === 'function' ? ticketFunnelKvHtml(t) : `- Evento
- ${esc(t.event || '—')}
`}
+ ${t.assigned_to ? `- Atribuído
- ${esc(t.assigned_to)}
` : ''}
+ ${t.assisted_by ? `- Assistido por
- ${esc(t.assisted_by)}
` : ''}
+ ${t.client_paused ? '- Cliente
- pausado
' : ''}
+ ${t.ready_for_ops ? '- Ops
- ready for ops
' : ''}
+ ${t.severity != null ? `- Severidade
- ${severityBadge(t.severity)}
` : ''}
+ ${t.rule_id ? `- Regra
- ${esc(t.rule_id)}
` : ''}
+ ${t.description ? `- Descrição
- ${esc(t.description)}
` : ''}
+ ${t.desk_message ? `- Nota
- ${esc(t.desk_message)}
` : ''}
+ ${t.registration_role ? `- Perfil
- ${esc(roleLabel(t.registration_role))}
` : ''}
+ ${t.activation_url ? `- Ativar conta
- Abrir link de ativação
` : ''}
+ - Sessão/Alert ID
${esc(t.session_id || '—')}
+ ${t.wizard_ticket_id ? `- Chamado wizard
${esc(t.wizard_ticket_id)} ` : ''}
+ ${t.wizard_client_note ? `- Nota cliente
- ${esc(t.wizard_client_note)}
` : ''}
+ - Verificado
- ${t.account_verified ? 'Sim' : 'Não'}
+ - Revisão
- ${t.needs_review ? 'Necessária' : 'Não'}
+ - Criado
- ${fmtDate(t.created_at)}
+
+ ${sessionId && t.source === 'vm112-onboard' ? assistActionsHtml(sessionId, {
+ can_escalate: assistMeta?.can_escalate,
+ assist_status: assistMeta?.assist_status || assistMeta?.ticket_status,
+ ticket_status: assistMeta?.ticket_status || t.status,
+ client_paused: assistMeta?.ticket?.client_paused ?? t.client_paused,
+ assisted_by: assistMeta?.assisted_by,
+ actions: assistMeta?.actions,
+ }, assistMeta?._console || {}) : ''}
+ ${carbonioBlock ? `
${carbonioBlockPanelHtml(carbonioBlock)}
` : ''}
+
+ ${typeof canPatchTickets === 'function' && canPatchTickets()
+ ? (closeStatuses.includes(t.status)
+ ? ''
+ : '')
+ : ''}
+
+
+ Payload técnico
+ ${esc(JSON.stringify(t.payload, null, 2))}
+
+
`;
+ },
+
+ livePaneHtml(sessionId) {
+ const enriched = window.TicketsWorkspace?.context?.liveBySession?.[sessionId];
+ return `
+
+ ${enriched ? `
+
+
LIVE
+
+ - Path
${esc(enriched.path || '—')}
+ - Passo wizard
- ${esc(enriched.wizard_step || '—')}
+ - IP
- ${esc(enriched.ip || '—')}
+ - Último sinal
- ${fmtDate(enriched.last_seen_at || enriched.updated_at)}
+
+
` : '
Cliente offline neste momento.
'}
+
+
+
`;
+ },
+
+ funilPaneHtml(timeline, timing) {
+ return `
+
+ ${timeline?.length
+ ? `${phaseTimingCardHtml(timing, timeline)}${timelineHtml(timeline, timing, { compact: false })}`
+ : '
Sem eventos de funil para esta sessão.
'}
+
`;
+ },
+
+ bindTabs(detailEl) {
+ detailEl.querySelectorAll('[data-ticket-tab]').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ this.activeTab = btn.dataset.ticketTab;
+ detailEl.querySelectorAll('[data-ticket-tab]').forEach((b) => {
+ b.classList.toggle('active', b === btn);
+ b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
+ });
+ detailEl.querySelectorAll('[data-ticket-pane]').forEach((pane) => {
+ pane.hidden = pane.dataset.ticketPane !== this.activeTab;
+ });
+ if (this.activeTab === 'live') {
+ const sid = detailEl.dataset.sessionId;
+ const trail = detailEl.querySelector('#ticket-detail-live-trail');
+ if (sid && trail && window.DeskLive?.renderNavigationTab) {
+ DeskLive.renderNavigationTab(sid, trail);
+ }
+ }
+ if (this.activeTab === 'espelho') {
+ const sid = detailEl.dataset.sessionId;
+ if (sid) this.startMirrorPoll(detailEl, sid);
+ } else {
+ this.stopMirrorPoll();
+ }
+ if (this.activeTab === 'funil') bindLiveTimingClock(detailEl);
+ });
+ });
+ },
+
+ bindNextActions(detailEl, sessionId) {
+ detailEl.querySelector('[data-next-action="takeover"]')?.addEventListener('click', async (e) => {
+ const btn = e.currentTarget;
+ btn.disabled = true;
+ try {
+ await runAssistAction('takeover', sessionId);
+ await renderTickets();
+ } catch (err) {
+ alert(err.message || 'Falha ao assumir sessão');
+ } finally {
+ btn.disabled = false;
+ }
+ });
+ detailEl.querySelector('[data-next-action="resume-wizard"]')?.addEventListener('click', async (e) => {
+ const btn = e.currentTarget;
+ btn.disabled = true;
+ try {
+ await runAssistAction('resume-wizard', sessionId);
+ await renderTickets();
+ } catch (err) {
+ alert(err.message || 'Falha ao reabrir wizard ASM');
+ } finally {
+ btn.disabled = false;
+ }
+ });
+ detailEl.querySelector('[data-next-action="escalate"]')?.addEventListener('click', async (e) => {
+ const btn = e.currentTarget;
+ btn.disabled = true;
+ try {
+ await runAssistAction('escalate', sessionId);
+ await renderTickets();
+ } catch (err) {
+ alert(err.message || 'Falha ao escalar');
+ } finally {
+ btn.disabled = false;
+ }
+ });
+ detailEl.querySelector('[data-next-action="scroll-carbonio"]')?.addEventListener('click', () => {
+ this.activeTab = 'resumo';
+ detailEl.querySelector('[data-ticket-tab="resumo"]')?.click();
+ detailEl.querySelector('#ticket-carbonio-block')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ detailEl.querySelector('[data-open-live-trail]')?.addEventListener('click', (e) => {
+ if (window.DeskLive?.openTrail) DeskLive.openTrail(e.currentTarget.dataset.openLiveTrail);
+ });
+ },
+
+ async render(ticketId, detailEl) {
+ if (!ticketId || !detailEl) return;
+ this.stopMirrorPoll();
+ const isFreshTicket = this.lastTicketId !== ticketId;
+ if (isFreshTicket) {
+ this.activeTab = 'resumo';
+ this.lastTicketId = ticketId;
+ }
+ detailEl.innerHTML = '';
+ try {
+ const t = await api(`/v1/desk/tickets/${ticketId}`);
+ const sessionId = t.session_id || state.selectedSessionId;
+ const assistMeta = sessionId && t.source === 'vm112-onboard'
+ ? await loadAssistMeta(sessionId)
+ : null;
+ if (sessionId) state.selectedSessionId = sessionId;
+
+ let carbonioBlock = null;
+ if (t.source === 'vm112-onboard' && window.DeskModules?.isEnabled('carbonio-release')) {
+ try {
+ const byTicket = await api(`/v1/carbonio-blocks?ticket_id=${t.id}&status=pending&limit=1`);
+ carbonioBlock = byTicket.blocks?.[0] || null;
+ if (!carbonioBlock && sessionId) {
+ const bySession = await api(`/v1/carbonio-blocks?session_id=${encodeURIComponent(sessionId)}&status=pending&limit=1`);
+ carbonioBlock = bySession.blocks?.[0] || null;
+ }
+ } catch {
+ carbonioBlock = null;
+ }
+ }
+
+ const timeline = assistMeta?.timeline?.length
+ ? assistMeta.timeline
+ : (t.timeline || t.related_events || []);
+ const timing = assistMeta?.timing || t.timing;
+ const hasLive = Boolean(sessionId && t.source === 'vm112-onboard' && window.DeskLive?.enabled());
+ const hasEspelho = Boolean(sessionId && t.source === 'vm112-onboard');
+ const hasFunil = timeline.length > 0;
+ const assistStatus = normalizeAssistStatus(assistMeta?.assist_status || assistMeta?.ticket_status || t.status);
+ if (isFreshTicket && hasEspelho && hasLive && assistStatus !== 'assisting') {
+ this.activeTab = 'espelho';
+ }
+ const nextAction = this.computeNextAction(t, assistMeta, carbonioBlock);
+
+ detailEl.innerHTML = `
+
+
+ ${this.nextActionHtml(nextAction)}
+ ${this.tabsHtml({ t, sessionId, hasLive, hasFunil, hasEspelho })}
+ ${this.resumoHtml(t, { sessionId, assistMeta, carbonioBlock, timeline, timing })}
+ ${hasEspelho ? this.espelhoPaneHtml() : ''}
+ ${hasLive ? this.livePaneHtml(sessionId) : ''}
+ ${hasFunil ? this.funilPaneHtml(timeline, timing) : ''}
+
`;
+
+ this.bindTabs(detailEl);
+ this.bindNextActions(detailEl, sessionId);
+ if (sessionId && t.source === 'vm112-onboard') bindAssistActions(detailEl, sessionId);
+ bindCarbonioResolveForms(detailEl);
+ bindLiveTimingClock(detailEl);
+ if (this.activeTab === 'espelho' && sessionId) {
+ this.startMirrorPoll(detailEl, sessionId);
+ }
+ if (this.activeTab === 'live' && hasLive) {
+ const trail = detailEl.querySelector('#ticket-detail-live-trail');
+ if (trail) await DeskLive.renderNavigationTab(sessionId, trail);
+ }
+ detailEl.querySelector('[data-action="close"]')?.addEventListener('click', () => updateTicketStatus('closed'));
+ detailEl.querySelector('[data-action="open"]')?.addEventListener('click', () => updateTicketStatus('open'));
+ } catch (e) {
+ detailEl.innerHTML = ``;
+ }
+ },
+};
+
+window.TicketsDetailPanel = TicketsDetailPanel;
diff --git a/projects/ops-desk/frontend/assets/tickets-workspace.js b/projects/ops-desk/frontend/assets/tickets-workspace.js
new file mode 100644
index 0000000..8e1f2f5
--- /dev/null
+++ b/projects/ops-desk/frontend/assets/tickets-workspace.js
@@ -0,0 +1,497 @@
+/**
+ * Tickets Workspace — P0 lista (8 KPIs + 3 sinais) · P1 filas · P2 KPI click + softRefresh
+ * Arquitectura separada; app.js delega render aqui.
+ */
+const TicketsWorkspace = {
+ context: null,
+ searchQuery: '',
+ queueFilter: null,
+ _pageReady: false,
+ _listFingerprint: '',
+
+ QUEUE_CHIPS: [
+ { key: 'live', label: 'Live agora', icon: '🟢' },
+ { key: 'stale', label: 'Parados', icon: '⏸' },
+ { key: 'unassigned', label: 'Sem dono', icon: '👤' },
+ { key: 'billing', label: 'Billing', icon: '💳' },
+ { key: 'wazuh', label: 'Wazuh', icon: '⚠️' },
+ { key: 'escalated', label: 'Escalados', icon: '🚨' },
+ ],
+
+ async loadContext() {
+ const user = typeof getUser === 'function' ? getUser() : null;
+ 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,
+ user,
+ };
+ 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 live = sid ? ctx.liveBySession[sid] : null;
+ const funnel = sid ? ctx.funnelBySession[sid] : null;
+ const isActive = ['open', 'escalated', 'assisting', 'resolved'].includes(t.status);
+ const lastAt = funnel?.last_event_at || t.created_at;
+ let idleHours = 0;
+ if (lastAt) {
+ idleHours = (Date.now() - new Date(lastAt).getTime()) / 3600000;
+ }
+ const stale = funnel?.stale || (isActive && idleHours >= ctx.staleHours);
+ const stage = funnel?.current_stage || t.lead_funnel_stage || t.event;
+ return {
+ ...t,
+ _live: Boolean(live),
+ _livePath: (live?.path && !String(live.path).startsWith('/api/')) ? live.path : (funnel?.current_path && !String(funnel.current_path).startsWith('/api/') ? funnel.current_path : null),
+ _stage: stage,
+ _stageLabel: typeof FUNNEL_LABELS !== 'undefined' ? (FUNNEL_LABELS[stage] || stage) : stage,
+ _idleHours: Math.round(idleHours),
+ _stale: stale && isActive,
+ _unassigned: isActive && !t.assigned_to && !t.assisted_by,
+ _billing: Boolean(t.billing_state) || (t.subject || '').includes('[billing'),
+ _wazuh: t.source === 'wazuh' || t.event === 'wazuh.alert',
+ _carbonioHint: (t.subject || '').toLowerCase().includes('carbonio')
+ || (t.subject || '').toLowerCase().includes('account_exists'),
+ };
+ },
+
+ computeIndicators(tickets) {
+ const enriched = tickets.map((t) => this.enrichTicket(t));
+ const active = enriched.filter((t) => ['open', 'escalated', 'assisting', 'resolved'].includes(t.status));
+ return {
+ open: active.filter((t) => t.status === 'open').length,
+ assisting: enriched.filter((t) => t.status === 'assisting').length,
+ escalated: enriched.filter((t) => t.status === 'escalated').length,
+ live: enriched.filter((t) => t._live).length,
+ unassigned: active.filter((t) => t._unassigned).length,
+ stale: active.filter((t) => t._stale).length,
+ billing: enriched.filter((t) => t._billing).length,
+ wazuh: enriched.filter((t) => t._wazuh).length,
+ total: enriched.length,
+ };
+ },
+
+ indicatorsHtml(ind) {
+ const items = [
+ { key: 'open', label: 'Abertos', value: ind.open, tone: 'info' },
+ { key: 'assisting', label: 'Assistindo', value: ind.assisting, tone: 'brand' },
+ { key: 'escalated', label: 'Escalados', value: ind.escalated, tone: 'danger' },
+ { key: 'live', label: 'Live agora', value: ind.live, tone: 'live' },
+ { key: 'unassigned', label: 'Sem dono', value: ind.unassigned, tone: 'warn' },
+ { key: 'stale', label: 'Parados', value: ind.stale, tone: 'muted' },
+ { key: 'billing', label: 'Billing', value: ind.billing, tone: 'billing', icon: '💳' },
+ { key: 'wazuh', label: 'Wazuh', value: ind.wazuh, tone: 'security', icon: '⚠️' },
+ ];
+ return `
+
+ ${items.map((it) => `
+ `).join('')}
+
`;
+ },
+
+ queueChipsHtml() {
+ return `
+
+ Filas:
+ ${this.QUEUE_CHIPS.map((c) => `
+ `).join('')}
+ ${this.queueFilter ? '' : ''}
+
`;
+ },
+
+ phaseSignal(t) {
+ if (t._stale) return { text: `parado ${t._idleHours}h`, cls: 'stale' };
+ if (t._stageLabel && t._stageLabel !== t.event) {
+ const short = String(t._stageLabel).replace(/validado|aplicado|criada/gi, '').trim() || t._stageLabel;
+ return { text: short.slice(0, 28), cls: 'phase' };
+ }
+ if (t.event) return { text: String(t.event).replace('onboarding.', '').replace('.', ' '), cls: 'phase' };
+ return { text: '—', cls: 'muted' };
+ },
+
+ ticketCardHtml(t) {
+ const phase = this.phaseSignal(t);
+ const isOnboard = t.source === 'vm112-onboard' || t.event?.startsWith?.('onboarding');
+ const title = t.event === 'wazuh.alert'
+ ? esc(t.description || t.agent || t.subject)
+ : t.domain
+ ? esc(t.domain)
+ : isOnboard
+ ? `Onboarding · ${esc(t._stageLabel || 'VM112')}`
+ : esc(t.subject || `Ticket #${t.id}`);
+ const metaParts = [];
+ metaParts.push(`#${t.id}`);
+ if (t.wizard_ticket_id) metaParts.push(esc(t.wizard_ticket_id));
+ if (t.session_id) metaParts.push(sessionHashHtml(t.session_id, { full: false }));
+ if (t.email) metaParts.push(esc(t.email));
+ if (t.assigned_to || t.assisted_by) metaParts.push(esc(t.assisted_by || t.assigned_to));
+ metaParts.push(fmtDate(t.created_at));
+ const icons = [
+ t._billing ? '💳' : '',
+ t._carbonioHint ? '🔒' : '',
+ t._wazuh ? `${severityBadge(t.severity) || '⚠️'}` : '',
+ t.crm_track === 'lead' ? 'LEAD' : '',
+ ].filter(Boolean).join('');
+ const selected = state.selectedTicketId === t.id;
+ return `
+ `;
+ },
+
+ filterByQueue(tickets) {
+ if (!this.queueFilter) return tickets;
+ const rules = {
+ open: (t) => t.status === 'open',
+ assisting: (t) => t.status === 'assisting',
+ escalated: (t) => t.status === 'escalated',
+ live: (t) => t._live,
+ unassigned: (t) => t._unassigned,
+ stale: (t) => t._stale,
+ billing: (t) => t._billing,
+ wazuh: (t) => t._wazuh,
+ };
+ const fn = rules[this.queueFilter];
+ return fn ? tickets.filter(fn) : tickets;
+ },
+
+ filterBySearch(tickets) {
+ const raw = (this.searchQuery || '').trim();
+ if (!raw) return tickets;
+ const q = raw.toLowerCase();
+ const ticketIdQuery = q.replace(/^ticket\s*#?/, '').replace(/^#/, '').trim();
+
+ return tickets.filter((t) => {
+ if (/^\d+$/.test(ticketIdQuery) && String(t.id) === ticketIdQuery) return true;
+ const wiz = (t.wizard_ticket_id || '').toLowerCase();
+ if (wiz && (wiz === q || wiz.includes(q))) return true;
+ const liveIp = this.context?.liveBySession?.[t.session_id]?.client_ip;
+ const hay = [
+ t.id,
+ `#${t.id}`,
+ `ticket ${t.id}`,
+ t.subject,
+ t.domain,
+ t.email,
+ t.session_id,
+ t.agent,
+ t.description,
+ t.assigned_to,
+ t.assisted_by,
+ t.wizard_ticket_id,
+ liveIp,
+ ].filter(Boolean).join(' ').toLowerCase();
+ return hay.includes(q);
+ });
+ },
+
+ applyFilters(tickets) {
+ return this.filterBySearch(this.filterByQueue(tickets));
+ },
+
+ listFingerprint(tickets) {
+ return tickets.map((t) => [
+ t.id, t.status, t._live ? 1 : 0, t._stale ? 1 : 0, t._stage, t._idleHours, t._livePath || '',
+ ].join(':')).join('|');
+ },
+
+ setQueueFilter(key) {
+ this.queueFilter = this.queueFilter === key ? null : key;
+ const bar = document.getElementById('tickets-queue-bar');
+ if (bar) {
+ bar.innerHTML = this.queueChipsHtml();
+ this.bindQueueBar(bar);
+ }
+ this.syncQueueUi();
+ this.renderListOnly();
+ },
+
+ syncQueueUi() {
+ document.querySelectorAll('[data-ticket-kpi]').forEach((el) => {
+ el.classList.toggle('active', el.dataset.ticketKpi === this.queueFilter);
+ });
+ document.querySelectorAll('[data-ticket-queue]').forEach((el) => {
+ el.classList.toggle('active', el.dataset.ticketQueue === this.queueFilter);
+ });
+ const clearBtn = document.querySelector('[data-ticket-queue-clear]');
+ if (clearBtn) clearBtn.hidden = !this.queueFilter;
+ },
+
+ bindKpiStrip(strip) {
+ strip.querySelectorAll('[data-ticket-kpi]').forEach((el) => {
+ el.addEventListener('click', () => this.setQueueFilter(el.dataset.ticketKpi));
+ });
+ },
+
+ bindQueueBar(bar) {
+ bar.querySelectorAll('[data-ticket-queue]').forEach((el) => {
+ el.addEventListener('click', () => this.setQueueFilter(el.dataset.ticketQueue));
+ });
+ bar.querySelector('[data-ticket-queue-clear]')?.addEventListener('click', () => {
+ this.queueFilter = null;
+ this.syncQueueUi();
+ this.renderListOnly();
+ });
+ },
+
+ bindList(listEl) {
+ listEl.querySelectorAll('.ticket-card').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ state.selectedTicketId = Number(btn.dataset.id);
+ state.selectedSessionId = btn.dataset.session || null;
+ listEl.querySelectorAll('.ticket-card').forEach((r) => r.classList.remove('selected'));
+ btn.classList.add('selected');
+ if (window.TicketsDetailPanel) TicketsDetailPanel.render(state.selectedTicketId, document.getElementById('ticket-detail'));
+ else if (typeof renderTicketDetail === 'function') renderTicketDetail();
+ });
+ });
+ },
+
+ mountSearchToolbar() {
+ let bar = document.getElementById('tickets-search-bar');
+ if (!bar) {
+ const toolbar = document.querySelector('#view-tickets .toolbar');
+ if (!toolbar) return;
+ bar = document.createElement('div');
+ bar.id = 'tickets-search-bar';
+ bar.className = 'tickets-search-bar';
+ bar.innerHTML = `
+
+ Scan rápido · 3 sinais por card`;
+ toolbar.parentNode.insertBefore(bar, toolbar);
+ bar.querySelector('#tickets-search-input')?.addEventListener('input', (e) => {
+ this.searchQuery = e.target.value;
+ if (state.view === 'tickets') this.renderListOnly();
+ });
+ }
+ },
+
+ mountQueueBar() {
+ let bar = document.getElementById('tickets-queue-bar');
+ if (!bar) {
+ const view = document.getElementById('view-tickets');
+ const toolbar = view?.querySelector('.toolbar');
+ if (!toolbar) return;
+ bar = document.createElement('div');
+ bar.id = 'tickets-queue-bar';
+ toolbar.parentNode.insertBefore(bar, toolbar.nextSibling);
+ }
+ bar.innerHTML = this.queueChipsHtml();
+ this.bindQueueBar(bar);
+ },
+
+ updateKpiStrip(ind) {
+ const strip = document.getElementById('tickets-kpi-strip');
+ if (!strip) return;
+ const map = {
+ open: ind.open, assisting: ind.assisting, escalated: ind.escalated, live: ind.live,
+ unassigned: ind.unassigned, stale: ind.stale, billing: ind.billing, wazuh: ind.wazuh,
+ };
+ strip.querySelectorAll('[data-ticket-kpi]').forEach((el) => {
+ const key = el.dataset.ticketKpi;
+ const val = map[key];
+ if (val == null) return;
+ const valueEl = el.querySelector('.tickets-kpi-value');
+ if (valueEl) {
+ const icon = (key === 'billing' && val) ? '💳 ' : (key === 'wazuh' && val) ? '⚠️ ' : '';
+ valueEl.textContent = `${icon}${val}`;
+ }
+ });
+ },
+
+ patchLiveSignals(listEl, tickets) {
+ const byId = {};
+ for (const t of tickets) byId[t.id] = t;
+ listEl.querySelectorAll('.ticket-card').forEach((card) => {
+ const t = byId[Number(card.dataset.id)];
+ if (!t) return;
+ const wasLive = card.dataset.live === '1';
+ const isLive = t._live;
+ if (wasLive !== isLive) {
+ card.dataset.live = isLive ? '1' : '0';
+ card.classList.toggle('ticket-card--live', isLive);
+ const sig = card.querySelector('.ticket-signal--live');
+ if (sig) {
+ sig.classList.toggle('is-live', isLive);
+ sig.classList.toggle('is-offline', !isLive);
+ sig.lastChild.textContent = isLive ? 'LIVE' : 'offline';
+ }
+ }
+ card.dataset.stale = t._stale ? '1' : '0';
+ const phase = card.querySelector('.ticket-signal--phase');
+ if (phase) {
+ const p = this.phaseSignal(t);
+ phase.textContent = p.text;
+ phase.className = `ticket-signal ticket-signal--phase ticket-signal--${p.cls}`;
+ }
+ const pathEl = card.querySelector('[data-live-path]');
+ if (t._live && t._livePath) {
+ if (pathEl) pathEl.textContent = t._livePath;
+ else {
+ const main = card.querySelector('.ticket-card-main');
+ if (main) {
+ const span = document.createElement('span');
+ span.className = 'ticket-card-livepath';
+ span.dataset.livePath = '';
+ span.textContent = t._livePath;
+ main.appendChild(span);
+ }
+ }
+ } else if (pathEl) pathEl.remove();
+ });
+ },
+
+ renderListHtml(tickets) {
+ return tickets.length
+ ? `${tickets.map((t) => this.ticketCardHtml(t)).join('')}
`
+ : 'Nenhum ticket neste filtro
';
+ },
+
+ async renderListOnly() {
+ const listEl = document.getElementById('ticket-list');
+ if (!listEl || !state.tickets?.length) return;
+ const enriched = state.tickets.map((t) => this.enrichTicket(this.stripEnrichment(t)));
+ state.tickets = enriched;
+ const filtered = this.applyFilters(enriched);
+ const fp = this.listFingerprint(filtered);
+ if (fp === this._listFingerprint && listEl.querySelector('.ticket-card')) {
+ this.patchLiveSignals(listEl, filtered);
+ this.updateKpiStrip(this.computeIndicators(enriched));
+ return;
+ }
+ this._listFingerprint = fp;
+ listEl.innerHTML = this.renderListHtml(filtered);
+ this.bindList(listEl, filtered);
+ this.updateKpiStrip(this.computeIndicators(enriched));
+ },
+
+ async softRefresh() {
+ if (!this._pageReady || state.view !== 'tickets') return;
+ try {
+ await this.loadContext();
+ const enriched = (state.tickets || []).map((t) => this.enrichTicket(this.stripEnrichment(t)));
+ state.tickets = enriched;
+ const ind = this.computeIndicators(enriched);
+ this.updateKpiStrip(ind);
+ const listEl = document.getElementById('ticket-list');
+ if (listEl) {
+ const filtered = this.applyFilters(enriched);
+ const fp = this.listFingerprint(filtered);
+ if (fp === this._listFingerprint && listEl.querySelector('.ticket-card')) {
+ this.patchLiveSignals(listEl, filtered);
+ } else {
+ this._listFingerprint = fp;
+ const selectedId = state.selectedTicketId;
+ listEl.innerHTML = this.renderListHtml(filtered);
+ this.bindList(listEl, filtered);
+ if (selectedId) {
+ listEl.querySelector(`.ticket-card[data-id="${selectedId}"]`)?.classList.add('selected');
+ }
+ }
+ }
+ if (state.selectedTicketId && TicketsDetailPanel?.activeTab === 'live') {
+ const detailEl = document.getElementById('ticket-detail');
+ const sid = detailEl?.dataset?.sessionId || state.selectedSessionId;
+ const trail = detailEl?.querySelector('#ticket-detail-live-trail');
+ if (sid && trail && window.DeskLive?.renderNavigationTab) {
+ await DeskLive.renderNavigationTab(sid, trail);
+ }
+ }
+ } catch { /* ignore poll errors */ }
+ },
+
+ async renderPage({ listEl, detailEl, tickets }) {
+ this.mountSearchToolbar();
+ this.mountQueueBar();
+ await this.loadContext();
+ const enriched = tickets.map((t) => this.enrichTicket(t));
+ state.tickets = enriched;
+ const ind = this.computeIndicators(enriched);
+ const filtered = this.applyFilters(enriched);
+ this._listFingerprint = this.listFingerprint(filtered);
+
+ let strip = document.getElementById('tickets-kpi-strip');
+ if (!strip) {
+ strip = document.createElement('div');
+ strip.id = 'tickets-kpi-strip';
+ const view = document.getElementById('view-tickets');
+ const searchBar = document.getElementById('tickets-search-bar');
+ if (view && searchBar) view.insertBefore(strip, searchBar);
+ else if (view) {
+ const toolbar = view.querySelector('.toolbar');
+ if (toolbar) view.insertBefore(strip, toolbar);
+ }
+ }
+ strip.innerHTML = this.indicatorsHtml(ind);
+ this.bindKpiStrip(strip);
+
+ listEl.innerHTML = this.renderListHtml(filtered);
+ this.bindList(listEl, filtered);
+ this._pageReady = true;
+
+ if (state.selectedTicketId && window.TicketsDetailPanel) {
+ await TicketsDetailPanel.render(state.selectedTicketId, detailEl);
+ } else if (state.selectedTicketId && typeof renderTicketDetail === 'function') {
+ await renderTicketDetail();
+ } else if (state.selectedSessionId && typeof renderSessionDetail === 'function') {
+ await renderSessionDetail();
+ } else if (detailEl) {
+ detailEl.innerHTML = 'Selecione um ticket para ver detalhes
';
+ }
+ },
+};
+
+window.TicketsWorkspace = TicketsWorkspace;
diff --git a/projects/ops-desk/frontend/index.html b/projects/ops-desk/frontend/index.html
index ddfaffa..ccb3e0b 100644
--- a/projects/ops-desk/frontend/index.html
+++ b/projects/ops-desk/frontend/index.html
@@ -414,9 +414,13 @@
-
-
-
-
+
+
+
+
+
+
+
+