chore(i18n): finish translating code comments and scripts to English
CI / Format and clippy (push) Successful in 1m13s
CI / Dependency audit (push) Successful in 13s
CI / Tests (Linux) (push) Successful in 1m35s
Release / Create release (push) Successful in 7s
CI / Check desktop app (Linux) (push) Successful in 3m19s
Release / CLI (linux-aarch64) (push) Successful in 1m3s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-aarch64) (push) Successful in 7m16s
Release / Desktop app (linux-x86_64) (push) Successful in 10m17s
Release / Desktop app (windows) (push) Successful in 4m54s
CI / Format and clippy (push) Successful in 1m13s
CI / Dependency audit (push) Successful in 13s
CI / Tests (Linux) (push) Successful in 1m35s
Release / Create release (push) Successful in 7s
CI / Check desktop app (Linux) (push) Successful in 3m19s
Release / CLI (linux-aarch64) (push) Successful in 1m3s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-aarch64) (push) Successful in 7m16s
Release / Desktop app (linux-x86_64) (push) Successful in 10m17s
Release / Desktop app (windows) (push) Successful in 4m54s
Completes the English-only standardization for code, comments, scripts and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en, untouched). Covers the remaining crates/, apps/gui/ui/ and apps/gui/src-tauri/ comments, .gitea/ workflow and template comments, .githooks/ hooks, and the scripts/ source comments for the files already renamed in a prior commit (check.sh, generate-logo.py, install-hooks.sh). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
1 parent
bf059fd0bc
commit
55e19a8e5b
59 files changed
+935
-879
No files matched your search
+19
-21
@@ -8,11 +8,11 @@
|
||||
--text-3: #8e8e93;
|
||||
--line: rgba(0, 0, 0, 0.09);
|
||||
|
||||
/* "canto" — cobre quente, o peito do sabiá-laranjeira */
|
||||
/* "song" — warm copper, the rufous-bellied thrush breast */
|
||||
--accent: #b45309;
|
||||
--accent-ink: #ffffff;
|
||||
--accent-soft: rgba(180, 83, 9, 0.11);
|
||||
/* "sinal" — ciano, a onda que o rádio enxerga */
|
||||
/* "signal" — cyan, the radio wave */
|
||||
--signal: #0891b2;
|
||||
--signal-soft: rgba(8, 145, 178, 0.12);
|
||||
|
||||
@@ -131,7 +131,7 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.page { max-width: 1040px; margin: 0 auto; padding: 30px 36px 100px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* -------------------------------------------------------- topo + linha de sinal */
|
||||
/* -------------------------------------------------------- topbar + signal line */
|
||||
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.topbar h1 { font-size: 21px; }
|
||||
.topbar .sub { color: var(--text-2); font-size: 13px; margin-top: 2px; }
|
||||
@@ -141,14 +141,14 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.wave.scanning path.live { opacity: 1; }
|
||||
.wave.scanning path.flat { opacity: 0; }
|
||||
|
||||
/* ------------------------------------------------------------ aviso */
|
||||
/* ------------------------------------------------------------ notice banner */
|
||||
.notice { display: flex; gap: 10px; background: var(--surface); border-radius: 12px; padding: 12px 14px; box-shadow: var(--shadow); font-size: 13px; color: var(--text-2); margin-bottom: 20px; align-items: flex-start; }
|
||||
.notice svg { flex: none; margin-top: 2px; color: var(--warn); }
|
||||
.notice.warn { background: var(--warn-soft); } .notice.warn svg { color: var(--warn); }
|
||||
.notice.bad { background: var(--bad-soft); } .notice.bad svg { color: var(--bad); }
|
||||
.notice b { color: var(--text); }
|
||||
|
||||
/* ------------------------------------------------------------ controles */
|
||||
/* ------------------------------------------------------------ controls */
|
||||
.controls { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px 18px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 22px; }
|
||||
.controls .group { display: flex; align-items: center; gap: 8px; }
|
||||
.pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); }
|
||||
@@ -166,7 +166,7 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.btn.live { background: var(--bad); color: #fff; }
|
||||
.btn.live:hover { filter: brightness(1.06); }
|
||||
|
||||
/* ------------------------------------------------------------ tabela */
|
||||
/* ------------------------------------------------------------ table */
|
||||
.result-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 2px 10px; }
|
||||
.result-summary { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
||||
.result-head strong { font-size: 15px; }
|
||||
@@ -186,7 +186,7 @@ tbody tr:hover { background: var(--surface-2); }
|
||||
td.ssid { font-weight: 600; }
|
||||
td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
||||
|
||||
/* rede agrupada por ESSID; cada BSSID vira uma linha expansível */
|
||||
/* network grouped by ESSID; each BSSID renders as an expandable row */
|
||||
.caret-cell { width: 34px; padding-left: 10px; padding-right: 0; }
|
||||
.expander { width: 26px; height: 26px; border: 0; border-radius: 8px; background: transparent; color: var(--text-3); cursor: pointer; display: inline-grid; place-items: center; padding: 0; }
|
||||
.expander:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
|
||||
@@ -227,7 +227,7 @@ td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
||||
.empty-state h2 { font-size: 17px; color: var(--text); margin-bottom: 6px; }
|
||||
.empty-state p { font-size: 13.5px; max-width: 360px; margin: 0 auto; }
|
||||
|
||||
/* ------------------------------------------------------------ configurações */
|
||||
/* ------------------------------------------------------------ settings */
|
||||
dialog { border: 0; border-radius: 20px; padding: 24px; width: min(420px, calc(100vw - 32px)); background: var(--surface); color: var(--text); box-shadow: 0 30px 80px rgba(0, 0, 0, .35); }
|
||||
dialog::backdrop { background: rgba(0, 0, 0, .35); backdrop-filter: blur(6px); }
|
||||
dialog h2 { font-size: 20px; margin-bottom: 16px; }
|
||||
@@ -238,7 +238,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.select option { background-color: var(--surface); color: var(--text); }
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ detalhes do alvo */
|
||||
/* ------------------------------------------------------------ target details */
|
||||
.ssid-btn { display: block; border: 0; padding: 0; background: transparent; color: inherit; font: inherit; font-weight: 600; text-align: left; cursor: pointer; line-height: 1.25; }
|
||||
.ssid-btn:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.bssid-item { cursor: pointer; }
|
||||
@@ -300,13 +300,11 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.empty-inline p { margin: 0; font-size: 13px; }
|
||||
.auth-check { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; margin: 4px 0 14px; }
|
||||
.auth-check input { width: 16px; height: 16px; accent-color: var(--accent); }
|
||||
/* Colunas CSS em vez de grid: os cards de ataque têm alturas muito
|
||||
diferentes entre si (Deauth/Handshake são enormes, PMF/WPS são curtos).
|
||||
Num grid de 2 colunas comum, o par mais curto de uma linha deixava um
|
||||
vão vazio enorme embaixo (a altura da linha é a do item mais alto, e
|
||||
align-items:start não estica o curto pra preencher). Colunas fazem os
|
||||
cards fluírem como alvenaria: cada um só ocupa a altura que precisa, o
|
||||
próximo entra logo abaixo, sem buracos. */
|
||||
/* CSS multi-column layout instead of grid: attack cards have very different
|
||||
heights (Deauth/Handshake are tall, PMF/WPS are short). In a standard
|
||||
2-column grid, the shorter card in a row left a large empty gap underneath.
|
||||
Multi-column masonry flow lets each card take only the height it needs
|
||||
with the next card stacking right below. */
|
||||
.attack-grid { column-count: 2; column-gap: 12px; }
|
||||
.attack-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; min-width: 0; break-inside: avoid; margin: 0 0 12px; display: inline-block; width: 100%; }
|
||||
.attack-deauth, .attack-wide { column-span: all; }
|
||||
@@ -336,7 +334,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.crack-bar i { display: block; width: 35%; height: 100%; border-radius: 999px; background: var(--accent); animation: crack-slide 1.1s ease-in-out infinite; }
|
||||
@keyframes crack-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
|
||||
|
||||
/* ------------------------------------------------------------ responsivo */
|
||||
/* ------------------------------------------------------------ responsive */
|
||||
@media (max-width: 760px) {
|
||||
.app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
|
||||
.sidebar { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; align-items: center; overflow-x: auto; }
|
||||
@@ -359,7 +357,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.detail-card, .attack-card { padding: 12px; }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- análise de canais */
|
||||
/* --------------------------------------------------- channel analysis */
|
||||
.top-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.view-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 7%, transparent); }
|
||||
.view-switch button { border: 0; border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; color: var(--text-2); background: transparent; cursor: pointer; }
|
||||
@@ -401,7 +399,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.chan-aps { color: var(--text-3); font-size: 11px; white-space: nowrap; }
|
||||
.chan-best .chan-n, .chan-best .chan-aps { color: var(--signal); }
|
||||
|
||||
/* ------------------------------------------------- saúde do espectro (SMART) */
|
||||
/* ------------------------------------------------- spectrum health (SMART) */
|
||||
.chan-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin: 2px 0 16px; }
|
||||
.chan-tile { background: var(--surface-2); border-radius: 14px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; box-shadow: 0 0 0 1px var(--line); }
|
||||
.chan-tile small { color: var(--text-3); font-size: 12px; }
|
||||
@@ -424,7 +422,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.chan-table tr.chan-best-row td { color: var(--signal); font-weight: 600; }
|
||||
.mini-tag { font-size: 8.5px; padding: 1px 4px; border-radius: 4px; background: var(--warn-soft); color: var(--warn); }
|
||||
|
||||
/* ------------------------------------------------ clientes e linha do tempo */
|
||||
/* ------------------------------------------------ clients and timeline */
|
||||
.spark { display: inline-block; width: 62px; height: 16px; vertical-align: middle; margin-left: 6px; }
|
||||
.spark path { fill: none; stroke: var(--signal); stroke-width: 1.4; }
|
||||
.spark-big { display: block; width: 100%; height: 70px; margin: 10px 0 0; }
|
||||
@@ -439,7 +437,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
||||
.tl-probe { color: var(--signal); }
|
||||
.tl-detail { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ------------------------------------------------------------- WPS e relatório */
|
||||
/* ------------------------------------------------------------- WPS and report */
|
||||
.term { margin: 10px 0 0; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); font: 11.5px/1.5 var(--mono); white-space: pre-wrap; max-height: 180px; overflow: auto; }
|
||||
.report-dlg { width: min(1060px, 96vw); height: 92vh; max-height: 92vh; padding: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.report-dlg:not([open]) { display: none; }
|
||||
|
||||
+65
-67
@@ -53,11 +53,11 @@ function applyTheme(t) {
|
||||
|
||||
function updateThemePref(t) {
|
||||
applyTheme(t);
|
||||
try { localStorage.setItem('sabia-theme', t); } catch { /* sem armazenamento */ }
|
||||
try { localStorage.setItem('sabia-theme', t); } catch { /* storage unavailable */ }
|
||||
}
|
||||
|
||||
let themePref = 'auto';
|
||||
try { themePref = localStorage.getItem('sabia-theme') || 'auto'; } catch { /* sem armazenamento */ }
|
||||
try { themePref = localStorage.getItem('sabia-theme') || 'auto'; } catch { /* storage unavailable */ }
|
||||
applyTheme(themePref);
|
||||
|
||||
document.querySelectorAll('[data-theme-pick]').forEach((b) => {
|
||||
@@ -67,7 +67,7 @@ document.querySelectorAll('[data-theme-pick]').forEach((b) => {
|
||||
try {
|
||||
const sort = localStorage.getItem('sabia-sort');
|
||||
if (sort && [...els.sortSel.options].some((o) => o.value === sort)) els.sortSel.value = sort;
|
||||
} catch { /* sem armazenamento */ }
|
||||
} catch { /* storage unavailable */ }
|
||||
|
||||
els.settingsBtn.addEventListener('click', () => {
|
||||
if (!els.settingsDlg.open) els.settingsDlg.showModal();
|
||||
@@ -75,14 +75,14 @@ els.settingsBtn.addEventListener('click', () => {
|
||||
|
||||
function applyLangPref() {
|
||||
let pref = 'auto';
|
||||
try { pref = localStorage.getItem('sabia-lang') || 'auto'; } catch { /* ignora */ }
|
||||
try { pref = localStorage.getItem('sabia-lang') || 'auto'; } catch { /* ignore */ }
|
||||
els.langSel.value = pref;
|
||||
setLang(pref === 'auto' ? undefined : pref);
|
||||
}
|
||||
|
||||
els.langSel.addEventListener('change', () => {
|
||||
const pref = els.langSel.value;
|
||||
try { localStorage.setItem('sabia-lang', pref); } catch { /* ignora */ }
|
||||
try { localStorage.setItem('sabia-lang', pref); } catch { /* ignore */ }
|
||||
setLang(pref === 'auto' ? undefined : pref);
|
||||
});
|
||||
|
||||
@@ -90,12 +90,12 @@ let ifaces = [];
|
||||
let current = null;
|
||||
let scanning = false;
|
||||
|
||||
// Ordem de chegada estável por rede/BSSID: sem isso, cada poll reordenava a
|
||||
// lista pelo sinal instantâneo e as linhas ficavam "flutuando" de posição a
|
||||
// cada atualização, tornando impossível clicar no alvo certo. A ordem de
|
||||
// chegada serve de critério de desempate (e de ordenação padrão), então um
|
||||
// item só muda de posição se o usuário trocar o modo de ordenação de
|
||||
// verdade — não por uma flutuação de -1 dBm entre um poll e outro.
|
||||
// Stable arrival order per network/BSSID: without this, each poll resorted the
|
||||
// list by instantaneous RSSI and rows jumped around on every
|
||||
// update, making it hard to click a target. Arrival order
|
||||
// serves as the tiebreaker (and default sort), so an
|
||||
// item only moves when the user explicitly changes the sort mode
|
||||
// rather than on a 1 dBm RSSI fluctuation between polls.
|
||||
const firstSeenOrder = new Map();
|
||||
let firstSeenSeq = 0;
|
||||
function orderOf(key) {
|
||||
@@ -115,8 +115,8 @@ async function api(path, opts) {
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// No app desktop não existe servidor HTTP: as mesmas rotas viram chamadas
|
||||
// IPC diretas pro núcleo, pelos comandos registrados em src-tauri/src/main.rs.
|
||||
// In the desktop app there is no HTTP server: the same routes map to direct
|
||||
// IPC calls into the core via the Tauri commands in src-tauri/src/main.rs.
|
||||
async function tauriApi(path, opts = {}) {
|
||||
const [p, qs] = path.split('?');
|
||||
const q = new URLSearchParams(qs || '');
|
||||
@@ -280,8 +280,8 @@ function renderEmptyResult(message, hint) {
|
||||
els.resultArea.innerHTML = `<div class="ap-card empty-state">${ICON_EMPTY}<h2>${message}</h2><p>${hint}</p></div>`;
|
||||
}
|
||||
|
||||
// Só troca o HTML quando muda de verdade: o poll de 1 s não deve
|
||||
// reconstruir a tabela (e perder foco/hover) à toa.
|
||||
// Only update innerHTML when content actually changes: the 1s poll must not
|
||||
// rebuild the DOM table (and drop focus/hover) unnecessarily.
|
||||
function setResultsHtml(html) {
|
||||
if (els.resultArea._h === html) return;
|
||||
els.resultArea._h = html;
|
||||
@@ -352,9 +352,9 @@ els.btnScan.addEventListener('click', async () => {
|
||||
if (!scanning) {
|
||||
const res = await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
|
||||
if (res && res.ok === false) {
|
||||
// Sem isso, uma falha ao abrir a captura (permissão, interface saiu do
|
||||
// modo monitor, etc.) ficava muda: o botão virava "parar captura" e a
|
||||
// tela mostrava "escutando o ar" pra sempre, sem nenhuma rede chegar.
|
||||
// Without this, a failure to start capture (permissions, interface left
|
||||
// monitor mode, etc.) was silent: the button flipped to "stop capture" and
|
||||
// the screen showed "listening" forever without any networks arriving.
|
||||
alert('Não foi possível iniciar a captura: ' + (res.error || 'erro desconhecido'));
|
||||
return;
|
||||
}
|
||||
@@ -398,7 +398,7 @@ function signalOf(ap) {
|
||||
}
|
||||
|
||||
function signalBars(dbm) {
|
||||
// dBm tipicamente entre -30 (ótimo) e -90 (péssimo)
|
||||
// dBm typically ranges from -30 (excellent) to -90 (weak)
|
||||
let level = 0;
|
||||
if (dbm != null) {
|
||||
if (dbm >= -55) level = 4;
|
||||
@@ -414,9 +414,9 @@ function signalBars(dbm) {
|
||||
return `<span class="signal-bars">${bars}</span>`;
|
||||
}
|
||||
|
||||
// Agrupa por ESSID: uma rede pode ter vários BSSIDs (rádios) e todos
|
||||
// pertencem ao mesmo alvo. Redes ocultas ficam separadas, pois sem SSID
|
||||
// não há como afirmar que dois BSSIDs são a mesma rede.
|
||||
// Group by ESSID: a network may have multiple BSSIDs (radios) belonging
|
||||
// to the same target. Hidden networks stay separate since without an SSID
|
||||
// two BSSIDs cannot be assumed to belong to the same network.
|
||||
function buildGroups(aps) {
|
||||
const map = new Map();
|
||||
for (const ap of aps) {
|
||||
@@ -460,9 +460,9 @@ function filterGroups(groups) {
|
||||
g.aps.some((a) => a.bssid.toLowerCase().includes(q)));
|
||||
}
|
||||
|
||||
// Dessensibiliza o sinal antes de comparar: arredondar pro m\u00faltiplo de 4
|
||||
// dBm mais pr\u00f3ximo evita que uma oscila\u00e7\u00e3o de 1-2 dBm entre dois polls
|
||||
// inverta a ordem de dois alvos com sinal parecido a cada atualiza\u00e7\u00e3o.
|
||||
// Quantize RSSI before comparing: rounding to the nearest multiple of 4
|
||||
// dBm prevents a 1-2 dBm fluctuation between polls from
|
||||
// flipping the order of two targets with similar signal strength.
|
||||
function signalBucket(v) {
|
||||
return v == null ? -1000 : Math.round(v / 4) * 4;
|
||||
}
|
||||
@@ -535,7 +535,7 @@ function renderGroup(g) {
|
||||
let lastAps = [];
|
||||
const openGroups = new Set();
|
||||
let viewMode = 'nets';
|
||||
try { viewMode = localStorage.getItem('sabia-view') || 'nets'; } catch { /* sem armazenamento */ }
|
||||
try { viewMode = localStorage.getItem('sabia-view') || 'nets'; } catch { /* storage unavailable */ }
|
||||
let channelBand = null;
|
||||
let reportState = { iface: null, pages: 1, zoom: 1, url: null };
|
||||
let reportMeasureTimer = null;
|
||||
@@ -632,8 +632,8 @@ function spectrumSvg(band) {
|
||||
|
||||
const CHAN_TONE_WORD = { ok: 'Boa', warn: 'Atenção', bad: 'Saturado' };
|
||||
|
||||
/** Estado geral da banda: mesma lógica de "saúde" usada nos tons por canal,
|
||||
* só que aplicada ao canal recomendado — é o que decide o tom do card inteiro. */
|
||||
/** Overall band status: applies the per-channel health logic to the
|
||||
* recommended channel to determine the summary card tone. */
|
||||
function bandHealth(band, rec) {
|
||||
if (!rec) return { tone: 'bad', label: 'Sem canal livre nessa banda' };
|
||||
if (rec.score >= 1.5) return { tone: 'bad', label: 'Até o melhor canal está saturado' };
|
||||
@@ -752,7 +752,7 @@ function renderResults(aps) {
|
||||
|
||||
function setView(mode) {
|
||||
viewMode = mode === 'channels' ? 'channels' : 'nets';
|
||||
try { localStorage.setItem('sabia-view', viewMode); } catch { /* ignora */ }
|
||||
try { localStorage.setItem('sabia-view', viewMode); } catch { /* ignore */ }
|
||||
document.querySelectorAll('#viewNav [data-view]').forEach((b) => {
|
||||
b.setAttribute('aria-pressed', String(b.dataset.view === viewMode));
|
||||
});
|
||||
@@ -779,7 +779,7 @@ els.resultArea.addEventListener('click', (e) => {
|
||||
|
||||
els.searchInput.addEventListener('input', () => renderResults(lastAps));
|
||||
els.sortSel.addEventListener('change', () => {
|
||||
try { localStorage.setItem('sabia-sort', els.sortSel.value); } catch { /* sem armazenamento */ }
|
||||
try { localStorage.setItem('sabia-sort', els.sortSel.value); } catch { /* storage unavailable */ }
|
||||
renderResults(lastAps);
|
||||
});
|
||||
els.resultArea.addEventListener('click', (e) => {
|
||||
@@ -803,7 +803,7 @@ els.resultArea.addEventListener('keydown', (e) => {
|
||||
openTarget(target.dataset.bssid);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- alvo
|
||||
// ---------------------------------------------------------------- target
|
||||
let lastSnapshot = null;
|
||||
let selectedBssid = null;
|
||||
let activeTab = 'overview';
|
||||
@@ -1056,8 +1056,8 @@ const EVENT_LABEL = {
|
||||
deauth: 'desautenticado',
|
||||
};
|
||||
|
||||
/** Sparkline em tamanho real (não o mini de 62px da tabela), pra caber bem
|
||||
* sozinho dentro do card de sinal do detalhe do cliente. */
|
||||
/** Full-size RSSI sparkline (wider than the 62px table mini-chart) for the
|
||||
* client detail signal card. */
|
||||
function bigSparkline(history) {
|
||||
const points = (history || []).slice(-60);
|
||||
if (points.length < 2) return '<p class="hint">Sem amostras de sinal suficientes ainda.</p>';
|
||||
@@ -1077,9 +1077,8 @@ function bigSparkline(history) {
|
||||
return `<svg class="spark spark-big" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true"><path d="${d}"/></svg>`;
|
||||
}
|
||||
|
||||
/** Visão completa de um cliente: tudo que o Sabiá já capturou sobre ele —
|
||||
* identificação, rede, sinal e a linha do tempo de eventos — num painel só,
|
||||
* em vez de ter que adivinhar olhando a linha compacta da tabela. */
|
||||
/** Detailed client view: consolidates everything captured about a station —
|
||||
* identity hints, associated AP, signal history, and event timeline — in one modal. */
|
||||
function renderClientDetail(clients) {
|
||||
const client = clients.find((c) => c.mac === attack.timelineMac);
|
||||
if (!client) return '';
|
||||
@@ -1522,11 +1521,11 @@ async function loadSnapshot() {
|
||||
}
|
||||
}
|
||||
|
||||
// Enquanto startDeauthJob está religando a captura e esperando o AP
|
||||
// reaparecer na varredura, a sessão nova responde vazia por alguns ciclos
|
||||
// de poll — sem essa trava, renderTarget() interpreta "AP não visto ainda"
|
||||
// como "AP não existe mais" e fecha o diálogo debaixo do usuário no meio
|
||||
// da própria ação que ele acabou de pedir.
|
||||
// While startDeauthJob is restarting capture and waiting for the AP
|
||||
// to reappear in the scan, the fresh session returns empty for a few poll
|
||||
// cycles — without this guard, renderTarget() treats "AP not seen yet"
|
||||
// as "AP gone" and closes the modal underneath the user mid-action.
|
||||
//
|
||||
let resumingForBssid = null;
|
||||
|
||||
function renderTarget(force) {
|
||||
@@ -1671,13 +1670,13 @@ async function waitForBssidChannel(bssid, timeoutMs) {
|
||||
}
|
||||
|
||||
async function startDeauthJob(handshake) {
|
||||
// Fixa o alvo no início: o resto desta função faz esperas longas
|
||||
// (religar a captura, aguardar a rede reaparecer na varredura), e nesse
|
||||
// meio tempo o poll global pode não achar mais o AP na primeira resposta
|
||||
// vazia da sessão recém-reiniciada e fechar o diálogo, zerando
|
||||
// `selectedBssid`. Sem essa cópia local, o job acabava sendo disparado
|
||||
// com bssid=null ("MAC inválido: null") mesmo depois de localizar a rede
|
||||
// com sucesso.
|
||||
// Snapshot the target BSSID upfront: the rest of this function awaits
|
||||
// capture restart and AP rediscovery, during which the global poll could
|
||||
// see an empty initial snapshot and close the modal, clearing
|
||||
// `selectedBssid`.
|
||||
//
|
||||
//
|
||||
//
|
||||
const bssid = selectedBssid;
|
||||
const station = document.getElementById('deauthStation')?.value || '';
|
||||
const direction = document.getElementById('deauthDirection')?.value || attack.direction;
|
||||
@@ -1688,10 +1687,10 @@ async function startDeauthJob(handshake) {
|
||||
setResult('deauthResult', 'Escolha um cliente para desautenticar nas direções sta/both.', true);
|
||||
return;
|
||||
}
|
||||
// Essa ação exige uma sessão de captura ativa (é o que guarda o handshake
|
||||
// montado); sem isso o backend recusa com "inicie a captura antes do
|
||||
// job". Em vez de obrigar o usuário a voltar e religar manualmente, retoma
|
||||
// a captura aqui — do jeito que ele descreveu querer.
|
||||
// This action requires an active capture session (which stores the assembled
|
||||
// handshake); rather than forcing the user to manually restart capture,
|
||||
// resume capture automatically here.
|
||||
//
|
||||
if (!scanning) {
|
||||
const scanRes = await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
|
||||
if (scanRes && scanRes.ok === false) {
|
||||
@@ -1702,12 +1701,12 @@ async function startDeauthJob(handshake) {
|
||||
els.btnScan.textContent = 'parar captura';
|
||||
els.btnScan.classList.add('live');
|
||||
els.wave.classList.add('scanning');
|
||||
// A captura acabou de reiniciar: a sessão ainda não sabe em qual canal
|
||||
// este BSSID está, e o job travaria o canal errado (o canal em que o
|
||||
// scanner por acaso já estava saltando) em vez do canal real do AP —
|
||||
// os quadros de deauth saem no vácuo e o job termina sem efeito. Espera
|
||||
// o AP reaparecer no snapshot (ele já esteve no ar, é questão de
|
||||
// segundos de varredura) antes de disparar o job.
|
||||
// Capture just restarted: the session does not yet know which channel
|
||||
// this BSSID is on, so starting the job immediately would lock the wrong
|
||||
// channel. Wait briefly for the AP to reappear in the scan snapshot
|
||||
// before launching the job.
|
||||
//
|
||||
//
|
||||
setResult('deauthResult', 'Retomando a captura e localizando a rede…');
|
||||
resumingForBssid = bssid;
|
||||
let found;
|
||||
@@ -2370,10 +2369,9 @@ async function poll() {
|
||||
if (scanning && current) {
|
||||
const snap = await api(`/api/scan/snapshot?iface=${encodeURIComponent(current)}`);
|
||||
if (snap?.access_points && snap.running === false) {
|
||||
// A sessão existe e respondeu normal (200), mas o próprio engine
|
||||
// marcou running=false: a thread de captura morreu sozinha (erro de
|
||||
// I/O, adaptador caiu da USB, etc.). Sem isso, a UI continuava achando
|
||||
// que ainda estava capturando e nunca contava o motivo real.
|
||||
// The session exists and returned HTTP 200, but the engine marked
|
||||
// running=false because the capture thread died on its own (I/O error,
|
||||
// USB adapter unplugged, etc.). Surface the actual reason to the UI.
|
||||
scanning = false;
|
||||
els.btnScan.textContent = 'iniciar captura';
|
||||
els.btnScan.classList.remove('live');
|
||||
@@ -2386,10 +2384,10 @@ async function poll() {
|
||||
renderResults(snap.access_points);
|
||||
if (selectedBssid) renderTarget(false);
|
||||
} else if (snap && snap.ok === false) {
|
||||
// A captura morreu no meio (interface saiu do monitor, processo externo
|
||||
// tomou o adaptador, etc.). Antes disso caía no fallback de baixo, que
|
||||
// sempre responde 200 com lista vazia — a UI continuava "escutando o
|
||||
// ar" indefinidamente sem nenhuma rede aparecer e sem avisar por quê.
|
||||
// Capture stopped mid-flight (interface left monitor mode, external
|
||||
// process claimed the adapter, etc.). Without this branch, the fallback
|
||||
// below returns 200 with an empty list and the UI keeps listening
|
||||
// indefinitely without any network appearing or explaining why.
|
||||
scanning = false;
|
||||
els.btnScan.textContent = 'iniciar captura';
|
||||
els.btnScan.classList.remove('live');
|
||||
@@ -2405,7 +2403,7 @@ async function poll() {
|
||||
}
|
||||
|
||||
async function checkElevation() {
|
||||
if (!tauri) return; // via `sabia ui` a elevação é responsabilidade do terminal (sudo)
|
||||
if (!tauri) return; // in `sabia ui` mode privilege elevation is handled by the terminal (sudo)
|
||||
const notice = document.getElementById('elevateNotice');
|
||||
const msg = document.getElementById('elevateMsg');
|
||||
const retry = document.getElementById('btnElevateRetry');
|
||||
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor
|
||||
// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e
|
||||
// continua observando a página para traduzir o que for desenhado depois.
|
||||
// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} redes" -> "{n} networks").
|
||||
// UI language. Source strings are in Portuguese; in English mode a translator
|
||||
// replaces displayed strings (text nodes and attributes) using a dictionary, and
|
||||
// keeps observing the page to translate dynamically rendered elements.
|
||||
// Variable placeholders use {n} in the dictionary (e.g. "{n} networks").
|
||||
|
||||
import { EN } from './i18n-en.js';
|
||||
|
||||
@@ -16,7 +16,7 @@ export function resolveLang(pref) {
|
||||
}
|
||||
|
||||
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
||||
// Dados do usuário (SSID, BSSID, MAC, valores mono) nunca são traduzidos.
|
||||
// User data (SSID, BSSID, MAC, monospace values) is never translated.
|
||||
const SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style';
|
||||
|
||||
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
||||
@@ -26,7 +26,7 @@ const patterns = Object.entries(EN)
|
||||
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
||||
})
|
||||
// padrões mais específicos (mais texto fixo) primeiro
|
||||
// more specific patterns (more fixed text) first
|
||||
.sort((a, b) => b.re.source.length - a.re.source.length);
|
||||
|
||||
const cache = new Map();
|
||||
@@ -43,7 +43,7 @@ export function tr(text) {
|
||||
const m = core.match(p.re);
|
||||
if (m) {
|
||||
let i = 0;
|
||||
// os valores capturados também podem ter partes traduzíveis (ex.: estados)
|
||||
// captured values may also contain translatable parts (e.g. status labels)
|
||||
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
||||
break;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function tr(text) {
|
||||
return lead + out + trail;
|
||||
}
|
||||
|
||||
const orig = new WeakMap(); // nó -> texto original em português
|
||||
const orig = new WeakMap(); // node -> original source text
|
||||
let observer = null;
|
||||
|
||||
function translateNode(n) {
|
||||
|
||||
Vendored
+6
-6
@@ -1,10 +1,10 @@
|
||||
# Paged.js
|
||||
|
||||
`paged.polyfill.min.js` é o polyfill do [Paged.js](https://pagedjs.org/), versão
|
||||
0.4.3, usado para paginar os relatórios em A4 no navegador.
|
||||
`paged.polyfill.min.js` is the [Paged.js](https://pagedjs.org/) polyfill, version
|
||||
0.4.3, used to paginate A4 reports in the browser.
|
||||
|
||||
- Projeto: https://gitlab.coko.foundation/pagedjs/pagedjs
|
||||
- Licença: MIT (texto em `PAGEDJS-LICENSE`)
|
||||
- Arquivo: `paged.polyfill.min.js` (build de distribuição, não modificado)
|
||||
- Project: https://gitlab.coko.foundation/pagedjs/pagedjs
|
||||
- License: MIT (text in `PAGEDJS-LICENSE`)
|
||||
- File: `paged.polyfill.min.js` (unmodified distribution build)
|
||||
|
||||
O arquivo é vendorizado para que o relatório funcione offline, sem CDN.
|
||||
Vendorized so report generation works completely offline without external CDNs.
|
||||
Reference in new issue
Block a user