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:
parent
5cf722c0e3
commit
33ccbcd879
10 changed files with 580 additions and 76 deletions
|
|
@ -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-label">Eventos</span>
|
||||
</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-label">Tenants</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,36 @@
|
|||
/* Ligbox Console — tokens claros (Wazuh-like) — self-contained for Docker build */
|
||||
|
||||
:root {
|
||||
--lb-bg-app: #0f1219;
|
||||
--lb-bg-sidebar: #141824;
|
||||
--lb-bg-panel: #1a1f2e;
|
||||
--lb-bg-card: #252b3b;
|
||||
--lb-bg-hover: #2d3448;
|
||||
--lb-border: #3d4559;
|
||||
--lb-accent: #1565d8;
|
||||
--lb-text-primary: #e8eaed;
|
||||
--lb-text-secondary: #9aa0a6;
|
||||
--lb-text-muted: #6b7280;
|
||||
--lb-sev-high: #f44336;
|
||||
--lb-bg-app: #f5f7fa;
|
||||
--lb-bg-sidebar: #ffffff;
|
||||
--lb-bg-panel: #ffffff;
|
||||
--lb-bg-card: #ffffff;
|
||||
--lb-bg-hover: #e8ecf4;
|
||||
--lb-bg-subtle: #fafbfd;
|
||||
--lb-border: #d3dae6;
|
||||
--lb-border-light: #e4e8f0;
|
||||
--lb-accent: #006bb4;
|
||||
--lb-accent-hover: #005a9c;
|
||||
--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-onboard: #7c4dff;
|
||||
--lb-font: 'Inter', system-ui, sans-serif;
|
||||
--lb-mono: 'JetBrains Mono', monospace;
|
||||
--lb-sidebar-width: 240px;
|
||||
--lb-src-onboard: #6b4c9a;
|
||||
--lb-src-support: #017d73;
|
||||
--lb-src-audit: #69707d;
|
||||
--lb-st-novo: #5c6bc0;
|
||||
--lb-st-assumido: #006bb4;
|
||||
--lb-st-correcao: #f5a700;
|
||||
--lb-st-resolvido: #017d73;
|
||||
--lb-sidebar-width: 260px;
|
||||
--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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
import { NavLink, Route, Routes } from 'react-router-dom'
|
||||
import Overview from './views/Overview'
|
||||
import ConsoleHome from './views/ConsoleHome'
|
||||
import Discover from './views/Discover'
|
||||
import ChamadosList from './views/ChamadosList'
|
||||
import ChamadoHub from './views/ChamadoHub'
|
||||
|
||||
const nav = [
|
||||
{ to: '/', label: 'Overview', end: true },
|
||||
{ to: '/discover', label: 'Discover' },
|
||||
{ to: '/chamados', label: 'Chamados' },
|
||||
{ to: '/', label: 'Console', end: true, icon: '◆' },
|
||||
{ to: '/discover', label: 'Discover', icon: '◇' },
|
||||
{ to: '/chamados', label: 'Chamados', icon: '◇' },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="shell">
|
||||
<aside className="sidebar">
|
||||
<h1>Ligbox Ops Console</h1>
|
||||
<p>VM123 · hub CH-* · Spec 019</p>
|
||||
<div className="sidebar-brand">
|
||||
<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.map((item) => (
|
||||
<NavLink
|
||||
|
|
@ -34,7 +40,7 @@ export default function App() {
|
|||
</aside>
|
||||
<main className="main">
|
||||
<Routes>
|
||||
<Route path="/" element={<Overview />} />
|
||||
<Route path="/" element={<ConsoleHome />} />
|
||||
<Route path="/discover" element={<Discover />} />
|
||||
<Route path="/chamados" element={<ChamadosList />} />
|
||||
<Route path="/chamados/:publicId" element={<ChamadoHub />} />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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: 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' },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,45 +1,245 @@
|
|||
@import url('../design-tokens.css');
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--lb-font);
|
||||
background: var(--lb-bg-app);
|
||||
color: var(--lb-text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--lb-accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
.shell { display: flex; min-height: 100vh; }
|
||||
|
||||
.sidebar {
|
||||
width: var(--lb-sidebar-width);
|
||||
background: var(--lb-bg-sidebar);
|
||||
border-right: 1px solid var(--lb-border);
|
||||
padding: 1rem 0.75rem;
|
||||
padding: 1.25rem 1rem;
|
||||
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 {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
margin-bottom: 0.15rem;
|
||||
border: none;
|
||||
border-radius: var(--lb-radius);
|
||||
background: transparent;
|
||||
color: var(--lb-text-primary);
|
||||
color: var(--lb-text-secondary);
|
||||
cursor: pointer;
|
||||
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 {
|
||||
background: var(--lb-bg-card);
|
||||
border: 1px solid var(--lb-border);
|
||||
border-radius: var(--lb-radius);
|
||||
padding: 1rem;
|
||||
padding: 1rem 1.15rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: var(--lb-shadow);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.5rem;
|
||||
|
|
@ -47,39 +247,88 @@ a { color: var(--lb-accent); text-decoration: none; }
|
|||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-wazuh { background: var(--lb-src-wazuh); color: #000; }
|
||||
.badge-onboard { background: var(--lb-src-onboard); color: #fff; }
|
||||
.badge-sev-high { background: var(--lb-sev-high); color: #fff; }
|
||||
|
||||
.badge-wazuh { background: #e6f7fc; color: #006bb4; border: 1px solid #b3e4f5; }
|
||||
.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; }
|
||||
@media (max-width: 900px) { .hub-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.timeline { list-style: none; padding: 0; margin: 0; }
|
||||
.timeline li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--lb-border);
|
||||
border-bottom: 1px solid var(--lb-border-light);
|
||||
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 {
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: var(--lb-radius);
|
||||
border: 1px solid var(--lb-border);
|
||||
background: var(--lb-bg-panel);
|
||||
background: var(--lb-bg-card);
|
||||
color: var(--lb-text-secondary);
|
||||
cursor: pointer;
|
||||
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 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 {
|
||||
padding: 0.4rem 0.9rem;
|
||||
border-radius: var(--lb-radius);
|
||||
border: 1px solid var(--lb-border);
|
||||
background: var(--lb-bg-panel);
|
||||
background: var(--lb-bg-card);
|
||||
color: var(--lb-text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.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; }
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { MOCK_CHAMADOS } from '../mock'
|
|||
export default function ChamadosList() {
|
||||
return (
|
||||
<>
|
||||
<h2>Chamados</h2>
|
||||
<p style={{ color: 'var(--lb-text-secondary)' }}>Fila de trabalho — hub de investigação CH-*</p>
|
||||
<h2 className="page-title">Chamados</h2>
|
||||
<p className="page-lead">Fila de trabalho — hub de investigação CH-*</p>
|
||||
{MOCK_CHAMADOS.map((c) => (
|
||||
<Link key={c.public_id} to={`/chamados/${c.public_id}`} style={{ display: 'block' }}>
|
||||
<div className="card" style={{ cursor: 'pointer' }}>
|
||||
|
|
|
|||
|
|
@ -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 só 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,18 +4,18 @@ import { MOCK_EVENTS } from '../mock'
|
|||
export default function Discover() {
|
||||
return (
|
||||
<>
|
||||
<h2>Discover</h2>
|
||||
<p style={{ color: 'var(--lb-text-secondary)' }}>Feed unificado — click abre hub CH-* (≤ 2 cliques)</p>
|
||||
<div className="card" style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
|
||||
<h2 className="page-title">Discover</h2>
|
||||
<p className="page-lead">Feed unificado — clique num evento para abrir o hub CH-* (≤ 2 cliques).</p>
|
||||
<div className="card table-wrap">
|
||||
<table className="data-table">
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MOCK_EVENTS.map((e) => (
|
||||
<tr key={e.id} style={{ borderTop: '1px solid var(--lb-border)' }}>
|
||||
<tr key={e.id}>
|
||||
<td>{e.id}</td>
|
||||
<td><span className={`badge badge-${e.source}`}>{e.source}</span></td>
|
||||
<td>{e.event_type}</td>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
/* Surfaces */
|
||||
--lb-bg-app: #0f1219;
|
||||
--lb-bg-sidebar: #141824;
|
||||
--lb-bg-panel: #1a1f2e;
|
||||
--lb-bg-card: #252b3b;
|
||||
--lb-bg-hover: #2d3448;
|
||||
--lb-border: #3d4559;
|
||||
/* Surfaces — light */
|
||||
--lb-bg-app: #f5f7fa;
|
||||
--lb-bg-sidebar: #ffffff;
|
||||
--lb-bg-panel: #ffffff;
|
||||
--lb-bg-card: #ffffff;
|
||||
--lb-bg-hover: #e8ecf4;
|
||||
--lb-bg-subtle: #fafbfd;
|
||||
--lb-border: #d3dae6;
|
||||
--lb-border-light: #e4e8f0;
|
||||
|
||||
/* Brand */
|
||||
--lb-accent: #1565d8;
|
||||
--lb-accent-hover: #1e88e5;
|
||||
--lb-accent: #006bb4;
|
||||
--lb-accent-hover: #005a9c;
|
||||
--lb-accent-soft: #e6f0fa;
|
||||
|
||||
/* Text */
|
||||
--lb-text-primary: #e8eaed;
|
||||
--lb-text-secondary: #9aa0a6;
|
||||
--lb-text-muted: #6b7280;
|
||||
--lb-text-primary: #1a1c21;
|
||||
--lb-text-secondary: #535966;
|
||||
--lb-text-muted: #69707d;
|
||||
--lb-text-heading: #343741;
|
||||
|
||||
/* Severity (Wazuh levels) */
|
||||
--lb-sev-low: #4caf50;
|
||||
--lb-sev-med: #ff9800;
|
||||
--lb-sev-high: #f44336;
|
||||
--lb-sev-critical: #b71c1c;
|
||||
--lb-sev-low: #017d73;
|
||||
--lb-sev-med: #f5a700;
|
||||
--lb-sev-high: #bd2717;
|
||||
--lb-sev-critical: #8a0f0f;
|
||||
|
||||
/* Source badges */
|
||||
--lb-src-wazuh: #00a9e0;
|
||||
--lb-src-onboard: #7c4dff;
|
||||
--lb-src-support: #26a69a;
|
||||
--lb-src-audit: #78909c;
|
||||
--lb-src-onboard: #6b4c9a;
|
||||
--lb-src-support: #017d73;
|
||||
--lb-src-audit: #69707d;
|
||||
|
||||
/* Chamado status */
|
||||
--lb-st-novo: #5c6bc0;
|
||||
--lb-st-assumido: #29b6f6;
|
||||
--lb-st-correcao: #ffa726;
|
||||
--lb-st-resolvido: #66bb6a;
|
||||
--lb-st-assumido: #006bb4;
|
||||
--lb-st-correcao: #f5a700;
|
||||
--lb-st-resolvido: #017d73;
|
||||
|
||||
/* Layout */
|
||||
--lb-sidebar-width: 240px;
|
||||
--lb-sidebar-width: 260px;
|
||||
--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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue