Files
Lucas RayanandClaude Sonnet 5 be47b607df
Release / Plan (reuse the published image?) (push) Successful in 4s
CI / Docker image (build and smoke test) (push) Skipped
CI / Format and clippy (push) Successful in 16s
CI / Dependency audit (push) Successful in 17s
CI / Tests (Linux) (push) Successful in 34s
Release / ci (push) Successful in 35s
Release / Build image (arm64) (push) Successful in 4m55s
Release / Build image (amd64) (push) Failing after 11m10s
Release / Publish tags (push) Skipped
Release / Gitea release (push) Skipped
feat: build a real dashboard and fix dead panel navigation in the web UI
The sidebar nav (Overview/Alerts/Analysis) only did scrollIntoView on a
single stacked page, so clicking it produced no visible change. The
Alerts/Analysis panels were also placeholder shells with no real data
exploration despite the API already exposing rich per-pillar evidence.

- app.js/app.css/index.html: real panel switching via showView()/currentView,
  toggling an .active class with `.view:not(.active) { display: none }`;
  settings keeps opening its own dialog without disturbing the active view.
- New Hosts panel: table of observed hosts ranked by fusion score, with
  per-pillar (P1/P2/P3) columns, search, and drill-down into each pillar's
  evidence. Documents the known limitation that /api/alerts only lists hosts
  with at least one alert (no /api/hosts endpoint exists yet), so a benign
  host with no alert won't appear.
- Alerts panel: filterable by pillar and minimum score, sortable, with
  full evidence rendered as a formatted key/value table instead of raw JSON.
- Overview panel: KPI tiles (alert count, hosts with alerts, max score,
  dataset records) plus two interactive SVG charts (top hosts by fusion
  score, alert distribution by pillar) with hover tooltips.
- i18n-en.js: English strings for all new UI text.

No Rust files changed; this is purely apps/gui/ui/ consuming the existing
/api/alerts, /api/analyze and /api/status endpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-11-01 08:28:08 -03:00

335 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acauã</title>
<link rel="icon" href="/logo.png">
<link rel="stylesheet" href="/app.css">
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand">
<img class="logo" src="/logo.png" alt="">
<div>
<strong>Acauã</strong>
<small>caça a ameaças em rede</small>
</div>
</div>
<div class="side-title">Navegação</div>
<nav class="nav" aria-label="Navegação principal">
<button type="button" class="nav-item active" data-target="overview" aria-current="page">
<span class="nav-icon">◈</span>
<span><span class="nav-label">Visão geral</span><span class="nav-sub">estado e prioridades</span></span>
</button>
<button type="button" class="nav-item" data-target="alerts">
<span class="nav-icon">◎</span>
<span><span class="nav-label">Alertas</span><span class="nav-sub">filtros e evidências</span></span>
</button>
<button type="button" class="nav-item" data-target="hosts">
<span class="nav-icon">▤</span>
<span><span class="nav-label">Hosts</span><span class="nav-sub">fusão por evidência</span></span>
</button>
<button type="button" class="nav-item" data-target="analysis">
<span class="nav-icon">⌁</span>
<span><span class="nav-label">Análise</span><span class="nav-sub">dataset Zeek</span></span>
</button>
<button type="button" class="nav-item" data-target="settings">
<span class="nav-icon">⚙</span>
<span><span class="nav-label">Configurações</span><span class="nav-sub">idioma e tema</span></span>
</button>
</nav>
<div class="side-foot">
<div class="status-row">
<span id="connection" class="pill"><span class="dot"></span><span id="connectionText">Conectando…</span></span>
<span class="pill muted">pré-lançamento</span>
</div>
<div class="status-row">
<span id="version" class="mono">—</span>
<button id="updateBtn" type="button" class="link-btn">checar atualização</button>
</div>
</div>
</aside>
<main class="main">
<header class="topbar">
<div>
<p class="eyebrow">Acauã</p>
<h1 id="viewTitle">Visão geral</h1>
<p id="viewSub" class="sub">Estado, prioridades e distribuição dos alertas</p>
</div>
<div class="top-actions">
<span id="lastUpdated" class="mono last-updated">—</span>
<button id="refreshBtn" type="button" class="btn">Atualizar status</button>
<button id="settingsBtn" type="button" class="icon-btn" aria-label="Configurações" title="Configurações">⚙</button>
</div>
</header>
<section class="view active" id="overview">
<div class="kpi-grid">
<article class="kpi">
<span class="kpi-label">Alertas</span>
<strong id="kpiAlerts" class="kpi-value">—</strong>
<span id="kpiAlertsSub" class="kpi-sub">carregando…</span>
</article>
<article class="kpi">
<span class="kpi-label">Hosts com alerta</span>
<strong id="kpiHosts" class="kpi-value">—</strong>
<span id="kpiHostsSub" class="kpi-sub">ranqueados por fusão</span>
</article>
<article class="kpi">
<span class="kpi-label">Maior score</span>
<strong id="kpiScore" class="kpi-value">—</strong>
<span id="kpiScoreSub" class="kpi-sub">melhor host observado</span>
</article>
<article class="kpi">
<span class="kpi-label">Registros no dataset</span>
<strong id="kpiRecords" class="kpi-value">—</strong>
<span id="kpiRecordsSub" class="kpi-sub">armazenamento local</span>
</article>
</div>
<div class="charts-grid">
<section class="panel chart-panel">
<div class="panel-head">
<div>
<h2>Top hosts por fusão</h2>
<p class="hint">Score ponderado de evidências (0 a 1). Passe o mouse para ver os pilares.</p>
</div>
<span class="panel-tag">m1 · m2 · m3</span>
</div>
<div class="chart-wrap">
<svg id="hostChart" class="chart" role="img" aria-label="Hosts por score de fusão"></svg>
<div id="hostTip" class="chart-tip" hidden></div>
</div>
</section>
<section class="panel chart-panel">
<div class="panel-head">
<div>
<h2>Alertas por pilar</h2>
<p class="hint">Distribuição observada. Clique em uma barra para filtrar os alertas.</p>
</div>
<span class="panel-tag">P1 · P2 · P3</span>
</div>
<div class="chart-wrap">
<svg id="pillarChart" class="chart" role="img" aria-label="Distribuição de alertas por pilar"></svg>
<div id="pillarTip" class="chart-tip" hidden></div>
</div>
</section>
</div>
<section class="panel">
<div class="panel-head">
<div>
<h2>Alertas prioritários</h2>
<p class="hint">Os alertas com maior score individual. Abra a lista completa para filtrar.</p>
</div>
<button id="seeAllAlerts" type="button" class="btn">Ver todos os alertas</button>
</div>
<div id="overviewAlerts" class="overview-alerts"></div>
</section>
<section class="pillars" aria-label="Pilares de detecção">
<article class="card" data-pillar-card="1" tabindex="0" role="button">
<span class="card-kicker">Pilar 1</span>
<h2>Beaconing <span class="card-count" data-pillar-count="1">0</span></h2>
<p>Periodograma de Rayleigh/Schuster com grade limitada ao intervalo observado.</p>
<span class="tag">versão inicial</span>
</article>
<article class="card" data-pillar-card="2" tabindex="0" role="button">
<span class="card-kicker">Pilar 2</span>
<h2>Sessões persistentes <span class="card-count" data-pillar-count="2">0</span></h2>
<p>Sessões TCP longas, inclusive as que o Zeek divide por inatividade.</p>
<span class="tag">versão inicial</span>
</article>
<article class="card" data-pillar-card="3" tabindex="0" role="button">
<span class="card-kicker">Pilar 3</span>
<h2>Anomalias de protocolo <span class="card-count" data-pillar-count="3">0</span></h2>
<p>Volume de informação (entropia de Shannon) em subdomínios DNS.</p>
<span class="tag">versão inicial</span>
</article>
<article class="card" data-pillar-card="4" tabindex="0" role="button">
<span class="card-kicker">Pilar 4</span>
<h2>Fusão de evidências</h2>
<p>Prioridade por host com evidência por alerta.</p>
<span class="tag" id="pillarFusionTag">sem hosts</span>
</article>
</section>
<section class="notice">
<strong>Uso autorizado.</strong>
<span>Use somente em redes próprias ou com autorização explícita. Nenhum pacote é enviado para fora.</span>
</section>
</section>
<section class="view" id="alerts" hidden>
<section class="panel">
<div class="panel-head">
<div>
<h2>Alertas</h2>
<p class="hint">Filtre, ordene e expanda cada alerta para ver a evidência completa como tabela.</p>
</div>
<span id="alertCount" class="panel-tag">—</span>
</div>
<div class="toolbar">
<div class="chips" id="pillarFilters" role="group" aria-label="Filtrar por pilar">
<button type="button" class="chip active" data-pillar="all">Todos</button>
<button type="button" class="chip" data-pillar="1">Pilar 1</button>
<button type="button" class="chip" data-pillar="2">Pilar 2</button>
<button type="button" class="chip" data-pillar="3">Pilar 3</button>
</div>
<label class="field compact">
<span>Ordenar</span>
<select id="alertSort">
<option value="score-desc">Maior score</option>
<option value="score-asc">Menor score</option>
<option value="pillar">Pilar</option>
<option value="newest">Mais recentes</option>
</select>
</label>
<label class="field compact range-field">
<span>Score mínimo <output id="minScoreOut">0,00</output></span>
<input id="alertMin" type="range" min="0" max="1" step="0.01" value="0">
</label>
<label class="field compact search-field">
<span>Buscar</span>
<input id="alertSearch" type="search" placeholder="IP, host, domínio ou ID" autocomplete="off">
</label>
</div>
<div id="alertList" class="alert-cards">
<p class="empty">Nenhum alerta ainda. Analise um diretório de logs do Zeek.</p>
</div>
</section>
</section>
<section class="view" id="hosts" hidden>
<section class="panel">
<div class="panel-head">
<div>
<h2>Hosts observados</h2>
<p class="hint">Ranking por score de fusão (desc). Clique em uma linha para ver a evidência de cada pilar.</p>
</div>
<span id="hostCount" class="panel-tag">—</span>
</div>
<div class="toolbar toolbar-slim">
<label class="field compact search-field">
<span>Buscar host</span>
<input id="hostSearch" type="search" placeholder="192.168.1.50" autocomplete="off" spellcheck="false">
</label>
<div class="legend">
<span class="legend-item"><i class="swatch p1"></i>Pilar 1</span>
<span class="legend-item"><i class="swatch p2"></i>Pilar 2</span>
<span class="legend-item"><i class="swatch p3"></i>Pilar 3</span>
</div>
</div>
<div class="table-wrap">
<table class="host-table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Host</th>
<th scope="col">Fusão</th>
<th scope="col">P1</th>
<th scope="col">P2</th>
<th scope="col">P3</th>
<th scope="col">Alertas</th>
<th scope="col"><span class="sr-only">Detalhes</span></th>
</tr>
</thead>
<tbody id="hostRows">
<tr><td colspan="8" class="empty">Nenhum host carregado.</td></tr>
</tbody>
</table>
</div>
<p class="footnote">A API de fusão lista apenas hosts com pelo menos um alerta. Hosts sem alerta não aparecem nesta tabela.</p>
</section>
</section>
<section class="view" id="analysis" hidden>
<section class="panel">
<div class="panel-head">
<div>
<h2>Analisar dataset</h2>
<p class="hint">Aponte para um diretório de logs Zeek ou para um dataset local.</p>
</div>
<span id="reportStatus" class="panel-tag">—</span>
</div>
<div class="field-row">
<label class="field">
<span>Diretório de logs</span>
<input id="datasetPath" type="text" placeholder="/var/log/zeek/current" autocomplete="off" spellcheck="false">
</label>
<button id="analyzeBtn" type="button" class="btn primary">Analisar</button>
</div>
<div id="reportSummary" class="summary-grid" hidden></div>
<div class="report-detail">
<pre id="output" class="output">Nenhuma análise executada.</pre>
<div class="report-actions">
<button id="gotoAlerts" type="button" class="btn" hidden>Ver alertas</button>
<button id="gotoHosts" type="button" class="btn" hidden>Ver hosts</button>
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<h2>Estado do serviço</h2>
<p class="hint">Versão, dataset e armazenamento informados pela API local.</p>
</div>
</div>
<div id="serviceSummary" class="summary-grid summary-grid-wide"></div>
</section>
</section>
<footer class="foot">
<span>Acauã · pré-lançamento</span>
<span class="mono">MIT OR Apache-2.0</span>
</footer>
</main>
</div>
<dialog id="settingsDlg" aria-labelledby="settingsTitle">
<form method="dialog">
<div class="dialog-head">
<h2 id="settingsTitle">Configurações</h2>
<button type="button" id="closeSettings" class="icon-btn" aria-label="Fechar configurações" title="Fechar">×</button>
</div>
<label class="field">
<span>Idioma</span>
<select id="langSel">
<option value="auto">Auto</option>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
</label>
<div class="field">
<span>Tema</span>
<div class="segmented" id="themePick">
<button type="button" data-theme-pick="auto">Auto</button>
<button type="button" data-theme-pick="light">Claro</button>
<button type="button" data-theme-pick="dark">Escuro</button>
</div>
</div>
<menu>
<button type="button" id="doneSettings" class="btn primary">Fechar</button>
</menu>
</form>
</dialog>
<script type="module" src="/app.js"></script>
</body>
</html>