diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx b/specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx
new file mode 100644
index 0000000..b475903
--- /dev/null
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx
@@ -0,0 +1,39 @@
+import { Link } from 'react-router-dom'
+import { SEVERITY_BUCKETS } from '../lib/severity'
+
+export default function AlertBar({ counts, linkable = true }) {
+ return (
+
+ {SEVERITY_BUCKETS.map(({ key, label, hint, className }) => {
+ const val = counts[key] ?? 0
+ const inner = (
+ <>
+
{val}
+
{label}
+
{hint}
+ {linkable && val > 0 ? (
+
Ver alertas →
+ ) : null}
+ >
+ )
+ if (linkable && val > 0) {
+ return (
+
+ {inner}
+
+ )
+ }
+ return (
+
+ {inner}
+
+ )
+ })}
+
+ )
+}
diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js
new file mode 100644
index 0000000..2083b0a
--- /dev/null
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js
@@ -0,0 +1,32 @@
+import { MOCK_DISCOVER_EVENTS } from '../mock'
+import { apiGet } from './api'
+
+function normalizeApiEvent(row) {
+ const payload = row.payload || {}
+ const data = payload.data || {}
+ const level = row.severity ?? data.level ?? payload.level
+ return {
+ id: row.id,
+ source: row.source || 'wazuh',
+ event_type: row.event_type,
+ domain: row.domain || payload.domain || data.agent || '—',
+ severity: level != null ? Number(level) : null,
+ summary: data.description || payload.summary || row.event_type,
+ agent: data.agent || payload.agent || '—',
+ rule_id: data.rule_id || payload.rule_id,
+ chamado_public_id: payload.chamado_public_id || null,
+ created_at: row.created_at,
+ wazuh_link: 'https://wazuh.itecnologys.com/',
+ }
+}
+
+export async function loadDiscoverEvents() {
+ try {
+ const data = await apiGet('/api/v1/webhooks/events?source=wazuh')
+ const rows = (data.events || []).map(normalizeApiEvent)
+ if (rows.length) return { events: rows, fromApi: true }
+ } catch {
+ /* sem login JWT — mock */
+ }
+ return { events: MOCK_DISCOVER_EVENTS, fromApi: false }
+}
diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js
new file mode 100644
index 0000000..8f2709b
--- /dev/null
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js
@@ -0,0 +1,44 @@
+/** Buckets Wazuh — mesma escala da home Console */
+
+export const SEVERITY_BUCKETS = [
+ { key: 'critical', label: 'Critical severity', hint: 'Rule level 15 or higher', className: 'wz-sev-critical', min: 15, max: 99 },
+ { key: 'high', label: 'High severity', hint: 'Rule level 12 to 14', className: 'wz-sev-high', min: 12, max: 14 },
+ { key: 'medium', label: 'Medium severity', hint: 'Rule level 7 to 11', className: 'wz-sev-med', min: 7, max: 11 },
+ { key: 'low', label: 'Low severity', hint: 'Rule level 0 to 6', className: 'wz-sev-low', min: 0, max: 6 },
+]
+
+export function bucketForLevel(level) {
+ const n = Number(level)
+ if (Number.isNaN(n)) return null
+ return SEVERITY_BUCKETS.find((b) => n >= b.min && n <= b.max)?.key ?? null
+}
+
+export function aggregateWazuh24h(events) {
+ const counts = Object.fromEntries(SEVERITY_BUCKETS.map((b) => [b.key, 0]))
+ const now = Date.now()
+ const windowMs = 24 * 60 * 60 * 1000
+ for (const ev of events || []) {
+ if (ev.source !== 'wazuh') continue
+ const ts = ev.created_at ? new Date(ev.created_at).getTime() : now
+ if (now - ts > windowMs) continue
+ const bucket = bucketForLevel(ev.severity)
+ if (bucket) counts[bucket] += 1
+ }
+ return counts
+}
+
+export function filterByBucket(events, bucketKey) {
+ if (!bucketKey) return events
+ const now = Date.now()
+ const windowMs = 24 * 60 * 60 * 1000
+ return (events || []).filter((ev) => {
+ if (bucketForLevel(ev.severity) !== bucketKey) return false
+ if (ev.source !== 'wazuh') return false
+ const ts = ev.created_at ? new Date(ev.created_at).getTime() : now
+ return now - ts <= windowMs
+ })
+}
+
+export function bucketLabel(bucketKey) {
+ return SEVERITY_BUCKETS.find((b) => b.key === bucketKey)?.label ?? bucketKey
+}
diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js b/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js
index 3073ba8..5f86b9f 100644
--- a/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js
@@ -31,8 +31,27 @@ export const MOCK_CHAMADOS = [
]
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' },
+ { id: 901, chamado_public_id: 'CH-2026-00042', source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 12, summary: 'SSH brute force', agent: 'vm112-mail', rule_id: '5712', created_at: new Date(Date.now() - 3600000).toISOString() },
+ { id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout', agent: '—', rule_id: null, created_at: new Date(Date.now() - 3000000).toISOString() },
+]
+
+/** Alertas Wazuh últimas 24h — alimentam barra + Discover filtrável */
+export const MOCK_DISCOVER_EVENTS = [
+ { id: 1001, source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 9, summary: 'Integrity checksum changed', agent: 'vm112-mail', rule_id: '550', chamado_public_id: null, created_at: new Date(Date.now() - 2 * 3600000).toISOString() },
+ { id: 1002, source: 'wazuh', event_type: 'wazuh.alert', domain: 'ligbox.com.br', severity: 8, summary: 'Multiple authentication failures', agent: 'vm123-finance', rule_id: '5710', chamado_public_id: null, created_at: new Date(Date.now() - 5 * 3600000).toISOString() },
+ { id: 1003, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 5, summary: 'sshd: authentication failure', agent: 'vm122-desk', rule_id: '5712', chamado_public_id: null, created_at: new Date(Date.now() - 1 * 3600000).toISOString() },
+ { id: 1004, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 4, summary: 'PAM: User login failed', agent: 'vm122-desk', rule_id: '5503', chamado_public_id: null, created_at: new Date(Date.now() - 3 * 3600000).toISOString() },
+ { id: 1005, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 3, summary: 'New Yum package installed', agent: 'vm104-wazuh', rule_id: '23504', chamado_public_id: null, created_at: new Date(Date.now() - 6 * 3600000).toISOString() },
+ { id: 1006, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 3, summary: 'Listened ports status changed', agent: 'traefik-proxy', rule_id: '533', chamado_public_id: null, created_at: new Date(Date.now() - 8 * 3600000).toISOString() },
+ { id: 1007, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 2, summary: 'syslog: cron session opened', agent: 'vm112-mail', rule_id: '2833', chamado_public_id: null, created_at: new Date(Date.now() - 10 * 3600000).toISOString() },
+ { id: 1008, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 2, summary: 'File added to the system', agent: 'vm123-finance', rule_id: '554', chamado_public_id: null, created_at: new Date(Date.now() - 12 * 3600000).toISOString() },
+ { id: 1009, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 2, summary: 'Dpkg (Debian Package) changed', agent: 'vm122-desk', rule_id: '2902', chamado_public_id: null, created_at: new Date(Date.now() - 14 * 3600000).toISOString() },
+ { id: 1010, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 1, summary: 'Login session opened', agent: 'vm104-wazuh', rule_id: '5501', chamado_public_id: null, created_at: new Date(Date.now() - 16 * 3600000).toISOString() },
+ { id: 1011, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 1, summary: 'PAM: Login session closed', agent: 'traefik-proxy', rule_id: '5502', chamado_public_id: null, created_at: new Date(Date.now() - 18 * 3600000).toISOString() },
+ { id: 1012, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 1, summary: 'sshd: Accepted publickey', agent: 'vm112-mail', rule_id: '5715', chamado_public_id: null, created_at: new Date(Date.now() - 20 * 3600000).toISOString() },
+ { id: 1013, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 0, summary: 'Agent started', agent: 'vm122-desk', rule_id: '503', chamado_public_id: null, created_at: new Date(Date.now() - 22 * 3600000).toISOString() },
+ { id: 1014, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 0, summary: 'Agent keepalive', agent: 'vm123-finance', rule_id: '504', chamado_public_id: null, created_at: new Date(Date.now() - 23 * 3600000).toISOString() },
+ { id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout', agent: '—', rule_id: null, created_at: new Date(Date.now() - 3000000).toISOString() },
]
export const MOCK_TENANTS = [
diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css b/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css
index e46c55c..fcd3342 100644
--- a/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css
@@ -143,6 +143,43 @@ a:hover { text-decoration: underline; }
text-align: center;
}
+.wz-alert-stat--link {
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.15s;
+ cursor: pointer;
+}
+
+.wz-alert-stat--link:hover {
+ background: var(--lb-accent-soft);
+ text-decoration: none;
+}
+
+.wz-alert-hint {
+ display: block;
+ font-size: 0.65rem;
+ color: var(--lb-text-muted);
+ text-transform: none;
+ margin-top: 0.2rem;
+}
+
+.wz-alert-cta {
+ display: block;
+ font-size: 0.72rem;
+ color: var(--lb-accent);
+ margin-top: 0.45rem;
+ font-weight: 600;
+}
+
+.discover-filter-banner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin-bottom: 1rem;
+}
+
.wz-alert-stat:last-child { border-right: none; }
.wz-alert-stat strong {
diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx b/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx
index 85e7c9b..08fa5aa 100644
--- a/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx
@@ -1,27 +1,11 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
+import AlertBar from '../components/AlertBar'
+import { loadDiscoverEvents } from '../lib/events'
+import { aggregateWazuh24h } from '../lib/severity'
import { MOCK_AUDIT_TENANTS, MOCK_CHAMADOS, MOCK_TENANTS } from '../mock'
import { apiGet } from '../lib/api'
-function AlertBar() {
- return (
-
- {[
- ['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]) => (
-
- {val}
- {label}
- {hint}
-
- ))}
-
- )
-}
-
function FeatureSection({ title, children }) {
return (
@@ -103,12 +87,18 @@ function AuditTenantCards({ auditTenants }) {
export default function ConsoleHome() {
const [infra, setInfra] = useState(MOCK_TENANTS)
const [audit, setAudit] = useState(MOCK_AUDIT_TENANTS)
+ const [alertCounts, setAlertCounts] = useState({ critical: 0, high: 0, medium: 0, low: 0 })
const [usingMock, setUsingMock] = useState(true)
useEffect(() => {
let cancelled = false
;(async () => {
try {
+ const { events, fromApi } = await loadDiscoverEvents()
+ if (!cancelled) {
+ setAlertCounts(aggregateWazuh24h(events))
+ if (fromApi) setUsingMock(false)
+ }
const [tData, aData] = await Promise.all([
apiGet('/api/v1/tenants').catch(() => null),
apiGet('/api/v1/audit/overview').catch(() => null),
@@ -133,11 +123,11 @@ export default function ConsoleHome() {
<>
Console
- 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)' : ''}
+ Operações unificadas — clique nos números de alerta para ver quem gerou cada evento (agente, regra, domínio).
+ {usingMock ? ' Dados de demonstração até login API.' : ''}
-
+
diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx b/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx
index 18ceb29..145bb30 100644
--- a/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx
+++ b/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx
@@ -1,36 +1,119 @@
-import { Link } from 'react-router-dom'
-import { MOCK_EVENTS } from '../mock'
+import { useEffect, useMemo, useState } from 'react'
+import { Link, useSearchParams } from 'react-router-dom'
+import { loadDiscoverEvents } from '../lib/events'
+import { bucketLabel, filterByBucket } from '../lib/severity'
+
+function fmtWhen(iso) {
+ if (!iso) return '—'
+ try {
+ return new Date(iso).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' })
+ } catch {
+ return iso
+ }
+}
export default function Discover() {
+ const [searchParams, setSearchParams] = useSearchParams()
+ const bucket = searchParams.get('bucket')
+ const [events, setEvents] = useState([])
+ const [fromApi, setFromApi] = useState(false)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ let cancelled = false
+ ;(async () => {
+ setLoading(true)
+ const { events: rows, fromApi: api } = await loadDiscoverEvents()
+ if (!cancelled) {
+ setEvents(rows)
+ setFromApi(api)
+ setLoading(false)
+ }
+ })()
+ return () => { cancelled = true }
+ }, [])
+
+ const filtered = useMemo(() => {
+ if (!bucket) return events
+ return filterByBucket(events, bucket)
+ }, [events, bucket])
+
+ const clearFilter = () => setSearchParams({})
+
return (
<>
+ ← Console
Discover
- Feed unificado — clique num evento para abrir o hub CH-* (≤ 2 cliques).
-
-
-
-
- ID Origem Evento Domínio Sev Chamado
-
-
-
- {MOCK_EVENTS.map((e) => (
-
- {e.id}
- {e.source}
- {e.event_type}
- {e.domain}
- {e.severity ?? '—'}
-
- {e.chamado_public_id ? (
- {e.chamado_public_id}
- ) : '—'}
-
+ {bucket ? (
+
+
+ Filtro activo: {bucketLabel(bucket)} — {filtered.length} alerta(s) nas últimas 24h
+
+
Limpar filtro
+
+ ) : (
+
+ Feed unificado Wazuh + onboard. Cada linha mostra agente , regra e ligação ao chamado.
+ {fromApi ? '' : ' (demonstração — login API para dados live)'}
+
+ )}
+
+ {loading ? (
+ Carregando eventos…
+ ) : filtered.length === 0 ? (
+
+
Nenhum alerta neste filtro nas últimas 24 horas.
+
+ ) : (
+
+
+
+
+ Quando
+ Sev
+ Agente
+ Regra
+ Descrição
+ Domínio
+ Chamado
+ Wazuh
- ))}
-
-
-
+
+
+ {filtered.map((e) => (
+
+ {fmtWhen(e.created_at)}
+
+ {e.severity != null ? (
+ = 15 ? 'critical' : e.severity >= 12 ? 'high' : e.severity >= 7 ? 'medium' : 'low'}`}>
+ L{e.severity}
+
+ ) : '—'}
+
+ {e.agent || '—'}
+ {e.rule_id ? {e.rule_id} : '—'}
+ {e.summary || e.event_type}
+ {e.domain || '—'}
+
+ {e.chamado_public_id ? (
+ {e.chamado_public_id}
+ ) : '—'}
+
+
+ {e.source === 'wazuh' ? (
+
+ Abrir
+
+ ) : (
+ {e.source}
+ )}
+
+
+ ))}
+
+
+
+ )}
>
)
}