Standardize Infra process cards with uniform grid and detail modals.

Adopt proc-card pattern (fixed min-height, icons, spec labels, 2-line desc) per UX research; move purge forms and rich content to infra-process-modal; document catalog in Spec 033.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ligbox Spec Hub 2026-06-19 22:33:29 +00:00
parent 7dfdf5bc43
commit 68ec7bc901
4 changed files with 463 additions and 183 deletions

View file

@ -3825,17 +3825,190 @@ function infraKvHtml(items) {
).join('')}</dl>`;
}
const PROC_CARD_ICONS = {
soc: '📡',
openpanel: '🎛️',
purge: '🔐',
vm112: '🌐',
wazuh: '🛡️',
integrations: '🔗',
};
function procCardHtml(opts) {
const {
id,
icon,
accent = 'teal',
title,
spec,
desc,
statusLabel,
statusCls = 'review',
actions = [],
} = opts;
const acts = actions.map((a) =>
`<button type="button" class="btn ${a.primary ? 'secondary' : 'btn-ghost'} btn-sm" id="${esc(a.id)}">${esc(a.label)}</button>`
).join('');
return `
<article class="proc-card proc-card--${accent}" data-proc-id="${esc(id)}">
<span class="badge proc-card-badge ${statusCls}">${esc(statusLabel)}</span>
<header class="proc-card-head">
<span class="proc-card-icon" aria-hidden="true">${icon}</span>
<span class="proc-card-spec">${esc(spec)}</span>
</header>
<h3 class="proc-card-title">${esc(title)}</h3>
<p class="proc-card-desc">${desc}</p>
<footer class="proc-card-foot">${acts}</footer>
</article>`;
}
function closeInfraProcessModal() {
const modal = document.getElementById('infra-process-modal');
if (!modal) return;
modal.classList.add('hidden');
modal.setAttribute('aria-hidden', 'true');
}
function openInfraProcessModal(title, sub, bodyHtml) {
const modal = document.getElementById('infra-process-modal');
const titleEl = document.getElementById('infra-process-modal-title');
const subEl = document.getElementById('infra-process-modal-sub');
const body = document.getElementById('infra-process-modal-body');
if (!modal || !body) return;
if (titleEl) titleEl.textContent = title;
if (subEl) subEl.textContent = sub || '';
body.innerHTML = bodyHtml;
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
}
function bindInfraProcessModal() {
document.querySelectorAll('[data-close-infra-process-modal]').forEach((el) => {
el.addEventListener('click', closeInfraProcessModal);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeInfraProcessModal();
});
}
function openInfraProcessDetail(procId) {
const snap = state.infraSnapshot;
if (!snap) return;
const { vm112, wazuh, integrations, health, vm123Health, purgeMeta } = snap;
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 op = vm123Health?.openpanel || {};
const opOk = Boolean(op.ok);
const bridgeOk = Boolean(op.bridge);
const alerts = (health.alerts || []).map((a) =>
`<li class="badge ${a.level === 'critical' ? 'escalated' : 'assisting'}">${esc(a.message)}</li>`
).join('') || '<li class="muted">Nenhum alerta activo</li>';
if (procId === 'soc') {
openInfraProcessModal(
'SOC — Integração VM112',
'Webhook onboard · alertas de gap',
`${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')],
['Status integração', esc(health.status || '—')],
])}
<ul class="infra-alert-list">${alerts}</ul>
<div class="infra-actions">
<button type="button" class="btn secondary btn-sm" id="btn-test-webhook-modal">Testar webhook</button>
<button type="button" class="btn btn-ghost btn-sm" id="btn-refresh-health-modal">Atualizar</button>
</div>
<p class="infra-hint">Alerta se gap &gt; ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>`
);
document.getElementById('btn-test-webhook-modal')?.addEventListener('click', () => runWebhookIntegrationTest('infra'));
document.getElementById('btn-refresh-health-modal')?.addEventListener('click', () => {
closeInfraProcessModal();
renderInfra();
});
return;
}
if (procId === 'openpanel') {
openInfraProcessModal(
'OpenPanel API — Re-engenharia Ligbox',
'Spec 028 · VM123 bridge :18087',
`<p class="infra-hint">Multidomínio · conta temporária com cleanup automático.</p>
${vm123Health
? infraKvHtml([
['OpenPanel', opOk ? 'OK' : esc(op.error || 'offline')],
['Bridge API', bridgeOk ? 'OK' : 'offline'],
['Bridge URL', esc(op.bridge_url || '—')],
['VM123', vm123Health.ok ? 'OK' : esc(vm123Health.error || 'check')],
])
: '<p class="infra-hint">Status VM123 indisponível.</p>'}
<div class="infra-actions">
<button type="button" class="btn secondary btn-sm" id="btn-test-openpanel-modal">Testar multidomínio</button>
<a class="btn btn-ghost btn-sm" href="https://openpanel.ligbox.com.br" target="_blank" rel="noopener">OpenPanel UI</a>
</div>
<p class="infra-hint">Suite <code>openpanel-multidomain-api-confirm</code></p>`
);
document.getElementById('btn-test-openpanel-modal')?.addEventListener('click', () => runOpenPanelApiTest());
return;
}
if (procId === 'purge') {
openInfraProcessModal(
'Códigos purge — autorização extra',
'Spec 032 · domínios protegidos',
'<div id="purge-auth-modal-panel"><p class="loading">A carregar…</p></div>'
);
renderPurgeAuthPanel(document.getElementById('purge-auth-modal-panel'));
return;
}
if (procId === 'vm112') {
openInfraProcessModal(
'VM112 — Onboard Portal',
'HTTP health · serviço portal',
infraKvHtml([
['HTTP', String(vm112.http_status ?? '—')],
['Service', esc(vm112.vm112?.service || vm112.error || '—')],
['API integração', vmOk ? 'OK' : 'offline'],
])
);
return;
}
if (procId === 'wazuh') {
openInfraProcessModal(
'VM104 — Wazuh SOC',
'Spec 002 · API + webhook',
infraKvHtml([
['API HTTP', String(wazuh.http_status ?? '—')],
['Integração', 'webhook level ≥ 10 → VM122'],
['Status', wazuhOk ? 'online' : 'check'],
])
);
return;
}
if (procId === 'integrations') {
openInfraProcessModal(
'Integrações activas',
'Snapshot JSON · Desk API',
`<div class="infra-json-panel"><pre class="raw">${esc(JSON.stringify(integrations, null, 2))}</pre></div>`
);
}
}
async function renderInfra() {
const el = document.getElementById('infra-content');
el.innerHTML = '<p class="loading">Verificando…</p>';
try {
const [vm112, wazuh, integrations, health, vm123Health] = await Promise.all([
const [vm112, wazuh, integrations, health, vm123Health, purgeMeta] = await Promise.all([
api('/v1/infra/vm112/status'),
api('/v1/infra/wazuh/status'),
api('/v1/integrations'),
api('/v1/integrations/health'),
api('/v1/vm123/health').catch(() => null),
api('/v1/infra/purge-auth-domains').catch(() => ({ domains: [], can_generate: false })),
]);
state.infraSnapshot = { vm112, wazuh, integrations, health, vm123Health, purgeMeta };
const onboard = health.vm112_onboard || {};
const last = onboard.last_webhook;
const gap = onboard.gap_minutes != null ? `${Math.round(onboard.gap_minutes)} min` : '—';
@ -3845,124 +4018,110 @@ async function renderInfra() {
const opOk = Boolean(op.ok);
const bridgeOk = Boolean(op.bridge);
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 activo</li>';
const purgeDomains = purgeMeta.domains || [];
const purgeLabel = purgeDomains.length ? `${purgeDomains.length} domínio(s)` : 'sem extra-auth';
el.innerHTML = `
<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>
<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>
<span class="badge ${vmOk ? 'ok' : 'review'}">${vmOk ? 'online' : 'check'}</span>
</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>
<span class="badge ${wazuhOk ? 'ok' : 'review'}">${wazuhOk ? 'online' : 'check'}</span>
</div>
<div class="infra-hero-chip infra-hero-chip--openpanel">
<span class="infra-hero-dot ${opOk && bridgeOk ? '' : 'infra-hero-dot--warn'}" aria-hidden="true"></span>
<div class="infra-hero-body">
<strong>OpenPanel VM123</strong>
<span>Bridge ${bridgeOk ? 'OK' : 'check'} · ${esc(op.bridge_url || '10.10.10.123:18087')}</span>
</div>
<span class="badge ${opOk ? 'ok' : 'review'}">${opOk ? 'online' : 'check'}</span>
</div>
</div>
<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>
<p class="infra-hint">Alerta se gap &gt; ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>
</div>
</article>
<article class="ws-panel infra-panel infra-panel--wide infra-panel--featured" id="infra-openpanel-panel">
<div class="ws-panel-head ws-panel-head--orange">OpenPanel API Re-engenharia Ligbox · Spec 028</div>
<div class="ws-panel-body">
<p class="infra-hint">VM123 · bridge FOSS <code>:18087</code> · multidomínio · conta temporária com cleanup automático.</p>
${vm123Health
? infraKvHtml([
['OpenPanel', opOk ? 'OK' : esc(op.error || 'offline')],
['Bridge API', bridgeOk ? 'OK' : 'offline'],
['Bridge URL', esc(op.bridge_url || '—')],
['VM123', vm123Health.ok ? 'OK' : esc(vm123Health.error || 'check')],
])
: '<p class="infra-hint">Status VM123 indisponível — verifique API <code>/vm123/health</code>.</p>'}
<div class="infra-actions">
<button type="button" class="btn secondary btn-sm" id="btn-test-openpanel-api">Testar multidomínio</button>
<a class="btn btn-ghost btn-sm" href="https://openpanel.ligbox.com.br" target="_blank" rel="noopener">OpenPanel UI</a>
</div>
<p class="infra-hint">Suite <code>openpanel-multidomain-api-confirm</code> · CLI <code>scripts/test-openpanel-multidomain-api.sh</code></p>
</div>
</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--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>
<p class="infra-hint" style="margin:0">Processos de infraestrutura · cards uniformes · detalhes e formulários em modal (Spec 033).</p>
<div class="proc-grid">
${procCardHtml({
id: 'soc',
icon: PROC_CARD_ICONS.soc,
accent: 'teal',
title: 'SOC VM112',
spec: 'Webhook',
desc: `Gap ${gap} · ${last?.event ? esc(last.event) : 'sem eventos recentes'}`,
statusLabel: health.status || '—',
statusCls,
actions: [
{ id: 'btn-proc-soc-detail', label: 'Detalhes', primary: true },
{ id: 'btn-test-webhook', label: 'Testar', primary: false },
],
})}
${procCardHtml({
id: 'openpanel',
icon: PROC_CARD_ICONS.openpanel,
accent: 'orange',
title: 'OpenPanel API',
spec: 'Spec 028',
desc: `Bridge ${bridgeOk ? 'OK' : 'check'} · ${esc(op.bridge_url || '10.10.10.123:18087')}`,
statusLabel: opOk ? 'online' : 'check',
statusCls: opOk ? 'ok' : 'review',
actions: [
{ id: 'btn-proc-openpanel-detail', label: 'Detalhes', primary: true },
{ id: 'btn-test-openpanel-api', label: 'Testar', primary: false },
],
})}
${procCardHtml({
id: 'purge',
icon: PROC_CARD_ICONS.purge,
accent: 'rose',
title: 'Códigos purge',
spec: 'Spec 032',
desc: purgeDomains.length
? `Protegidos: ${purgeDomains.map((d) => esc(d)).join(', ')}`
: 'Geração de códigos para domínios com autorização extra',
statusLabel: purgeLabel,
statusCls: purgeDomains.length ? 'assisting' : 'open',
actions: [
{ id: 'btn-proc-purge-manage', label: 'Gerir códigos', primary: true },
],
})}
${procCardHtml({
id: 'vm112',
icon: PROC_CARD_ICONS.vm112,
accent: 'aqua',
title: 'VM112 Onboard',
spec: 'Portal',
desc: esc(vm112.vm112?.service || vm112.error || 'Portal de onboarding'),
statusLabel: vmOk ? 'online' : 'check',
statusCls: vmOk ? 'ok' : 'review',
actions: [
{ id: 'btn-proc-vm112-detail', label: 'Ver status', primary: true },
],
})}
${procCardHtml({
id: 'wazuh',
icon: PROC_CARD_ICONS.wazuh,
accent: 'slate',
title: 'Wazuh SOC',
spec: 'Spec 002',
desc: `API HTTP ${wazuh.http_status ?? '—'} · alertas nível ≥ 10`,
statusLabel: wazuhOk ? 'online' : 'check',
statusCls: wazuhOk ? 'ok' : 'review',
actions: [
{ id: 'btn-proc-wazuh-detail', label: 'Ver status', primary: true },
],
})}
${procCardHtml({
id: 'integrations',
icon: PROC_CARD_ICONS.integrations,
accent: 'violet',
title: 'Integrações',
spec: 'JSON',
desc: 'Snapshot das integrações configuradas no Desk',
statusLabel: 'activas',
statusCls: 'open',
actions: [
{ id: 'btn-proc-integrations-json', label: 'Ver JSON', primary: true },
],
})}
</div>
</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());
await renderPurgeAuthInfraPanel();
document.getElementById('btn-proc-soc-detail')?.addEventListener('click', () => openInfraProcessDetail('soc'));
document.getElementById('btn-proc-openpanel-detail')?.addEventListener('click', () => openInfraProcessDetail('openpanel'));
document.getElementById('btn-proc-purge-manage')?.addEventListener('click', () => openInfraProcessDetail('purge'));
document.getElementById('btn-proc-vm112-detail')?.addEventListener('click', () => openInfraProcessDetail('vm112'));
document.getElementById('btn-proc-wazuh-detail')?.addEventListener('click', () => openInfraProcessDetail('wazuh'));
document.getElementById('btn-proc-integrations-json')?.addEventListener('click', () => openInfraProcessDetail('integrations'));
} catch (e) {
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
}
}
async function renderPurgeAuthInfraPanel() {
const panel = document.getElementById('purge-auth-infra-panel');
async function renderPurgeAuthPanel(panel) {
if (!panel) return;
try {
const meta = await api('/v1/infra/purge-auth-domains');
@ -4060,6 +4219,10 @@ async function renderPurgeAuthInfraPanel() {
}
}
async function renderPurgeAuthInfraPanel() {
await renderPurgeAuthPanel(document.getElementById('purge-auth-infra-panel'));
}
async function refresh(options = {}) {
const { poll = false } = options;
await loadHealth();
@ -4143,6 +4306,7 @@ document.getElementById('btn-refresh')?.addEventListener('click', () => {
applyRoleNav();
DeskModules.applyVisibility();
bindOverviewModal();
bindInfraProcessModal();
bindTeamDrawerClose();
bindSocTestModal();
setView('dashboard');

View file

@ -4111,6 +4111,102 @@ button.health-card {
background: #0f172a;
}
/* Process cards — grid uniforme (Spec 033 § proc-card) */
.proc-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
align-items: stretch;
}
.proc-card {
position: relative;
display: flex;
flex-direction: column;
min-height: 168px;
padding: 16px;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: #fff;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.proc-card--teal { border-top: 3px solid #14b8a6; }
.proc-card--orange { border-top: 3px solid #f97316; }
.proc-card--rose { border-top: 3px solid #f43f5e; }
.proc-card--slate { border-top: 3px solid #64748b; }
.proc-card--violet { border-top: 3px solid #8b5cf6; }
.proc-card--aqua { border-top: 3px solid #06b6d4; }
.proc-card-head {
display: flex;
align-items: flex-start;
gap: 8px;
min-height: 32px;
}
.proc-card-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 1.05rem;
line-height: 1;
flex-shrink: 0;
}
.proc-card--teal .proc-card-icon { background: #ccfbf1; }
.proc-card--orange .proc-card-icon { background: #ffedd5; }
.proc-card--rose .proc-card-icon { background: #ffe4e6; }
.proc-card--slate .proc-card-icon { background: #f1f5f9; }
.proc-card--violet .proc-card-icon { background: #ede9fe; }
.proc-card--aqua .proc-card-icon { background: #cffafe; }
.proc-card-spec {
font-size: 0.62rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-left: auto;
text-align: right;
line-height: 1.2;
max-width: 42%;
}
.proc-card-badge {
position: absolute;
top: 12px;
right: 12px;
font-size: 0.62rem;
}
.proc-card-title {
margin: 8px 0 0;
font-size: 0.88rem;
font-weight: 600;
color: #0f172a;
line-height: 1.35;
padding-right: 3.5rem;
}
.proc-card-desc {
margin: 6px 0 0;
font-size: 0.75rem;
color: #64748b;
line-height: 1.45;
flex: 1;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.proc-card-foot {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f1f5f9;
}
.proc-card-foot .btn { flex: 1 1 auto; min-width: 0; }
@media (max-width: 480px) {
.proc-grid { grid-template-columns: 1fr; }
}
/* Spec 021 — Acesso utilizador (separado do VM112 Onboard) */
.ws-access-zone {
margin-bottom: 1.25rem;

View file

@ -413,14 +413,27 @@
</aside>
</div>
</div>
<div id="infra-process-modal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" data-close-infra-process-modal></div>
<div class="modal-panel modal-panel-lg" role="dialog" aria-modal="true" aria-labelledby="infra-process-modal-title">
<div class="modal-header">
<div>
<h3 id="infra-process-modal-title">Processo</h3>
<p id="infra-process-modal-sub" class="ticket-meta"></p>
</div>
<button type="button" class="btn btn-ghost btn-sm" data-close-infra-process-modal>Fechar</button>
</div>
<div id="infra-process-modal-body" class="modal-body"></div>
</div>
</div>
<script src="/assets/auth.js?v=20260619tickets3"></script>
<script src="/assets/modules.js?v=20260619tickets3"></script>
<script src="/assets/billing-ui.js?v=20260619tickets3"></script>
<script src="/assets/desk-live-stub.js?v=20260619tickets3"></script>
<script src="/assets/tickets-workspace.js?v=20260619tickets3"></script>
<script src="/assets/tickets-detail-panel.js?v=20260619tickets3"></script>
<script src="/assets/servicos.js?v=20260619tickets3"></script>
<script src="/assets/app.js?v=20260619tickets3"></script>
<script src="/assets/auth.js?v=20260619proc1"></script>
<script src="/assets/modules.js?v=20260619proc1"></script>
<script src="/assets/billing-ui.js?v=20260619proc1"></script>
<script src="/assets/desk-live-stub.js?v=20260619proc1"></script>
<script src="/assets/tickets-workspace.js?v=20260619proc1"></script>
<script src="/assets/tickets-detail-panel.js?v=20260619proc1"></script>
<script src="/assets/servicos.js?v=20260619proc1"></script>
<script src="/assets/app.js?v=20260619proc1"></script>
</body>
</html>

View file

@ -1,6 +1,7 @@
# Spec 033 — Desk Infra Console UI (ws-panel aqua/teal)
# Spec 033 — Desk Infra Console UI (process cards)
**Criado:** 2026-06-19
**Actualizado:** 2026-06-19 (padronização `proc-card`)
**Solicitado por:** Roger
**Prioridade:** P2 (UX operacional)
**Status:** ✅ Implementado (Desk VM122 frontend)
@ -11,9 +12,30 @@
## Resumo
Redesenho da página **Infraestrutura** do Desk: layout tipo wizard (`ws-panel` + gradientes teal/aqua/orange), chips de status no topo e painéis por integração — substituindo cards genéricos pouco legíveis.
Página **Infraestrutura** do Desk com **process cards** uniformes (`proc-card`): mesmo tamanho, tipografia, ícone, badge de status e acções no rodapé. Conteúdo rico (métricas, formulários, JSON) abre em **modal largo** (`#infra-process-modal`).
**Motivo:** cards antigos (`class="card"`) eram visualmente pobres e o painel OpenPanel (Spec 028) ficou pouco visível após o primeiro redesign.
**Motivo:** painéis `ws-panel` de alturas variadas quebravam o alinhamento visual; inputs inline (purge) inflavam cards.
---
## Referência de design (pesquisa)
Padrões adoptados de boas práticas de UI card (Material UI equal-height, UX Collective, CSS Grid auto-fill):
| Regra | Valor Desk |
|-------|------------|
| Grid | `repeat(auto-fill, minmax(220px, 1fr))` · gap **16px** (sistema 8pt) |
| Altura mínima card | **168px** · `flex-column` + `justify` implícito via footer |
| Padding card | **16px** |
| Título | **0.88rem** · weight 600 |
| Spec label | **0.62rem** uppercase · letter-spacing 0.05em |
| Descrição | **0.75rem** · **2 linhas** (`line-clamp: 2`) |
| Ícone | **32×32px** · fundo pastel por accent |
| Badge status | canto superior direito · classes `badge` existentes |
| Inputs / tabelas / JSON | **modal** `modal-panel-lg` — nunca no card |
| Testes (webhook, OpenPanel) | botão rápido no card **ou** no modal · resultado em `#soc-test-modal` |
**Alinhamento igual altura:** grid `align-items: stretch` + `min-height` fixo no card (não aspect-ratio — conteúdo operacional, não media).
---
@ -27,71 +49,60 @@ Redesenho da página **Infraestrutura** do Desk: layout tipo wizard (`ws-panel`
---
## Layout
## Catálogo de process cards (Infra)
### 1. Hero — chips de status (`infra-hero`)
| ID | Ícone | Título | Spec label | Accent | Status | Acções card |
|----|-------|--------|------------|--------|--------|-------------|
| `soc` | 📡 | SOC VM112 | Webhook | teal | `health.status` | Detalhes · Testar |
| `openpanel` | 🎛️ | OpenPanel API | Spec 028 | orange | online/check | Detalhes · Testar |
| `purge` | 🔐 | Códigos purge | Spec 032 | rose | N domínios | Gerir códigos → modal |
| `vm112` | 🌐 | VM112 Onboard | Portal | aqua | online/check | Ver status |
| `wazuh` | 🛡️ | Wazuh SOC | Spec 002 | slate | online/check | Ver status |
| `integrations` | 🔗 | Integrações | JSON | violet | activas | Ver JSON |
Quatro chips com dot + badge:
Mapa de ícones: constante `PROC_CARD_ICONS` em `app.js`.
| Chip | Fonte API |
|------|-----------|
| SOC integração | `GET /api/v1/integrations/health` |
| VM112 Portal | idem + `GET /api/v1/infra/vm112/status` |
| VM104 Wazuh | `GET /api/v1/infra/wazuh/status` |
| **OpenPanel VM123** | `GET /api/v1/vm123/health``openpanel` |
---
### 2. Grid — painéis `ws-panel` (`infra-grid`)
## Modais
| Painel | Cabeçalho CSS | Largura | Spec / função |
|--------|---------------|---------|----------------|
| SOC VM112 | `ws-panel-head--teal` | wide | Webhook + alertas |
| **OpenPanel API** | `ws-panel-head--orange` | **wide + featured** | **028** — teste multidomínio |
| Códigos purge | `ws-panel-head--rose` | normal | **032** — geração códigos |
| VM112 Onboard | teal | normal | HTTP / service |
| VM104 Wazuh | `ws-panel-head--slate` | normal | API SOC |
| Integrações JSON | `ws-panel-head--violet` | wide | dump `/integrations` |
| Modal | Uso |
|-------|-----|
| `#infra-process-modal` | Detalhes, formulário purge, JSON integrações |
| `#soc-test-modal` | Resultado testes webhook / OpenPanel multidomínio |
### 3. OpenPanel (Spec 028) — conteúdo obrigatório
Funções: `openInfraProcessModal()`, `openInfraProcessDetail(procId)`, `closeInfraProcessModal()`, `bindInfraProcessModal()`.
Painel **largura total**, classe `infra-panel--featured`:
- Título: `OpenPanel API — Re-engenharia Ligbox · Spec 028`
- Métricas: OpenPanel OK, Bridge API, Bridge URL, VM123 health
- Botões: **Testar multidomínio** (`POST /api/v1/vm123/openpanel/test-confirm`) · link OpenPanel UI
- Hint: suite `openpanel-multidomain-api-confirm` + script CLI
Modal de resultado: reutiliza `#soc-test-modal` (`showOpenPanelTestResult`).
### 4. Purge auth (Spec 032)
Sub-render: `renderPurgeAuthInfraPanel()``#purge-auth-infra-panel`
Snapshot API: `state.infraSnapshot` (reutilizado ao abrir detalhe sem re-fetch).
---
## APIs consumidas
| Endpoint | Uso na página |
|----------|----------------|
| `GET /api/v1/infra/vm112/status` | Chip + painel VM112 |
| `GET /api/v1/infra/wazuh/status` | Chip + painel Wazuh |
| `GET /api/v1/integrations` | JSON painel violet |
| `GET /api/v1/integrations/health` | Chip SOC + alertas |
| `GET /api/v1/vm123/health` | Chip + painel OpenPanel |
| `GET /api/v1/infra/purge-auth-domains` | Spec 032 panel |
| `GET/POST /api/v1/infra/purge-auth-codes` | Spec 032 panel |
| `POST /api/v1/vm123/openpanel/test-confirm` | Botão teste OpenPanel |
| Endpoint | Uso |
|----------|-----|
| `GET /api/v1/infra/vm112/status` | Card VM112 + modal |
| `GET /api/v1/infra/wazuh/status` | Card Wazuh + modal |
| `GET /api/v1/integrations` | Card + modal JSON |
| `GET /api/v1/integrations/health` | Card SOC + modal |
| `GET /api/v1/vm123/health` | Card OpenPanel + modal |
| `GET /api/v1/infra/purge-auth-domains` | Card purge + modal |
| `GET/POST /api/v1/infra/purge-auth-codes` | Modal purge (Spec 032) |
| `POST /api/v1/vm123/openpanel/test-confirm` | Teste OpenPanel |
---
## CSS (`frontend/assets/styles.css`)
Classes principais:
Classes **proc-card** (padrão reutilizável em outras páginas):
- `.infra-page`, `.infra-hero`, `.infra-hero-chip`, `.infra-hero-dot`
- `.infra-grid`, `.infra-panel`, `.infra-panel--wide`, `.infra-panel--featured`
- `.infra-kv` — métricas em grid
- `.infra-actions`, `.infra-hint`, `.infra-alert-list`
- Reutiliza `.ws-panel`, `.ws-panel-head--{teal|orange|rose|slate|violet}` (wizard/SOC)
- `.proc-grid`, `.proc-card`, `.proc-card--{teal|orange|rose|slate|violet|aqua}`
- `.proc-card-head`, `.proc-card-icon`, `.proc-card-spec`, `.proc-card-badge`
- `.proc-card-title`, `.proc-card-desc`, `.proc-card-foot`
Helpers Infra (modais): `.infra-kv`, `.infra-actions`, `.infra-hint`, `.purge-auth-form`
Helper JS: `procCardHtml(opts)` — gera HTML uniforme.
---
@ -99,36 +110,32 @@ Classes principais:
| Ficheiro | Função |
|----------|--------|
| `frontend/assets/app.js` | `renderInfra()`, `renderPurgeAuthInfraPanel()`, `infraKvHtml()` |
| `frontend/assets/styles.css` | Estilos Infra + purge auth form |
| `frontend/index.html` | `#infra-content` em `#view-infra` |
**Commits:** `41c0c2d` (layout inicial) · follow-up OpenPanel featured panel.
| `frontend/assets/app.js` | `renderInfra()`, `procCardHtml()`, modais Infra |
| `frontend/assets/styles.css` | `.proc-card` + Infra |
| `frontend/index.html` | `#infra-content`, `#infra-process-modal` |
---
## Critérios de aceitação
1. Infra mostra **4 chips** no topo (incl. OpenPanel VM123).
2. Painel OpenPanel é **largura total**, título Spec 028, métricas bridge e botão teste.
3. Botão «Testar multidomínio» abre modal com passos da suite 028.
4. Painel purge Spec 032 mantém formulário e tabela de códigos activos.
5. Layout responsivo: 1 coluna em mobile (`max-width: 900px`).
1. Grid com **6 cards** de **mesma altura mínima** e alinhamento visual.
2. Cada card: ícone + spec label + título + descrição (2 linhas) + badge + acções.
3. Purge: formulário e tabela **só no modal** «Gerir códigos».
4. OpenPanel / SOC: teste rápido no card; detalhes no modal.
5. Responsivo: 1 coluna em mobile (`max-width: 480px`).
---
## Fora de escopo
## Extensão futura
- Redesign `Infra 2` / SOC dark (`renderInfra2`)
- Dashboard widgets duplicando Infra
- Edição de integrações na UI
O sistema `proc-card` pode ser adoptado em **Serviços** (substituir `servicos-tile` gradualmente) e **Dashboard** — manter o mesmo catálogo de accents e ícones.
---
## Conclusão
A **Spec 033** documenta apenas a **construção visual e layout** da página Infra. Funcionalidades específicas:
**Spec 033** = layout e padronização visual da Infra. Funcionalidades:
- OpenPanel teste **Spec 028** (+ `CONFIRMACAO-TESTE-API.md`)
- OpenPanel → **Spec 028**
- Códigos purge → **Spec 032**
- Webhook SOC → Spec **001** / health integrations
- Webhook SOC → health integrations / Spec **001**