fix: wire TicketsWorkspace.renderPage in app.js (VM122 live)

This commit is contained in:
Ligbox Spec Hub 2026-06-19 19:30:39 +00:00
parent cb07e3bf88
commit 44dad0063c

View file

@ -84,9 +84,26 @@ const views = {
};
function roleLabel(role) {
return { super_admin: 'Super Admin', ops_lead: 'Chefe Ops', technician: 'Suporte', noc: 'NOC' }[role] || role;
return ROLE_LABELS[role] || role;
}
const ROLE_LABELS = {
super_admin: 'Super Admin',
ops_lead: 'Chefe Ops',
technician: 'Suporte',
noc: 'NOC',
sales_admin: 'Sales Admin',
sales_support: 'Sales Support',
finance: 'Financeiro',
marketing: 'Marketing',
seo: 'SEO',
developer: 'Developer',
devops: 'DevOps',
security_analyst: 'Segurança / SOC',
content_editor: 'Conteúdo / CMS',
agentic_operator: 'Operador Agentes IA',
};
function statusLabel(status) {
return {
pending: 'pendente',
@ -1914,7 +1931,7 @@ async function renderTickets(options = {}) {
state.tickets = tickets;
listEl.innerHTML = state.tickets.length
? state.tickets.map(ticketRowHtml).join('')
: '<p class="loading">Nenhum ticket nesto filtro</p>';
: '<p class="loading">Nenhum ticket neste filtro</p>';
listEl.querySelectorAll('.ticket-row').forEach((btn) => {
btn.addEventListener('click', () => {
state.selectedTicketId = Number(btn.dataset.id);
@ -2568,6 +2585,16 @@ function roleBadgeHtml(role) {
ops_lead: 'role-lead',
technician: 'role-tech',
noc: 'role-noc',
sales_admin: 'role-sales-admin',
sales_support: 'role-sales-support',
finance: 'role-finance',
marketing: 'role-marketing',
seo: 'role-seo',
developer: 'role-developer',
devops: 'role-devops',
security_analyst: 'role-security',
content_editor: 'role-content',
agentic_operator: 'role-agentic',
}[role] || 'role-default';
return `<span class="role-badge ${cls}">${esc(roleLabel(role))}</span>`;
}
@ -2582,14 +2609,34 @@ function mfaBadgeHtml(user) {
}
const ROLE_OPTIONS = [
{ value: 'super_admin', label: 'Super Admin' },
{ value: 'ops_lead', label: 'Chefe Ops' },
{ value: 'technician', label: 'Suporte' },
{ value: 'noc', label: 'NOC' },
{ value: 'super_admin', label: 'Super Admin', group: 'Ops' },
{ value: 'ops_lead', label: 'Chefe Ops', group: 'Ops' },
{ value: 'technician', label: 'Suporte', group: 'Ops' },
{ value: 'noc', label: 'NOC', group: 'Ops' },
{ value: 'sales_admin', label: 'Sales Admin', group: 'Comercial' },
{ value: 'sales_support', label: 'Sales Support', group: 'Comercial' },
{ value: 'finance', label: 'Financeiro', group: 'Negócio' },
{ value: 'marketing', label: 'Marketing', group: 'Negócio' },
{ value: 'seo', label: 'SEO', group: 'Negócio' },
{ value: 'developer', label: 'Developer', group: 'Plataforma' },
{ value: 'devops', label: 'DevOps', group: 'Plataforma' },
{ value: 'security_analyst', label: 'Segurança / SOC', group: 'Plataforma' },
{ value: 'content_editor', label: 'Conteúdo / CMS', group: 'Plataforma' },
{ value: 'agentic_operator', label: 'Operador Agentes IA', group: 'Plataforma' },
];
const ASSIGNABLE_ROLE_OPTIONS = ROLE_OPTIONS.filter((r) => r.value !== 'super_admin');
function registrationRoleSelectHtml(selected = 'technician') {
const groups = [...new Set(ASSIGNABLE_ROLE_OPTIONS.map((r) => r.group))];
return groups.map((group) => {
const opts = ASSIGNABLE_ROLE_OPTIONS.filter((r) => r.group === group)
.map((r) => `<option value="${r.value}" ${r.value === selected ? 'selected' : ''}>${esc(r.label)}</option>`)
.join('');
return `<optgroup label="${esc(group)}">${opts}</optgroup>`;
}).join('');
}
function roleSelectHtml(username, current, assignableOnly = true) {
const options = assignableOnly && current !== 'super_admin'
? ASSIGNABLE_ROLE_OPTIONS
@ -2961,7 +3008,7 @@ async function renderModules() {
}
}
const REG_ROLE_LABELS = { ops_lead: 'Chefe Ops (admin)', technician: 'Técnico', noc: 'NOC' };
const REG_ROLE_LABELS = ROLE_LABELS;
async function renderMessages() {
const el = document.getElementById('messages-content');
@ -2986,9 +3033,7 @@ async function renderMessages() {
</div>
<label>Perfil a atribuir
<select class="req-role">
<option value="ops_lead">Chefe Ops (admin)</option>
<option value="technician" selected>Técnico</option>
<option value="noc">NOC</option>
${registrationRoleSelectHtml('technician')}
</select>
</label>
<div class="actions" style="margin-top:0.75rem">
@ -3001,7 +3046,7 @@ async function renderMessages() {
<tr>
<td>${esc(r.email)}</td>
<td><span class="badge ${r.status === 'active' ? 'ok' : r.status === 'rejected' ? 'closed' : 'review'}">${esc(statusLabel(r.status))}</span></td>
<td>${esc(r.role || '—')}</td>
<td>${esc(r.role ? roleLabel(r.role) : '—')}</td>
<td>${fmtDate(r.updated_at || r.created_at)}</td>
</tr>`).join('');
el.innerHTML = `
@ -3356,6 +3401,90 @@ function showSocWebhookTestError(err) {
modal.setAttribute('aria-hidden', 'false');
}
function showOpenPanelTestResult(result) {
const modal = document.getElementById('soc-test-modal');
const title = document.getElementById('soc-test-modal-title');
const sub = document.getElementById('soc-test-modal-sub');
const body = document.getElementById('soc-test-modal-body');
if (!modal || !body) return;
const ok = result.ok === true;
title.textContent = ok ? 'OpenPanel API — confirmado' : 'OpenPanel API — falha';
sub.textContent = `Spec 028 · ${result.steps_passed || 0}/${result.steps_total || 0} passos · ${result.duration_sec || '—'}s`;
const steps = (result.steps || []).map((s) => `
<li class="badge ${s.ok ? 'ok' : 'escalated'}">
<strong>${esc(s.name)}</strong> ${esc(s.detail || (s.ok ? 'OK' : 'FAIL'))}
</li>`).join('');
body.innerHTML = `
<div class="soc-test-result">
<div class="soc-test-status ${ok ? 'soc-test-status--ok' : 'soc-test-status--fail'}">
<span class="soc-sev ${ok ? 'soc-sev--low' : 'soc-sev--high'}"></span>
${esc(result.message || (ok ? 'Multidomínio OK' : 'Falha'))}
</div>
<ul class="soc-alerts" style="list-style:none;padding:0;margin:0.75rem 0;display:flex;flex-direction:column;gap:0.35rem">${steps || '<li>—</li>'}</ul>
<p class="soc-test-hint">
Suite <code>openpanel-multidomain-api-confirm</code> provisiona 2 contas temporárias
(2 domínios na plataforma), valida listagem e remove. Pode executar quantas vezes quiser.
Script CLI: <code>scripts/test-openpanel-multidomain-api.sh</code>
</p>
<div class="soc-test-actions">
<button type="button" class="soc-btn soc-btn--ghost" data-close-soc-test-modal>Fechar</button>
</div>
</div>`;
body.querySelector('[data-close-soc-test-modal]')?.addEventListener('click', closeSocTestModal);
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
}
function showOpenPanelTestError(err) {
const modal = document.getElementById('soc-test-modal');
const title = document.getElementById('soc-test-modal-title');
const sub = document.getElementById('soc-test-modal-sub');
const body = document.getElementById('soc-test-modal-body');
if (!modal || !body) return;
const msg = err?.message || String(err);
const is403 = /403|permiss/i.test(msg);
title.textContent = 'OpenPanel API — erro';
sub.textContent = 'Teste não concluído';
body.innerHTML = `
<div class="soc-test-result">
<div class="soc-test-status soc-test-status--fail">
<span class="soc-sev soc-sev--high"></span>
${esc(msg)}
</div>
${is403 ? '<p class="soc-test-hint">Perfis: <strong>super_admin</strong>, <strong>devops</strong>, <strong>developer</strong>.</p>' : ''}
<div class="soc-test-actions">
<button type="button" class="soc-btn soc-btn--ghost" data-close-soc-test-modal>Fechar</button>
</div>
</div>`;
body.querySelector('[data-close-soc-test-modal]')?.addEventListener('click', closeSocTestModal);
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
}
async function runOpenPanelApiTest() {
const btn = document.getElementById('btn-test-openpanel-api');
const prevLabel = btn?.textContent;
if (btn) {
btn.disabled = true;
btn.textContent = 'Testando…';
}
try {
const r = await api('/v1/vm123/openpanel/test-confirm', { method: 'POST' });
showOpenPanelTestResult(r);
} catch (ex) {
showOpenPanelTestError(ex);
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = prevLabel || 'Testar multidomínio';
}
}
}
async function runWebhookIntegrationTest(refreshView) {
const btn = document.getElementById('soc-btn-test') || document.getElementById('btn-test-webhook');
const prevLabel = btn?.textContent;
@ -3726,6 +3855,13 @@ async function renderInfra() {
</div>
<p class="health-card-hint">Alerta se gap &gt; ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>
</div>
<div class="card">
<h3>OpenPanel API Re-engenharia Ligbox</h3>
<p class="health-card-hint">Spec 028 · VM123 bridge :18087 · multidomínio · conta temporária com cleanup automático.</p>
<div class="actions">
<button type="button" class="btn secondary" id="btn-test-openpanel-api">Testar multidomínio</button>
</div>
</div>
<div class="card">
<h3>VM112 Portal Onboard</h3>
<dl class="kv">
@ -3746,6 +3882,7 @@ async function renderInfra() {
</div>`;
document.getElementById('btn-refresh-health')?.addEventListener('click', () => renderInfra());
document.getElementById('btn-test-webhook')?.addEventListener('click', () => runWebhookIntegrationTest('infra'));
document.getElementById('btn-test-openpanel-api')?.addEventListener('click', () => runOpenPanelApiTest());
} catch (e) {
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
}