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-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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />} />
|
||||||
|
|
|
||||||
|
|
@ -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: 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' },
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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' }}>
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue