Files
acaua/apps/gui/ui/app.js
T
Lucas RayanandClaude Sonnet 5 8a80504c45
Release / Plan (reuse the published image?) (push) Successful in 3s
CI / Docker image (build and smoke test) (push) Skipped
CI / Format and clippy (push) Successful in 4m32s
CI / Dependency audit (push) Successful in 11s
CI / Tests (Linux) (push) Successful in 9m15s
Release / ci (push) Successful in 9m18s
Release / Build image (arm64) (push) Failing after 7m7s
Release / Build image (amd64) (push) Failing after 13m53s
Release / Publish tags (push) Skipped
Release / Gitea release (push) Skipped
refactor: distribute Acaua as a Docker image instead of a desktop app
Acaua is a collective SOC daemon consuming only Zeek log files, not a
single-user tool needing local hardware like its siblings. Drop the
Tauri desktop shell, installers and self-update entirely; the server
and its web dashboard ship as a container, updated by pulling a new
image tag.

Link against the official prebuilt libduckdb instead of compiling it
from source via the bundled feature, cutting image build time from
15+ minutes to about 3. Fix a real bug found along the way: as PID 1
in a container, the daemon ignored SIGTERM and was always killed
after the stop timeout; it now shuts down gracefully.

Also corrects the Pillar 1 beaconing score: the period estimate had a
harmonic alias (a perfectly periodic 4h series resolved to 600s), and
the old score formula flagged pure random traffic as suspicious
(p95 = 0.839 on Poisson noise). The new formula was validated against
300 samples per scenario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-31 19:54:45 -03:00

221 lines
6.9 KiB
JavaScript

import { setLang, tr } from './i18n.js';
const $ = (selector) => document.querySelector(selector);
const connection = $('#connection');
const connectionText = $('#connectionText');
const version = $('#version');
const updateBtn = $('#updateBtn');
const datasetPath = $('#datasetPath');
const analyzeBtn = $('#analyzeBtn');
const refreshBtn = $('#refreshBtn');
const output = $('#output');
const alertList = $('#alertList');
const settingsDlg = $('#settingsDlg');
const settingsBtn = $('#settingsBtn');
const closeSettings = $('#closeSettings');
const doneSettings = $('#doneSettings');
const langSel = $('#langSel');
const themePick = $('#themePick');
let lastAlerts = [];
let themePreference = localStorage.getItem('acaua-theme') || 'auto';
async function fetchJson(path, options = {}) {
const response = await fetch(path, options);
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.error || response.statusText);
return body;
}
async function call(command, args = {}) {
switch (command) {
case 'app_info':
return fetchJson('/api/status');
case 'analyze_dataset':
return fetchJson('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args),
});
case 'list_alerts':
return fetchJson('/api/alerts');
case 'update_check':
return fetchJson('/api/update');
case 'doctor_report':
return fetchJson('/api/health');
default:
throw new Error(`unsupported command: ${command}`);
}
}
function message(template, ...values) {
let index = 0;
return tr(template).replace(/\{n\}/g, () => values[index++] ?? '');
}
function renderAlerts(alerts) {
lastAlerts = Array.isArray(alerts) ? alerts : [];
alertList.replaceChildren();
if (!lastAlerts.length) {
const empty = document.createElement('li');
empty.className = 'empty';
empty.textContent = tr('Nenhum alerta ainda.');
alertList.append(empty);
return;
}
for (const alert of lastAlerts) {
const item = document.createElement('li');
const meta = document.createElement('div');
meta.className = 'alert-meta';
const pillar = document.createElement('strong');
pillar.textContent = message('Pilar {n}', alert.pillar);
const score = document.createElement('span');
score.className = 'mono';
score.textContent = Number(alert.score).toFixed(3);
meta.append(pillar, score);
const summary = document.createElement('div');
summary.className = 'alert-summary';
summary.setAttribute('data-i18n-skip', '');
summary.textContent = alert.summary;
item.append(meta, summary);
alertList.append(item);
}
}
async function refreshStatus() {
try {
const info = await call('app_info');
version.textContent = `v${info.version}`;
connectionText.textContent = tr('Serviço local ativo');
connection.classList.remove('offline');
} catch (error) {
connectionText.textContent = tr('Serviço indisponível');
connection.classList.add('offline');
version.textContent = '—';
console.error(error);
}
}
async function refreshAlerts() {
try {
renderAlerts(await call('list_alerts'));
} catch (error) {
console.error(error);
}
}
async function analyzeDataset() {
const path = datasetPath.value.trim();
if (!path) {
output.textContent = tr('Nenhum dataset selecionado.');
return;
}
analyzeBtn.disabled = true;
output.textContent = tr('Analisando…');
try {
const report = await call('analyze_dataset', { path });
output.textContent = message(
'Análise concluída: {n} registros e {n} alertas.',
report.records,
report.alerts.length,
);
await refreshAlerts();
} catch (error) {
output.textContent = message('Falha na análise: {n}', error.message);
} finally {
analyzeBtn.disabled = false;
}
}
async function checkUpdate() {
updateBtn.disabled = true;
updateBtn.textContent = tr('Verificando atualização…');
try {
const status = await call('update_check');
updateBtn.textContent = status.update_available
? message('Atualização {n} disponível.', status.latest_version)
: message('Versão {n} é a mais recente.', status.current_version);
} catch (error) {
updateBtn.title = message('Não foi possível verificar a atualização: {n}', error.message);
updateBtn.textContent = tr('checar atualização');
} finally {
updateBtn.disabled = false;
window.setTimeout(() => {
updateBtn.textContent = tr('checar atualização');
updateBtn.title = '';
}, 6000);
}
}
function applyTheme(preference) {
themePreference = preference;
localStorage.setItem('acaua-theme', preference);
const light = window.matchMedia('(prefers-color-scheme: light)').matches;
const effective = preference === 'auto' ? (light ? 'light' : 'dark') : preference;
document.documentElement.dataset.theme = effective;
themePick.querySelectorAll('[data-theme-pick]').forEach((button) => {
button.classList.toggle('active', button.dataset.themePick === preference);
});
}
function openSettings() {
langSel.value = localStorage.getItem('acaua-lang') || 'auto';
settingsDlg.showModal();
}
function closeDialog() {
settingsDlg.close();
}
function setActiveNav(target) {
document.querySelectorAll('.nav-item').forEach((button) => {
button.classList.toggle('active', button.dataset.target === target);
});
}
function init() {
applyTheme(themePreference);
langSel.value = localStorage.getItem('acaua-lang') || 'auto';
setLang(langSel.value);
refreshStatus();
refreshAlerts();
analyzeBtn.addEventListener('click', analyzeDataset);
datasetPath.addEventListener('keydown', (event) => {
if (event.key === 'Enter') analyzeDataset();
});
refreshBtn.addEventListener('click', async () => {
await refreshStatus();
await refreshAlerts();
});
updateBtn.addEventListener('click', checkUpdate);
settingsBtn.addEventListener('click', openSettings);
closeSettings.addEventListener('click', closeDialog);
doneSettings.addEventListener('click', closeDialog);
langSel.addEventListener('change', () => {
localStorage.setItem('acaua-lang', langSel.value);
setLang(langSel.value);
refreshStatus();
renderAlerts(lastAlerts);
updateBtn.textContent = tr('checar atualização');
});
themePick.addEventListener('click', (event) => {
const button = event.target.closest('[data-theme-pick]');
if (button) applyTheme(button.dataset.themePick);
});
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
if (themePreference === 'auto') applyTheme('auto');
});
document.querySelectorAll('.nav-item').forEach((button) => {
button.addEventListener('click', () => {
const target = button.dataset.target;
setActiveNav(target);
if (target === 'settings') openSettings();
else document.getElementById(target)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
}
init();