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:
parent
7dfdf5bc43
commit
68ec7bc901
4 changed files with 463 additions and 183 deletions
|
|
@ -3825,17 +3825,190 @@ function infraKvHtml(items) {
|
||||||
).join('')}</dl>`;
|
).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 > ${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() {
|
async function renderInfra() {
|
||||||
const el = document.getElementById('infra-content');
|
const el = document.getElementById('infra-content');
|
||||||
el.innerHTML = '<p class="loading">Verificando…</p>';
|
el.innerHTML = '<p class="loading">Verificando…</p>';
|
||||||
try {
|
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/vm112/status'),
|
||||||
api('/v1/infra/wazuh/status'),
|
api('/v1/infra/wazuh/status'),
|
||||||
api('/v1/integrations'),
|
api('/v1/integrations'),
|
||||||
api('/v1/integrations/health'),
|
api('/v1/integrations/health'),
|
||||||
api('/v1/vm123/health').catch(() => null),
|
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 onboard = health.vm112_onboard || {};
|
||||||
const last = onboard.last_webhook;
|
const last = onboard.last_webhook;
|
||||||
const gap = onboard.gap_minutes != null ? `${Math.round(onboard.gap_minutes)} min` : '—';
|
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 opOk = Boolean(op.ok);
|
||||||
const bridgeOk = Boolean(op.bridge);
|
const bridgeOk = Boolean(op.bridge);
|
||||||
const statusCls = health.status === 'ok' ? 'ok' : health.status === 'critical' ? 'escalated' : 'assisting';
|
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 purgeDomains = purgeMeta.domains || [];
|
||||||
const alerts = (health.alerts || []).map((a) =>
|
const purgeLabel = purgeDomains.length ? `${purgeDomains.length} domínio(s)` : 'sem extra-auth';
|
||||||
`<li class="badge ${a.level === 'critical' ? 'escalated' : 'assisting'}">${esc(a.message)}</li>`
|
|
||||||
).join('') || '<li class="muted">Nenhum alerta activo</li>';
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="infra-page">
|
<div class="infra-page">
|
||||||
<div class="infra-hero">
|
<p class="infra-hint" style="margin:0">Processos de infraestrutura · cards uniformes · detalhes e formulários em modal (Spec 033).</p>
|
||||||
<div class="infra-hero-chip">
|
<div class="proc-grid">
|
||||||
<span class="infra-hero-dot ${heroHealthDot}" aria-hidden="true"></span>
|
${procCardHtml({
|
||||||
<div class="infra-hero-body">
|
id: 'soc',
|
||||||
<strong>SOC integração</strong>
|
icon: PROC_CARD_ICONS.soc,
|
||||||
<span>Webhook VM112 · gap ${gap}</span>
|
accent: 'teal',
|
||||||
</div>
|
title: 'SOC VM112',
|
||||||
<span class="badge ${statusCls}">${esc(health.status || '—')}</span>
|
spec: 'Webhook',
|
||||||
</div>
|
desc: `Gap ${gap} · ${last?.event ? esc(last.event) : 'sem eventos recentes'}`,
|
||||||
<div class="infra-hero-chip">
|
statusLabel: health.status || '—',
|
||||||
<span class="infra-hero-dot ${vmOk ? '' : 'infra-hero-dot--warn'}" aria-hidden="true"></span>
|
statusCls,
|
||||||
<div class="infra-hero-body">
|
actions: [
|
||||||
<strong>VM112 Portal</strong>
|
{ id: 'btn-proc-soc-detail', label: 'Detalhes', primary: true },
|
||||||
<span>${esc(vm112.vm112?.service || vm112.error || '—')}</span>
|
{ id: 'btn-test-webhook', label: 'Testar', primary: false },
|
||||||
</div>
|
],
|
||||||
<span class="badge ${vmOk ? 'ok' : 'review'}">${vmOk ? 'online' : 'check'}</span>
|
})}
|
||||||
</div>
|
${procCardHtml({
|
||||||
<div class="infra-hero-chip">
|
id: 'openpanel',
|
||||||
<span class="infra-hero-dot ${wazuhOk ? '' : 'infra-hero-dot--warn'}" aria-hidden="true"></span>
|
icon: PROC_CARD_ICONS.openpanel,
|
||||||
<div class="infra-hero-body">
|
accent: 'orange',
|
||||||
<strong>VM104 Wazuh</strong>
|
title: 'OpenPanel API',
|
||||||
<span>API HTTP ${wazuh.http_status ?? '—'}</span>
|
spec: 'Spec 028',
|
||||||
</div>
|
desc: `Bridge ${bridgeOk ? 'OK' : 'check'} · ${esc(op.bridge_url || '10.10.10.123:18087')}`,
|
||||||
<span class="badge ${wazuhOk ? 'ok' : 'review'}">${wazuhOk ? 'online' : 'check'}</span>
|
statusLabel: opOk ? 'online' : 'check',
|
||||||
</div>
|
statusCls: opOk ? 'ok' : 'review',
|
||||||
<div class="infra-hero-chip infra-hero-chip--openpanel">
|
actions: [
|
||||||
<span class="infra-hero-dot ${opOk && bridgeOk ? '' : 'infra-hero-dot--warn'}" aria-hidden="true"></span>
|
{ id: 'btn-proc-openpanel-detail', label: 'Detalhes', primary: true },
|
||||||
<div class="infra-hero-body">
|
{ id: 'btn-test-openpanel-api', label: 'Testar', primary: false },
|
||||||
<strong>OpenPanel VM123</strong>
|
],
|
||||||
<span>Bridge ${bridgeOk ? 'OK' : 'check'} · ${esc(op.bridge_url || '10.10.10.123:18087')}</span>
|
})}
|
||||||
</div>
|
${procCardHtml({
|
||||||
<span class="badge ${opOk ? 'ok' : 'review'}">${opOk ? 'online' : 'check'}</span>
|
id: 'purge',
|
||||||
</div>
|
icon: PROC_CARD_ICONS.purge,
|
||||||
</div>
|
accent: 'rose',
|
||||||
<div class="infra-grid">
|
title: 'Códigos purge',
|
||||||
<article class="ws-panel infra-panel infra-panel--wide">
|
spec: 'Spec 032',
|
||||||
<div class="ws-panel-head ws-panel-head--teal">SOC — Integração VM112</div>
|
desc: purgeDomains.length
|
||||||
<div class="ws-panel-body">
|
? `Protegidos: ${purgeDomains.map((d) => esc(d)).join(', ')}`
|
||||||
${infraKvHtml([
|
: 'Geração de códigos para domínios com autorização extra',
|
||||||
['Último evento', last ? esc(last.event) : '—'],
|
statusLabel: purgeLabel,
|
||||||
['Domínio', last?.domain ? esc(last.domain) : '—'],
|
statusCls: purgeDomains.length ? 'assisting' : 'open',
|
||||||
['Há quanto tempo', gap],
|
actions: [
|
||||||
['VM112 API', vmOk ? 'OK' : esc(onboard.vm112_api?.error || 'offline')],
|
{ id: 'btn-proc-purge-manage', label: 'Gerir códigos', primary: true },
|
||||||
])}
|
],
|
||||||
<ul class="infra-alert-list">${alerts}</ul>
|
})}
|
||||||
<div class="infra-actions">
|
${procCardHtml({
|
||||||
<button type="button" class="btn secondary btn-sm" id="btn-test-webhook">Testar webhook</button>
|
id: 'vm112',
|
||||||
<button type="button" class="btn secondary btn-sm" id="btn-refresh-health">Atualizar</button>
|
icon: PROC_CARD_ICONS.vm112,
|
||||||
</div>
|
accent: 'aqua',
|
||||||
<p class="infra-hint">Alerta se gap > ${health.webhook_gap_alert_minutes || 15} min sem eventos VM112.</p>
|
title: 'VM112 Onboard',
|
||||||
</div>
|
spec: 'Portal',
|
||||||
</article>
|
desc: esc(vm112.vm112?.service || vm112.error || 'Portal de onboarding'),
|
||||||
<article class="ws-panel infra-panel infra-panel--wide infra-panel--featured" id="infra-openpanel-panel">
|
statusLabel: vmOk ? 'online' : 'check',
|
||||||
<div class="ws-panel-head ws-panel-head--orange">OpenPanel API — Re-engenharia Ligbox · Spec 028</div>
|
statusCls: vmOk ? 'ok' : 'review',
|
||||||
<div class="ws-panel-body">
|
actions: [
|
||||||
<p class="infra-hint">VM123 · bridge FOSS <code>:18087</code> · multidomínio · conta temporária com cleanup automático.</p>
|
{ id: 'btn-proc-vm112-detail', label: 'Ver status', primary: true },
|
||||||
${vm123Health
|
],
|
||||||
? infraKvHtml([
|
})}
|
||||||
['OpenPanel', opOk ? 'OK' : esc(op.error || 'offline')],
|
${procCardHtml({
|
||||||
['Bridge API', bridgeOk ? 'OK' : 'offline'],
|
id: 'wazuh',
|
||||||
['Bridge URL', esc(op.bridge_url || '—')],
|
icon: PROC_CARD_ICONS.wazuh,
|
||||||
['VM123', vm123Health.ok ? 'OK' : esc(vm123Health.error || 'check')],
|
accent: 'slate',
|
||||||
])
|
title: 'Wazuh SOC',
|
||||||
: '<p class="infra-hint">Status VM123 indisponível — verifique API <code>/vm123/health</code>.</p>'}
|
spec: 'Spec 002',
|
||||||
<div class="infra-actions">
|
desc: `API HTTP ${wazuh.http_status ?? '—'} · alertas nível ≥ 10`,
|
||||||
<button type="button" class="btn secondary btn-sm" id="btn-test-openpanel-api">Testar multidomínio</button>
|
statusLabel: wazuhOk ? 'online' : 'check',
|
||||||
<a class="btn btn-ghost btn-sm" href="https://openpanel.ligbox.com.br" target="_blank" rel="noopener">OpenPanel UI</a>
|
statusCls: wazuhOk ? 'ok' : 'review',
|
||||||
</div>
|
actions: [
|
||||||
<p class="infra-hint">Suite <code>openpanel-multidomain-api-confirm</code> · CLI <code>scripts/test-openpanel-multidomain-api.sh</code></p>
|
{ id: 'btn-proc-wazuh-detail', label: 'Ver status', primary: true },
|
||||||
</div>
|
],
|
||||||
</article>
|
})}
|
||||||
<article class="ws-panel infra-panel">
|
${procCardHtml({
|
||||||
<div class="ws-panel-head ws-panel-head--rose">Códigos purge · Spec 032</div>
|
id: 'integrations',
|
||||||
<div class="ws-panel-body" id="purge-auth-infra-panel"><p class="loading">A carregar…</p></div>
|
icon: PROC_CARD_ICONS.integrations,
|
||||||
</article>
|
accent: 'violet',
|
||||||
<article class="ws-panel infra-panel">
|
title: 'Integrações',
|
||||||
<div class="ws-panel-head ws-panel-head--teal">VM112 — Onboard</div>
|
spec: 'JSON',
|
||||||
<div class="ws-panel-body">
|
desc: 'Snapshot das integrações configuradas no Desk',
|
||||||
${infraKvHtml([
|
statusLabel: 'activas',
|
||||||
['HTTP', String(vm112.http_status ?? '—')],
|
statusCls: 'open',
|
||||||
['Service', esc(vm112.vm112?.service || vm112.error || '—')],
|
actions: [
|
||||||
])}
|
{ id: 'btn-proc-integrations-json', label: 'Ver JSON', primary: true },
|
||||||
</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>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.getElementById('btn-refresh-health')?.addEventListener('click', () => renderInfra());
|
|
||||||
document.getElementById('btn-test-webhook')?.addEventListener('click', () => runWebhookIntegrationTest('infra'));
|
document.getElementById('btn-test-webhook')?.addEventListener('click', () => runWebhookIntegrationTest('infra'));
|
||||||
document.getElementById('btn-test-openpanel-api')?.addEventListener('click', () => runOpenPanelApiTest());
|
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) {
|
} catch (e) {
|
||||||
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
|
el.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPurgeAuthInfraPanel() {
|
async function renderPurgeAuthPanel(panel) {
|
||||||
const panel = document.getElementById('purge-auth-infra-panel');
|
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
try {
|
try {
|
||||||
const meta = await api('/v1/infra/purge-auth-domains');
|
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 = {}) {
|
async function refresh(options = {}) {
|
||||||
const { poll = false } = options;
|
const { poll = false } = options;
|
||||||
await loadHealth();
|
await loadHealth();
|
||||||
|
|
@ -4143,6 +4306,7 @@ document.getElementById('btn-refresh')?.addEventListener('click', () => {
|
||||||
applyRoleNav();
|
applyRoleNav();
|
||||||
DeskModules.applyVisibility();
|
DeskModules.applyVisibility();
|
||||||
bindOverviewModal();
|
bindOverviewModal();
|
||||||
|
bindInfraProcessModal();
|
||||||
bindTeamDrawerClose();
|
bindTeamDrawerClose();
|
||||||
bindSocTestModal();
|
bindSocTestModal();
|
||||||
setView('dashboard');
|
setView('dashboard');
|
||||||
|
|
|
||||||
|
|
@ -4111,6 +4111,102 @@ button.health-card {
|
||||||
background: #0f172a;
|
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) */
|
/* Spec 021 — Acesso utilizador (separado do VM112 Onboard) */
|
||||||
.ws-access-zone {
|
.ws-access-zone {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
|
|
|
||||||
|
|
@ -413,14 +413,27 @@
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</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/auth.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/modules.js?v=20260619tickets3"></script>
|
<script src="/assets/modules.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/billing-ui.js?v=20260619tickets3"></script>
|
<script src="/assets/billing-ui.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/desk-live-stub.js?v=20260619tickets3"></script>
|
<script src="/assets/desk-live-stub.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/tickets-workspace.js?v=20260619tickets3"></script>
|
<script src="/assets/tickets-workspace.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/tickets-detail-panel.js?v=20260619tickets3"></script>
|
<script src="/assets/tickets-detail-panel.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/servicos.js?v=20260619tickets3"></script>
|
<script src="/assets/servicos.js?v=20260619proc1"></script>
|
||||||
<script src="/assets/app.js?v=20260619tickets3"></script>
|
<script src="/assets/app.js?v=20260619proc1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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
|
**Criado:** 2026-06-19
|
||||||
|
**Actualizado:** 2026-06-19 (padronização `proc-card`)
|
||||||
**Solicitado por:** Roger
|
**Solicitado por:** Roger
|
||||||
**Prioridade:** P2 (UX operacional)
|
**Prioridade:** P2 (UX operacional)
|
||||||
**Status:** ✅ Implementado (Desk VM122 frontend)
|
**Status:** ✅ Implementado (Desk VM122 frontend)
|
||||||
|
|
@ -11,9 +12,30 @@
|
||||||
|
|
||||||
## Resumo
|
## 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 |
|
| Modal | Uso |
|
||||||
|--------|---------------|---------|----------------|
|
|-------|-----|
|
||||||
| SOC VM112 | `ws-panel-head--teal` | wide | Webhook + alertas |
|
| `#infra-process-modal` | Detalhes, formulário purge, JSON integrações |
|
||||||
| **OpenPanel API** | `ws-panel-head--orange` | **wide + featured** | **028** — teste multidomínio |
|
| `#soc-test-modal` | Resultado testes webhook / OpenPanel 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` |
|
|
||||||
|
|
||||||
### 3. OpenPanel (Spec 028) — conteúdo obrigatório
|
Funções: `openInfraProcessModal()`, `openInfraProcessDetail(procId)`, `closeInfraProcessModal()`, `bindInfraProcessModal()`.
|
||||||
|
|
||||||
Painel **largura total**, classe `infra-panel--featured`:
|
Snapshot API: `state.infraSnapshot` (reutilizado ao abrir detalhe sem re-fetch).
|
||||||
|
|
||||||
- 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`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## APIs consumidas
|
## APIs consumidas
|
||||||
|
|
||||||
| Endpoint | Uso na página |
|
| Endpoint | Uso |
|
||||||
|----------|----------------|
|
|----------|-----|
|
||||||
| `GET /api/v1/infra/vm112/status` | Chip + painel VM112 |
|
| `GET /api/v1/infra/vm112/status` | Card VM112 + modal |
|
||||||
| `GET /api/v1/infra/wazuh/status` | Chip + painel Wazuh |
|
| `GET /api/v1/infra/wazuh/status` | Card Wazuh + modal |
|
||||||
| `GET /api/v1/integrations` | JSON painel violet |
|
| `GET /api/v1/integrations` | Card + modal JSON |
|
||||||
| `GET /api/v1/integrations/health` | Chip SOC + alertas |
|
| `GET /api/v1/integrations/health` | Card SOC + modal |
|
||||||
| `GET /api/v1/vm123/health` | Chip + painel OpenPanel |
|
| `GET /api/v1/vm123/health` | Card OpenPanel + modal |
|
||||||
| `GET /api/v1/infra/purge-auth-domains` | Spec 032 panel |
|
| `GET /api/v1/infra/purge-auth-domains` | Card purge + modal |
|
||||||
| `GET/POST /api/v1/infra/purge-auth-codes` | Spec 032 panel |
|
| `GET/POST /api/v1/infra/purge-auth-codes` | Modal purge (Spec 032) |
|
||||||
| `POST /api/v1/vm123/openpanel/test-confirm` | Botão teste OpenPanel |
|
| `POST /api/v1/vm123/openpanel/test-confirm` | Teste OpenPanel |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CSS (`frontend/assets/styles.css`)
|
## 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`
|
- `.proc-grid`, `.proc-card`, `.proc-card--{teal|orange|rose|slate|violet|aqua}`
|
||||||
- `.infra-grid`, `.infra-panel`, `.infra-panel--wide`, `.infra-panel--featured`
|
- `.proc-card-head`, `.proc-card-icon`, `.proc-card-spec`, `.proc-card-badge`
|
||||||
- `.infra-kv` — métricas em grid
|
- `.proc-card-title`, `.proc-card-desc`, `.proc-card-foot`
|
||||||
- `.infra-actions`, `.infra-hint`, `.infra-alert-list`
|
|
||||||
- Reutiliza `.ws-panel`, `.ws-panel-head--{teal|orange|rose|slate|violet}` (wizard/SOC)
|
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 |
|
| Ficheiro | Função |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| `frontend/assets/app.js` | `renderInfra()`, `renderPurgeAuthInfraPanel()`, `infraKvHtml()` |
|
| `frontend/assets/app.js` | `renderInfra()`, `procCardHtml()`, modais Infra |
|
||||||
| `frontend/assets/styles.css` | Estilos Infra + purge auth form |
|
| `frontend/assets/styles.css` | `.proc-card` + Infra |
|
||||||
| `frontend/index.html` | `#infra-content` em `#view-infra` |
|
| `frontend/index.html` | `#infra-content`, `#infra-process-modal` |
|
||||||
|
|
||||||
**Commits:** `41c0c2d` (layout inicial) · follow-up OpenPanel featured panel.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Critérios de aceitação
|
## Critérios de aceitação
|
||||||
|
|
||||||
1. Infra mostra **4 chips** no topo (incl. OpenPanel VM123).
|
1. Grid com **6 cards** de **mesma altura mínima** e alinhamento visual.
|
||||||
2. Painel OpenPanel é **largura total**, título Spec 028, métricas bridge e botão teste.
|
2. Cada card: ícone + spec label + título + descrição (2 linhas) + badge + acções.
|
||||||
3. Botão «Testar multidomínio» abre modal com passos da suite 028.
|
3. Purge: formulário e tabela **só no modal** «Gerir códigos».
|
||||||
4. Painel purge Spec 032 mantém formulário e tabela de códigos activos.
|
4. OpenPanel / SOC: teste rápido no card; detalhes no modal.
|
||||||
5. Layout responsivo: 1 coluna em mobile (`max-width: 900px`).
|
5. Responsivo: 1 coluna em mobile (`max-width: 480px`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fora de escopo
|
## Extensão futura
|
||||||
|
|
||||||
- Redesign `Infra 2` / SOC dark (`renderInfra2`)
|
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.
|
||||||
- Dashboard widgets duplicando Infra
|
|
||||||
- Edição de integrações na UI
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Conclusão
|
## 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**
|
- Códigos purge → **Spec 032**
|
||||||
- Webhook SOC → Spec **001** / health integrations
|
- Webhook SOC → health integrations / Spec **001**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue