Console: tema claro Wazuh + Tenants fundidos no menu Console.

Substitui fundo escuro por layout branco com cards e barra de alertas; home Console unifica infra tenants e scorecard; Desk esconde menu Tenants e aponta para console.ligbox.com.br.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ligbox Spec Hub 2026-06-25 11:48:00 +00:00
parent 5cf722c0e3
commit 33ccbcd879
10 changed files with 580 additions and 76 deletions

View file

@ -215,7 +215,11 @@
<span class="nav-icon-wrap" aria-hidden="true"><svg class="nav-icon-svg"><use href="#icon-events"/></svg></span> <span class="nav-icon-wrap" aria-hidden="true"><svg class="nav-icon-svg"><use href="#icon-events"/></svg></span>
<span class="nav-label">Eventos</span> <span class="nav-label">Eventos</span>
</button> </button>
<button type="button" data-view="tenants" data-module="tenants" id="nav-tenants" class="nav-item nav-item-tenants"> <a href="https://console.ligbox.com.br" class="nav-item nav-item-console" id="nav-console" target="_blank" rel="noopener noreferrer" title="Console unificado — Tenants + Ops">
<span class="nav-icon-wrap" aria-hidden="true"><svg class="nav-icon-svg"><use href="#icon-tenants"/></svg></span>
<span class="nav-label">Console</span>
</a>
<button type="button" data-view="tenants" data-module="tenants" id="nav-tenants" hidden class="nav-item nav-item-tenants">
<span class="nav-icon-wrap" aria-hidden="true"><svg class="nav-icon-svg"><use href="#icon-tenants"/></svg></span> <span class="nav-icon-wrap" aria-hidden="true"><svg class="nav-icon-svg"><use href="#icon-tenants"/></svg></span>
<span class="nav-label">Tenants</span> <span class="nav-label">Tenants</span>
</button> </button>

View file

@ -1,19 +1,36 @@
/* Ligbox Console — tokens claros (Wazuh-like) — self-contained for Docker build */
:root { :root {
--lb-bg-app: #0f1219; --lb-bg-app: #f5f7fa;
--lb-bg-sidebar: #141824; --lb-bg-sidebar: #ffffff;
--lb-bg-panel: #1a1f2e; --lb-bg-panel: #ffffff;
--lb-bg-card: #252b3b; --lb-bg-card: #ffffff;
--lb-bg-hover: #2d3448; --lb-bg-hover: #e8ecf4;
--lb-border: #3d4559; --lb-bg-subtle: #fafbfd;
--lb-accent: #1565d8; --lb-border: #d3dae6;
--lb-text-primary: #e8eaed; --lb-border-light: #e4e8f0;
--lb-text-secondary: #9aa0a6; --lb-accent: #006bb4;
--lb-text-muted: #6b7280; --lb-accent-hover: #005a9c;
--lb-sev-high: #f44336; --lb-accent-soft: #e6f0fa;
--lb-text-primary: #1a1c21;
--lb-text-secondary: #535966;
--lb-text-muted: #69707d;
--lb-text-heading: #343741;
--lb-sev-low: #017d73;
--lb-sev-med: #f5a700;
--lb-sev-high: #bd2717;
--lb-sev-critical: #8a0f0f;
--lb-src-wazuh: #00a9e0; --lb-src-wazuh: #00a9e0;
--lb-src-onboard: #7c4dff; --lb-src-onboard: #6b4c9a;
--lb-font: 'Inter', system-ui, sans-serif; --lb-src-support: #017d73;
--lb-mono: 'JetBrains Mono', monospace; --lb-src-audit: #69707d;
--lb-sidebar-width: 240px; --lb-st-novo: #5c6bc0;
--lb-st-assumido: #006bb4;
--lb-st-correcao: #f5a700;
--lb-st-resolvido: #017d73;
--lb-sidebar-width: 260px;
--lb-radius: 6px; --lb-radius: 6px;
--lb-shadow: 0 1px 2px rgba(26, 28, 33, 0.06);
--lb-font: 'Inter', system-ui, -apple-system, sans-serif;
--lb-mono: 'JetBrains Mono', 'Fira Code', monospace;
} }

View file

@ -1,21 +1,27 @@
import { NavLink, Route, Routes } from 'react-router-dom' import { NavLink, Route, Routes } from 'react-router-dom'
import Overview from './views/Overview' import ConsoleHome from './views/ConsoleHome'
import Discover from './views/Discover' import Discover from './views/Discover'
import ChamadosList from './views/ChamadosList' import ChamadosList from './views/ChamadosList'
import ChamadoHub from './views/ChamadoHub' import ChamadoHub from './views/ChamadoHub'
const nav = [ const nav = [
{ to: '/', label: 'Overview', end: true }, { to: '/', label: 'Console', end: true, icon: '◆' },
{ to: '/discover', label: 'Discover' }, { to: '/discover', label: 'Discover', icon: '◇' },
{ to: '/chamados', label: 'Chamados' }, { to: '/chamados', label: 'Chamados', icon: '◇' },
] ]
export default function App() { export default function App() {
return ( return (
<div className="shell"> <div className="shell">
<aside className="sidebar"> <aside className="sidebar">
<h1>Ligbox Ops Console</h1> <div className="sidebar-brand">
<p>VM123 · hub CH-* · Spec 019</p> <div className="sidebar-logo" aria-hidden="true">LB</div>
<div>
<h1>Ligbox Console</h1>
<p className="sidebar-sub">Ops · Tenants · CH-*</p>
</div>
</div>
<p className="nav-section-label">Navegação</p>
<nav> <nav>
{nav.map((item) => ( {nav.map((item) => (
<NavLink <NavLink
@ -34,7 +40,7 @@ export default function App() {
</aside> </aside>
<main className="main"> <main className="main">
<Routes> <Routes>
<Route path="/" element={<Overview />} /> <Route path="/" element={<ConsoleHome />} />
<Route path="/discover" element={<Discover />} /> <Route path="/discover" element={<Discover />} />
<Route path="/chamados" element={<ChamadosList />} /> <Route path="/chamados" element={<ChamadosList />} />
<Route path="/chamados/:publicId" element={<ChamadoHub />} /> <Route path="/chamados/:publicId" element={<ChamadoHub />} />

View file

@ -0,0 +1,13 @@
const API_BASE = (import.meta.env.VITE_API_URL || 'https://api.ops.ligbox.com.br').replace(/\/$/, '')
export async function apiGet(path, { token } = {}) {
const headers = { Accept: 'application/json' }
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`${API_BASE}${path}`, { headers, credentials: 'include' })
if (!res.ok) {
const err = new Error(`HTTP ${res.status}`)
err.status = res.status
throw err
}
return res.json()
}

View file

@ -34,3 +34,16 @@ export const MOCK_EVENTS = [
{ id: 901, chamado_public_id: 'CH-2026-00042', source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 12, summary: 'SSH brute force' }, { id: 901, chamado_public_id: 'CH-2026-00042', source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 12, summary: 'SSH brute force' },
{ id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout' }, { id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout' },
] ]
export const MOCK_TENANTS = [
{ id: 1, name: 'VM112 Onboard / Carbonio', ip: '10.10.10.112', role: 'onboarding_mail', created_at: '2026-06-08T00:00:00Z' },
{ id: 2, name: 'VM122 Ligbox Ops', ip: '10.10.10.122', role: 'ops_api', created_at: '2026-06-08T00:00:00Z' },
{ id: 3, name: 'VM104 Wazuh', ip: '10.10.10.104', role: 'soc', created_at: '2026-06-08T00:00:00Z' },
{ id: 4, name: 'VM123 Finance', ip: '10.10.10.123', role: 'billing', created_at: '2026-06-17T00:00:00Z' },
]
export const MOCK_AUDIT_TENANTS = [
{ id: 'vm112', label: 'VM112 Mail', kind: 'carbonio', score: 7, domain: 'onboard.ligbox.com.br' },
{ id: 'wazuh', label: 'Wazuh SOC', kind: 'wazuh_soc', score: 8 },
{ id: 'demo', label: 'cenario-demo.ops.ligbox.com.br', kind: 'domain', score: 6, domain: 'cenario-demo.ops.ligbox.com.br' },
]

View file

@ -1,45 +1,245 @@
@import url('../design-tokens.css'); @import url('../design-tokens.css');
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
margin: 0; margin: 0;
font-family: var(--lb-font); font-family: var(--lb-font);
background: var(--lb-bg-app); background: var(--lb-bg-app);
color: var(--lb-text-primary); color: var(--lb-text-primary);
-webkit-font-smoothing: antialiased;
} }
a { color: var(--lb-accent); text-decoration: none; } a { color: var(--lb-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell { display: flex; min-height: 100vh; } .shell { display: flex; min-height: 100vh; }
.sidebar { .sidebar {
width: var(--lb-sidebar-width); width: var(--lb-sidebar-width);
background: var(--lb-bg-sidebar); background: var(--lb-bg-sidebar);
border-right: 1px solid var(--lb-border); border-right: 1px solid var(--lb-border);
padding: 1rem 0.75rem; padding: 1.25rem 1rem;
flex-shrink: 0; flex-shrink: 0;
box-shadow: var(--lb-shadow);
} }
.sidebar h1 { font-size: 1rem; margin: 0 0 0.25rem; }
.sidebar p { font-size: 0.75rem; color: var(--lb-text-secondary); margin: 0 0 1rem; } .sidebar-brand {
display: flex;
align-items: center;
gap: 0.65rem;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--lb-border-light);
}
.sidebar-logo {
width: 36px;
height: 36px;
border-radius: 8px;
background: linear-gradient(135deg, #006bb4, #00a9e0);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 800;
font-size: 0.85rem;
}
.sidebar h1 {
font-size: 1rem;
font-weight: 700;
margin: 0;
color: var(--lb-text-heading);
letter-spacing: -0.02em;
}
.sidebar .sidebar-sub {
font-size: 0.72rem;
color: var(--lb-text-muted);
margin: 0.15rem 0 0;
}
.nav-section-label {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--lb-text-muted);
margin: 1rem 0 0.35rem 0.75rem;
}
.nav-link { .nav-link {
display: block; display: flex;
align-items: center;
gap: 0.5rem;
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 0.5rem 0.75rem; padding: 0.55rem 0.75rem;
margin-bottom: 0.25rem; margin-bottom: 0.15rem;
border: none; border: none;
border-radius: var(--lb-radius); border-radius: var(--lb-radius);
background: transparent; background: transparent;
color: var(--lb-text-primary); color: var(--lb-text-secondary);
cursor: pointer; cursor: pointer;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
} }
.nav-link:hover, .nav-link.active { background: var(--lb-bg-hover); }
.main { flex: 1; padding: 1.25rem 1.5rem; overflow: auto; } .nav-link:hover {
background: var(--lb-bg-hover);
color: var(--lb-text-primary);
text-decoration: none;
}
.nav-link.active {
background: var(--lb-accent-soft);
color: var(--lb-accent);
font-weight: 600;
}
.main {
flex: 1;
padding: 1.5rem 2rem 2rem;
overflow: auto;
max-width: 1400px;
}
.page-title {
font-size: 1.35rem;
font-weight: 700;
color: var(--lb-text-heading);
margin: 0 0 0.35rem;
}
.page-lead {
color: var(--lb-text-secondary);
font-size: 0.9rem;
margin: 0 0 1.25rem;
max-width: 52rem;
}
/* Wazuh-style alert summary bar */
.wz-alerts-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
background: var(--lb-bg-card);
border: 1px solid var(--lb-border);
border-radius: var(--lb-radius);
margin-bottom: 1.5rem;
overflow: hidden;
box-shadow: var(--lb-shadow);
}
@media (max-width: 900px) {
.wz-alerts-bar { grid-template-columns: repeat(2, 1fr); }
}
.wz-alert-stat {
padding: 1rem 1.25rem;
border-right: 1px solid var(--lb-border-light);
text-align: center;
}
.wz-alert-stat:last-child { border-right: none; }
.wz-alert-stat strong {
display: block;
font-size: 1.75rem;
font-weight: 700;
line-height: 1.1;
margin-bottom: 0.25rem;
}
.wz-alert-stat span {
font-size: 0.72rem;
color: var(--lb-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.wz-sev-critical strong { color: var(--lb-sev-critical); }
.wz-sev-high strong { color: var(--lb-sev-high); }
.wz-sev-med strong { color: var(--lb-sev-med); }
.wz-sev-low strong { color: var(--lb-sev-low); }
/* Section blocks */
.wz-section {
margin-bottom: 1.75rem;
}
.wz-section-title {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--lb-text-muted);
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--lb-border-light);
}
.wz-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.wz-feature-card {
background: var(--lb-bg-card);
border: 1px solid var(--lb-border);
border-radius: var(--lb-radius);
padding: 1.1rem 1.15rem;
display: flex;
gap: 1rem;
align-items: flex-start;
box-shadow: var(--lb-shadow);
transition: border-color 0.15s, box-shadow 0.15s;
}
.wz-feature-card:hover {
border-color: #b8c4d8;
box-shadow: 0 2px 8px rgba(26, 28, 33, 0.08);
}
.wz-feature-icon {
width: 48px;
height: 48px;
flex-shrink: 0;
border-radius: var(--lb-radius);
background: var(--lb-bg-subtle);
border: 1px solid var(--lb-border-light);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.35rem;
}
.wz-feature-card h3 {
margin: 0 0 0.35rem;
font-size: 0.95rem;
font-weight: 700;
color: var(--lb-text-heading);
}
.wz-feature-card p {
margin: 0;
font-size: 0.82rem;
line-height: 1.45;
color: var(--lb-text-secondary);
}
.card { .card {
background: var(--lb-bg-card); background: var(--lb-bg-card);
border: 1px solid var(--lb-border); border: 1px solid var(--lb-border);
border-radius: var(--lb-radius); border-radius: var(--lb-radius);
padding: 1rem; padding: 1rem 1.15rem;
margin-bottom: 1rem; margin-bottom: 1rem;
box-shadow: var(--lb-shadow);
} }
.badge { .badge {
display: inline-block; display: inline-block;
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
@ -47,39 +247,88 @@ a { color: var(--lb-accent); text-decoration: none; }
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 600; font-weight: 600;
} }
.badge-wazuh { background: var(--lb-src-wazuh); color: #000; }
.badge-onboard { background: var(--lb-src-onboard); color: #fff; } .badge-wazuh { background: #e6f7fc; color: #006bb4; border: 1px solid #b3e4f5; }
.badge-sev-high { background: var(--lb-sev-high); color: #fff; } .badge-onboard { background: #f0e8f8; color: #6b4c9a; border: 1px solid #d4c4e8; }
.badge-sev-high { background: #fce8e6; color: var(--lb-sev-high); }
.hub-grid { display: grid; grid-template-columns: 1fr 320px; gap: 1rem; } .hub-grid { display: grid; grid-template-columns: 1fr 320px; gap: 1rem; }
@media (max-width: 900px) { .hub-grid { grid-template-columns: 1fr; } } @media (max-width: 900px) { .hub-grid { grid-template-columns: 1fr; } }
.timeline { list-style: none; padding: 0; margin: 0; } .timeline { list-style: none; padding: 0; margin: 0; }
.timeline li { .timeline li {
padding: 0.5rem 0; padding: 0.5rem 0;
border-bottom: 1px solid var(--lb-border); border-bottom: 1px solid var(--lb-border-light);
font-size: 0.875rem; font-size: 0.875rem;
} }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.tab { .tab {
padding: 0.35rem 0.75rem; padding: 0.35rem 0.75rem;
border-radius: var(--lb-radius); border-radius: var(--lb-radius);
border: 1px solid var(--lb-border); border: 1px solid var(--lb-border);
background: var(--lb-bg-panel); background: var(--lb-bg-card);
color: var(--lb-text-secondary); color: var(--lb-text-secondary);
cursor: pointer; cursor: pointer;
font-size: 0.8rem; font-size: 0.8rem;
} }
.tab.active { border-color: var(--lb-accent); color: var(--lb-accent); } .tab.active { border-color: var(--lb-accent); color: var(--lb-accent); background: var(--lb-accent-soft); }
.kv { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 1rem; font-size: 0.85rem; } .kv { display: grid; grid-template-columns: auto 1fr; gap: 0.25rem 1rem; font-size: 0.85rem; }
.kv dt { color: var(--lb-text-muted); } .kv dt { color: var(--lb-text-muted); }
code { font-family: var(--lb-mono); font-size: 0.8rem; } code {
font-family: var(--lb-mono);
font-size: 0.8rem;
background: var(--lb-bg-subtle);
padding: 0.1rem 0.35rem;
border-radius: 3px;
border: 1px solid var(--lb-border-light);
}
.btn { .btn {
padding: 0.4rem 0.9rem; padding: 0.4rem 0.9rem;
border-radius: var(--lb-radius); border-radius: var(--lb-radius);
border: 1px solid var(--lb-border); border: 1px solid var(--lb-border);
background: var(--lb-bg-panel); background: var(--lb-bg-card);
color: var(--lb-text-primary); color: var(--lb-text-primary);
cursor: pointer; cursor: pointer;
font-size: 0.85rem; font-size: 0.85rem;
} }
.btn-primary { background: var(--lb-accent); border-color: var(--lb-accent); color: #fff; } .btn-primary { background: var(--lb-accent); border-color: var(--lb-accent); color: #fff; }
.status-pill { font-size: 0.75rem; color: var(--lb-text-secondary); } .btn-primary:hover { background: var(--lb-accent-hover); }
.status-pill {
font-size: 0.72rem;
color: var(--lb-text-muted);
padding: 0.5rem 0.75rem;
background: var(--lb-bg-subtle);
border-radius: var(--lb-radius);
border: 1px solid var(--lb-border-light);
}
.table-wrap { overflow-x: auto; }
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.data-table th {
text-align: left;
color: var(--lb-text-muted);
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.65rem 0.75rem;
border-bottom: 2px solid var(--lb-border);
background: var(--lb-bg-subtle);
}
.data-table td {
padding: 0.65rem 0.75rem;
border-bottom: 1px solid var(--lb-border-light);
}
.data-table tbody tr:hover { background: var(--lb-bg-subtle); }
.tenant-health-ok { color: var(--lb-sev-low); font-weight: 600; }
.tenant-health-warn { color: var(--lb-sev-med); font-weight: 600; }
.tenant-health-bad { color: var(--lb-sev-high); font-weight: 600; }

View file

@ -4,8 +4,8 @@ import { MOCK_CHAMADOS } from '../mock'
export default function ChamadosList() { export default function ChamadosList() {
return ( return (
<> <>
<h2>Chamados</h2> <h2 className="page-title">Chamados</h2>
<p style={{ color: 'var(--lb-text-secondary)' }}>Fila de trabalho hub de investigação CH-*</p> <p className="page-lead">Fila de trabalho hub de investigação CH-*</p>
{MOCK_CHAMADOS.map((c) => ( {MOCK_CHAMADOS.map((c) => (
<Link key={c.public_id} to={`/chamados/${c.public_id}`} style={{ display: 'block' }}> <Link key={c.public_id} to={`/chamados/${c.public_id}`} style={{ display: 'block' }}>
<div className="card" style={{ cursor: 'pointer' }}> <div className="card" style={{ cursor: 'pointer' }}>

View file

@ -0,0 +1,197 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { MOCK_AUDIT_TENANTS, MOCK_CHAMADOS, MOCK_TENANTS } from '../mock'
import { apiGet } from '../lib/api'
function AlertBar() {
return (
<div className="wz-alerts-bar" role="region" aria-label="Alertas últimas 24 horas">
{[
['0', 'Critical severity', 'wz-sev-critical', 'Rule level 15 or higher'],
['0', 'High severity', 'wz-sev-high', 'Rule level 12 to 14'],
['2', 'Medium severity', 'wz-sev-med', 'Rule level 7 to 11'],
['12', 'Low severity', 'wz-sev-low', 'Rule level 0 to 6'],
].map(([val, label, cls, hint]) => (
<div key={label} className={`wz-alert-stat ${cls}`}>
<strong>{val}</strong>
<span>{label}</span>
<span style={{ display: 'block', fontSize: '0.65rem', marginTop: '0.2rem', textTransform: 'none' }}>{hint}</span>
</div>
))}
</div>
)
}
function FeatureSection({ title, children }) {
return (
<section className="wz-section">
<h2 className="wz-section-title">{title}</h2>
<div className="wz-card-grid">{children}</div>
</section>
)
}
function FeatureCard({ icon, title, description, to }) {
const inner = (
<div className="wz-feature-card">
<div className="wz-feature-icon" aria-hidden="true">{icon}</div>
<div>
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
)
return to ? <Link to={to} style={{ textDecoration: 'none', color: 'inherit' }}>{inner}</Link> : inner
}
function InfraTable({ tenants }) {
return (
<div className="card table-wrap">
<h3 style={{ marginTop: 0, fontSize: '0.95rem' }}>Nós de infraestrutura</h3>
<p style={{ fontSize: '0.82rem', color: 'var(--lb-text-secondary)', marginTop: 0 }}>
Registo de VMs e serviços Ligbox (antigo menu Tenants).
</p>
<table className="data-table">
<thead>
<tr>
<th>ID</th><th>Nome</th><th>IP</th><th>Papel</th><th>Desde</th>
</tr>
</thead>
<tbody>
{tenants.map((t) => (
<tr key={t.id}>
<td>{t.id}</td>
<td><strong>{t.name}</strong></td>
<td><code>{t.ip}</code></td>
<td>{t.role}</td>
<td>{t.created_at ? new Date(t.created_at).toLocaleDateString('pt-BR') : '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
function AuditTenantCards({ auditTenants }) {
if (!auditTenants.length) return null
return (
<div className="card">
<h3 style={{ marginTop: 0, fontSize: '0.95rem' }}>Saúde por tenant / domínio</h3>
<div className="wz-card-grid" style={{ marginTop: '0.75rem' }}>
{auditTenants.map((t) => {
const score = t.score ?? t.health_score
const cls = score >= 8 ? 'tenant-health-ok' : score >= 5 ? 'tenant-health-warn' : 'tenant-health-bad'
return (
<div key={t.id || t.domain} className="wz-feature-card">
<div className="wz-feature-icon">📧</div>
<div>
<h3>{t.label || t.domain || t.name}</h3>
<p>
{t.kind ? <span className="badge badge-onboard">{t.kind}</span> : null}{' '}
<span className={cls}>{score != null ? `${score}/8 checks` : t.status || 'monitorizar'}</span>
</p>
</div>
</div>
)
})}
</div>
</div>
)
}
export default function ConsoleHome() {
const [infra, setInfra] = useState(MOCK_TENANTS)
const [audit, setAudit] = useState(MOCK_AUDIT_TENANTS)
const [usingMock, setUsingMock] = useState(true)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const [tData, aData] = await Promise.all([
apiGet('/api/v1/tenants').catch(() => null),
apiGet('/api/v1/audit/overview').catch(() => null),
])
if (cancelled) return
if (tData?.tenants?.length) {
setInfra(tData.tenants)
setUsingMock(false)
}
if (aData?.tenants?.length) {
setAudit(aData.tenants)
setUsingMock(false)
}
} catch {
/* mock fallback */
}
})()
return () => { cancelled = true }
}, [])
return (
<>
<h2 className="page-title">Console</h2>
<p className="page-lead">
Operações unificadas chamados, discover, tenants de infraestrutura e saúde de domínios num lugar.
{usingMock ? ' (dados de demonstração — login API em breve)' : ''}
</p>
<AlertBar />
<FeatureSection title="Security operations">
<FeatureCard
icon="🔍"
title="Discover"
description="Feed unificado de eventos Wazuh, onboard e suporte — investigação em até 2 cliques."
to="/discover"
/>
<FeatureCard
icon="📋"
title="Chamados CH-*"
description="Hub de investigação — timeline, observables, assist wizard e runbooks."
to="/chamados"
/>
<FeatureCard
icon="🛡️"
title="Threat hunting"
description="Alertas correlacionados por domínio e sessão de onboarding."
to="/discover"
/>
</FeatureSection>
<FeatureSection title="Platform tenants">
<FeatureCard
icon="🖥️"
title="Infraestrutura Ligbox"
description="VM112 onboard, VM122 ops, VM104 Wazuh — registo de nós da plataforma."
/>
<FeatureCard
icon="📊"
title="Scorecard domínios"
description="DNS, TLS, Carbonio e webmail — visão consolidada por tenant de oferta."
/>
<FeatureCard
icon="⚙️"
title="Provisionamento"
description="Estado MOSP, purge e orquestração VM112 (Desk Serviços)."
/>
</FeatureSection>
<InfraTable tenants={infra} />
<AuditTenantCards auditTenants={audit} />
<div className="card">
<h3 style={{ marginTop: 0, fontSize: '0.95rem' }}>Chamados recentes</h3>
<ul className="timeline">
{MOCK_CHAMADOS.map((c) => (
<li key={c.public_id}>
<Link to={`/chamados/${c.public_id}`}>{c.public_id}</Link> {c.domain}{' '}
{c.max_severity ? <span className="badge badge-sev-high">L{c.max_severity}</span> : null}
</li>
))}
</ul>
</div>
</>
)
}

View file

@ -4,18 +4,18 @@ import { MOCK_EVENTS } from '../mock'
export default function Discover() { export default function Discover() {
return ( return (
<> <>
<h2>Discover</h2> <h2 className="page-title">Discover</h2>
<p style={{ color: 'var(--lb-text-secondary)' }}>Feed unificado click abre hub CH-* ( 2 cliques)</p> <p className="page-lead">Feed unificado clique num evento para abrir o hub CH-* ( 2 cliques).</p>
<div className="card" style={{ overflowX: 'auto' }}> <div className="card table-wrap">
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}> <table className="data-table">
<thead> <thead>
<tr style={{ textAlign: 'left', color: 'var(--lb-text-muted)' }}> <tr>
<th>ID</th><th>Origem</th><th>Evento</th><th>Domínio</th><th>Sev</th><th>Chamado</th> <th>ID</th><th>Origem</th><th>Evento</th><th>Domínio</th><th>Sev</th><th>Chamado</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{MOCK_EVENTS.map((e) => ( {MOCK_EVENTS.map((e) => (
<tr key={e.id} style={{ borderTop: '1px solid var(--lb-border)' }}> <tr key={e.id}>
<td>{e.id}</td> <td>{e.id}</td>
<td><span className={`badge badge-${e.source}`}>{e.source}</span></td> <td><span className={`badge badge-${e.source}`}>{e.source}</span></td>
<td>{e.event_type}</td> <td>{e.event_type}</td>

View file

@ -1,44 +1,49 @@
/* Ligbox Ops Console — design tokens (referência Wazuh 4.x SOC) */ /* Ligbox Console — tokens claros (referência Wazuh 4.x home) */
:root { :root {
/* Surfaces */ /* Surfaces — light */
--lb-bg-app: #0f1219; --lb-bg-app: #f5f7fa;
--lb-bg-sidebar: #141824; --lb-bg-sidebar: #ffffff;
--lb-bg-panel: #1a1f2e; --lb-bg-panel: #ffffff;
--lb-bg-card: #252b3b; --lb-bg-card: #ffffff;
--lb-bg-hover: #2d3448; --lb-bg-hover: #e8ecf4;
--lb-border: #3d4559; --lb-bg-subtle: #fafbfd;
--lb-border: #d3dae6;
--lb-border-light: #e4e8f0;
/* Brand */ /* Brand */
--lb-accent: #1565d8; --lb-accent: #006bb4;
--lb-accent-hover: #1e88e5; --lb-accent-hover: #005a9c;
--lb-accent-soft: #e6f0fa;
/* Text */ /* Text */
--lb-text-primary: #e8eaed; --lb-text-primary: #1a1c21;
--lb-text-secondary: #9aa0a6; --lb-text-secondary: #535966;
--lb-text-muted: #6b7280; --lb-text-muted: #69707d;
--lb-text-heading: #343741;
/* Severity (Wazuh levels) */ /* Severity (Wazuh levels) */
--lb-sev-low: #4caf50; --lb-sev-low: #017d73;
--lb-sev-med: #ff9800; --lb-sev-med: #f5a700;
--lb-sev-high: #f44336; --lb-sev-high: #bd2717;
--lb-sev-critical: #b71c1c; --lb-sev-critical: #8a0f0f;
/* Source badges */ /* Source badges */
--lb-src-wazuh: #00a9e0; --lb-src-wazuh: #00a9e0;
--lb-src-onboard: #7c4dff; --lb-src-onboard: #6b4c9a;
--lb-src-support: #26a69a; --lb-src-support: #017d73;
--lb-src-audit: #78909c; --lb-src-audit: #69707d;
/* Chamado status */ /* Chamado status */
--lb-st-novo: #5c6bc0; --lb-st-novo: #5c6bc0;
--lb-st-assumido: #29b6f6; --lb-st-assumido: #006bb4;
--lb-st-correcao: #ffa726; --lb-st-correcao: #f5a700;
--lb-st-resolvido: #66bb6a; --lb-st-resolvido: #017d73;
/* Layout */ /* Layout */
--lb-sidebar-width: 240px; --lb-sidebar-width: 260px;
--lb-radius: 6px; --lb-radius: 6px;
--lb-font: 'Inter', system-ui, sans-serif; --lb-shadow: 0 1px 2px rgba(26, 28, 33, 0.06);
--lb-font: 'Inter', system-ui, -apple-system, sans-serif;
--lb-mono: 'JetBrains Mono', 'Fira Code', monospace; --lb-mono: 'JetBrains Mono', 'Fira Code', monospace;
} }