Improve Infra page cards with wizard-style ws-panel aqua/teal layout.

Redesign SOC, purge auth, and integration panels; fix servicos-tile-icon CSS typo.
This commit is contained in:
Ligbox Spec Hub 2026-06-19 22:25:17 +00:00
parent 2168d432f7
commit 41c0c2d428
2 changed files with 338 additions and 83 deletions

View file

@ -3819,6 +3819,12 @@ async function renderInfra2() {
}
}
function infraKvHtml(items) {
return `<dl class="infra-kv">${items.map(([label, value]) =>
`<div><dt>${esc(label)}</dt><dd>${value}</dd></div>`
).join('')}</dl>`;
}
async function renderInfra() {
const el = document.getElementById('infra-content');
el.innerHTML = '<p class="loading">Verificando…</p>';
@ -3832,58 +3838,97 @@ async function renderInfra() {
const onboard = health.vm112_onboard || {};
const last = onboard.last_webhook;
const gap = onboard.gap_minutes != null ? `${Math.round(onboard.gap_minutes)} min` : '—';
const vmOk = onboard.vm112_api?.reachable;
const wazuhOk = wazuh.http_status === 200;
const statusCls = health.status === 'ok' ? 'ok' : health.status === 'critical' ? 'escalated' : 'assisting';
const heroHealthDot = health.status === 'ok' ? '' : health.status === 'critical' ? 'infra-hero-dot--bad' : 'infra-hero-dot--warn';
const alerts = (health.alerts || []).map((a) =>
`<li class="badge ${a.level === 'critical' ? 'escalated' : 'assisting'}">${esc(a.message)}</li>`
).join('') || '<li class="muted">Nenhum alerta</li>';
).join('') || '<li class="muted">Nenhum alerta activo</li>';
el.innerHTML = `
<div class="card soc-panel">
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.75rem;flex-wrap:wrap">
<h3>SOC Integração VM112</h3>
<div class="infra-page">
<div class="infra-hero">
<div class="infra-hero-chip">
<span class="infra-hero-dot ${heroHealthDot}" aria-hidden="true"></span>
<div class="infra-hero-body">
<strong>SOC integração</strong>
<span>Webhook VM112 · gap ${gap}</span>
</div>
<span class="badge ${statusCls}">${esc(health.status || '—')}</span>
</div>
<dl class="kv">
<dt>Último webhook</dt><dd>${last ? esc(last.event) : ''}</dd>
<dt>Domínio</dt><dd>${last?.domain ? esc(last.domain) : ''}</dd>
<dt> quanto tempo</dt><dd>${gap}</dd>
<dt>VM112 API</dt><dd>${onboard.vm112_api?.reachable ? 'OK' : esc(onboard.vm112_api?.error || 'offline')}</dd>
</dl>
<ul class="soc-alerts" style="list-style:none;padding:0;margin:0.5rem 0;display:flex;flex-direction:column;gap:0.35rem">${alerts}</ul>
<div class="actions">
<button type="button" class="btn secondary" id="btn-test-webhook">Testar webhook</button>
<button type="button" class="btn secondary" id="btn-refresh-health">Atualizar</button>
<div class="infra-hero-chip">
<span class="infra-hero-dot ${vmOk ? '' : 'infra-hero-dot--warn'}" aria-hidden="true"></span>
<div class="infra-hero-body">
<strong>VM112 Portal</strong>
<span>${esc(vm112.vm112?.service || vm112.error || '—')}</span>
</div>
<p class="health-card-hint">Alerta se gap &gt; ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>
<span class="badge ${vmOk ? 'ok' : 'review'}">${vmOk ? 'online' : 'check'}</span>
</div>
<div class="card">
<h3>Códigos autorização purge</h3>
<p class="health-card-hint">Domínios protegidos (ex.: <code>myvexx.com</code>) exigem código único gerado aqui pelo <strong>root</strong> válido para conferência / purge em Serviços.</p>
<div id="purge-auth-infra-panel"><p class="loading">A carregar</p></div>
<div class="infra-hero-chip">
<span class="infra-hero-dot ${wazuhOk ? '' : 'infra-hero-dot--warn'}" aria-hidden="true"></span>
<div class="infra-hero-body">
<strong>VM104 Wazuh</strong>
<span>API HTTP ${wazuh.http_status ?? '—'}</span>
</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>
<span class="badge ${wazuhOk ? 'ok' : 'review'}">${wazuhOk ? 'online' : 'check'}</span>
</div>
</div>
<div class="card">
<h3>VM112 Portal Onboard</h3>
<dl class="kv">
<dt>HTTP</dt><dd>${vm112.http_status ?? ''}</dd>
<dt>Service</dt><dd>${esc(vm112.vm112?.service || vm112.error || '')}</dd>
</dl>
<div class="infra-grid">
<article class="ws-panel infra-panel infra-panel--wide">
<div class="ws-panel-head ws-panel-head--teal">SOC Integração VM112</div>
<div class="ws-panel-body">
${infraKvHtml([
['Último evento', last ? esc(last.event) : '—'],
['Domínio', last?.domain ? esc(last.domain) : '—'],
['Há quanto tempo', gap],
['VM112 API', vmOk ? 'OK' : esc(onboard.vm112_api?.error || 'offline')],
])}
<ul class="infra-alert-list">${alerts}</ul>
<div class="infra-actions">
<button type="button" class="btn secondary btn-sm" id="btn-test-webhook">Testar webhook</button>
<button type="button" class="btn secondary btn-sm" id="btn-refresh-health">Atualizar</button>
</div>
<div class="card">
<h3>VM104 Wazuh SOC</h3>
<dl class="kv">
<dt>API</dt><dd>${wazuh.http_status ?? ''}</dd>
<dt>Integração</dt><dd>webhook level 10 VM122</dd>
</dl>
<p class="infra-hint">Alerta se gap &gt; ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>
</div>
<div class="card">
<h3>Integrações ativas</h3>
</article>
<article class="ws-panel infra-panel">
<div class="ws-panel-head ws-panel-head--rose">Códigos purge · Spec 032</div>
<div class="ws-panel-body" id="purge-auth-infra-panel"><p class="loading">A carregar</p></div>
</article>
<article class="ws-panel infra-panel">
<div class="ws-panel-head ws-panel-head--orange">OpenPanel API</div>
<div class="ws-panel-body">
<p class="infra-hint">Spec 028 · VM123 bridge :18087 · multidomínio · conta temporária com cleanup.</p>
<div class="infra-actions">
<button type="button" class="btn secondary btn-sm" id="btn-test-openpanel-api">Testar multidomínio</button>
</div>
</div>
</article>
<article class="ws-panel infra-panel">
<div class="ws-panel-head ws-panel-head--teal">VM112 Onboard</div>
<div class="ws-panel-body">
${infraKvHtml([
['HTTP', String(vm112.http_status ?? '—')],
['Service', esc(vm112.vm112?.service || vm112.error || '—')],
])}
</div>
</article>
<article class="ws-panel infra-panel">
<div class="ws-panel-head ws-panel-head--slate">VM104 Wazuh SOC</div>
<div class="ws-panel-body">
${infraKvHtml([
['API HTTP', String(wazuh.http_status ?? '—')],
['Integração', 'webhook level ≥ 10 → VM122'],
])}
</div>
</article>
<article class="ws-panel infra-panel infra-panel--wide">
<div class="ws-panel-head ws-panel-head--violet">Integrações activas</div>
<div class="ws-panel-body infra-json-panel" style="padding:0">
<pre class="raw">${esc(JSON.stringify(integrations, null, 2))}</pre>
</div>
</article>
</div>
</div>`;
document.getElementById('btn-refresh-health')?.addEventListener('click', () => renderInfra());
document.getElementById('btn-test-webhook')?.addEventListener('click', () => runWebhookIntegrationTest('infra'));
@ -3899,43 +3944,57 @@ async function renderPurgeAuthInfraPanel() {
if (!panel) return;
try {
const meta = await api('/v1/infra/purge-auth-domains');
const domains = (meta.domains || []).map((d) => `<code>${esc(d)}</code>`).join(', ') || '—';
const domainChips = (meta.domains || []).map((d) =>
`<span class="infra-domain-chip">${esc(d)}</span>`
).join('') || '<span class="ticket-meta">Nenhum</span>';
const canGen = meta.can_generate && typeof canManageUsers === 'function' && canManageUsers();
let codesHtml = '';
if (canGen) {
const data = await api('/v1/infra/purge-auth-codes?limit=20');
const rows = (data.codes || []).map((c) => `
<tr>
<td>${esc(c.domain)}</td>
<td><code>${esc(c.domain)}</code></td>
<td>${esc(c.note || '—')}</td>
<td>${fmtDate(c.expires_at)}</td>
<td>${esc(c.created_by || '—')}</td>
</tr>`).join('');
codesHtml = `
<p class="infra-hint">Gere código com senha Root use na conferência antes do purge em Serviços.</p>
<div class="infra-domain-chips">${domainChips}</div>
<form id="purge-auth-generate-form" class="purge-auth-form">
<label>Domínio</label>
<input type="text" id="purge-auth-domain" class="cf-select" placeholder="myvexx.com" required />
<label>Nota (conferência / ticket)</label>
<input type="text" id="purge-auth-note" class="cf-select" placeholder="Autorizado em call com Roger" />
<label>Validade (horas)</label>
<input type="number" id="purge-auth-ttl" class="cf-select" value="24" min="1" max="168" />
<label>Senha Root</label>
<input type="password" id="purge-auth-root-pwd" class="cf-select" autocomplete="current-password" required />
<button type="submit" class="btn secondary">Gerar código</button>
<div>
<label for="purge-auth-domain">Domínio</label>
<input type="text" id="purge-auth-domain" placeholder="myvexx.com" required />
</div>
<div>
<label for="purge-auth-ttl">Validade (horas)</label>
<input type="number" id="purge-auth-ttl" value="24" min="1" max="168" />
</div>
<div style="grid-column:1/-1">
<label for="purge-auth-note">Nota (conferência / ticket)</label>
<input type="text" id="purge-auth-note" placeholder="Autorizado em call com Roger" />
</div>
<div style="grid-column:1/-1">
<label for="purge-auth-root-pwd">Senha Root</label>
<input type="password" id="purge-auth-root-pwd" autocomplete="current-password" required />
</div>
<button type="submit" class="btn secondary btn-sm">Gerar código</button>
</form>
<p id="purge-auth-gen-msg" class="ticket-meta"></p>
<div id="purge-auth-generated" class="purge-auth-generated hidden"></div>
<h4 style="margin-top:1rem">Códigos activos</h4>
<h4 style="margin:0.85rem 0 0.35rem;font-size:0.78rem;text-transform:uppercase;color:#64748b">Códigos activos</h4>
<div class="infra-table-wrap">
<table class="purge-history-table">
<thead><tr><th>Domínio</th><th>Nota</th><th>Expira</th><th>Por</th></tr></thead>
<tbody>${rows || '<tr><td colspan="4">Nenhum código activo</td></tr>'}</tbody>
</table>`;
</table>
</div>`;
} else {
codesHtml = '<p class="ticket-meta">Apenas <strong>super_admin (root)</strong> gera códigos. Peça o código ao root antes do purge em Serviços.</p>';
codesHtml = `
<div class="infra-domain-chips">${domainChips}</div>
<p class="infra-hint">Apenas <strong>super_admin (root)</strong> gera códigos. Peça o código ao root antes do purge em Serviços.</p>`;
}
panel.innerHTML = `
<p><strong>Domínios protegidos:</strong> ${domains}</p>
${codesHtml}`;
panel.innerHTML = codesHtml;
const form = panel.querySelector('#purge-auth-generate-form');
if (form) {
form.addEventListener('submit', async (ev) => {

View file

@ -3771,6 +3771,7 @@ button.health-card {
border-radius: 8px;
border: 1px dashed #cbd5e1;
}
.servicos-tile-icon {
font-size: 1.35rem;
margin-bottom: 0.35rem;
}
@ -3888,29 +3889,224 @@ button.health-card {
overflow-y: auto;
margin-top: 0.75rem;
}
.purge-auth-generated {
margin: 0.75rem 0;
padding: 0.75rem 1rem;
background: rgba(46, 125, 50, 0.12);
border-radius: 6px;
}
.purge-auth-generated.hidden { display: none; }
.purge-auth-code-display {
font-size: 1.25rem;
letter-spacing: 0.08em;
}
.purge-auth-form {
display: grid;
gap: 0.5rem;
max-width: 28rem;
margin-top: 0.75rem;
}
.purge-history-removed {
font-size: 0.85rem;
color: var(--muted, #6b7280);
max-width: 14rem;
}
/* Infra — layout tipo wizard (ws-panel + aqua/teal) */
.infra-page {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.infra-hero {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
align-items: stretch;
}
.infra-hero-chip {
flex: 1 1 200px;
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.75rem 1rem;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: linear-gradient(135deg, #f0fdfa 0%, #fff 55%);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.infra-hero-chip--alert {
background: linear-gradient(135deg, #fff7ed 0%, #fff 55%);
border-color: #fed7aa;
}
.infra-hero-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
background: #14b8a6;
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.25);
}
.infra-hero-dot--warn { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.25); }
.infra-hero-dot--bad { background: #ef4444; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.25); }
.infra-hero-body strong {
display: block;
font-size: 0.88rem;
color: #0f172a;
}
.infra-hero-body span {
display: block;
font-size: 0.75rem;
color: #64748b;
margin-top: 0.1rem;
}
.infra-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.infra-panel--wide { grid-column: 1 / -1; }
@media (max-width: 900px) {
.infra-grid { grid-template-columns: 1fr; }
}
.infra-panel .ws-panel-body { padding: 0.85rem 1rem; }
.infra-kv {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.65rem 1rem;
margin: 0;
}
.infra-kv div {
padding: 0.55rem 0.65rem;
border-radius: 8px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.infra-kv dt {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
margin: 0;
}
.infra-kv dd {
margin: 0.2rem 0 0;
font-size: 0.9rem;
font-weight: 600;
color: #0f172a;
font-variant-numeric: tabular-nums;
}
.infra-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.infra-hint {
margin: 0.65rem 0 0;
font-size: 0.78rem;
color: #64748b;
line-height: 1.45;
}
.infra-alert-list {
list-style: none;
margin: 0.65rem 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.infra-alert-list li {
padding: 0.45rem 0.65rem;
border-radius: 8px;
background: #f8fafc;
border: 1px solid #e2e8f0;
font-size: 0.82rem;
}
.ws-panel-head--aqua {
background: linear-gradient(135deg, #0891b2 0%, #22d3ee 45%, #2dd4bf 100%);
}
.ws-panel-head--slate {
background: linear-gradient(90deg, #334155, #64748b);
}
.ws-panel-head--violet {
background: linear-gradient(90deg, #6d28d9, #8b5cf6);
}
.ws-panel-head--rose {
background: linear-gradient(90deg, #be123c, #f43f5e);
}
.infra-domain-chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 0.75rem;
}
.infra-domain-chip {
font-size: 0.78rem;
padding: 0.25rem 0.55rem;
border-radius: 999px;
background: #ecfeff;
border: 1px solid #99f6e4;
color: #0f766e;
font-family: ui-monospace, monospace;
}
.purge-auth-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem 0.85rem;
margin-top: 0.5rem;
padding: 0.85rem;
border-radius: 10px;
background: linear-gradient(180deg, #f0fdfa 0%, #f8fafc 100%);
border: 1px solid #ccfbf1;
}
.purge-auth-form label {
display: block;
font-size: 0.72rem;
font-weight: 600;
color: #475569;
margin-bottom: 0.25rem;
}
.purge-auth-form input {
width: 100%;
padding: 0.5rem 0.65rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
font: inherit;
background: #fff;
}
.purge-auth-form input:focus {
outline: none;
border-color: #14b8a6;
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.2);
}
.purge-auth-form button {
grid-column: 1 / -1;
justify-self: start;
}
.purge-auth-generated {
margin: 0.75rem 0;
padding: 1rem 1.1rem;
border-radius: 12px;
background: linear-gradient(135deg, #ecfdf5 0%, #f0fdfa 100%);
border: 1px solid #6ee7b7;
text-align: center;
}
.purge-auth-generated.hidden { display: none; }
.purge-auth-code-display {
font-size: 1.5rem;
letter-spacing: 0.12em;
color: #0f766e;
font-weight: 700;
}
.infra-table-wrap {
margin-top: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
}
.infra-table-wrap table {
margin: 0;
}
.infra-table-wrap th {
background: #f1f5f9;
font-size: 0.68rem;
}
.infra-table-wrap td {
font-size: 0.82rem;
}
.infra-json-panel pre.raw {
margin: 0;
max-height: 240px;
border-radius: 0;
border: none;
background: #0f172a;
}
/* Spec 021 — Acesso utilizador (separado do VM112 Onboard) */
.ws-access-zone {
margin-bottom: 1.25rem;