const params = new URLSearchParams(location.search);
const token = params.get('token');
const tauri = window.__TAURI__;
const els = {
list: document.getElementById('ifaceList'),
ifaceSub: document.getElementById('ifaceSub'),
modePill: document.getElementById('modePill'),
modeLabel: document.getElementById('modeLabel'),
btnMonitor: document.getElementById('btnMonitor'),
btnScan: document.getElementById('btnScan'),
resultArea: document.getElementById('resultArea'),
count: document.getElementById('count'),
wave: document.getElementById('wave'),
verInfo: document.getElementById('verInfo'),
btnUpdate: document.getElementById('btnUpdate'),
};
let ifaces = [];
let current = null;
let scanning = false;
const ICON_WIFI = ``;
const ICON_EMPTY = ``;
async function api(path, opts) {
if (tauri) return tauriApi(path);
const url = new URL(path, location.origin);
url.searchParams.set('token', token);
const r = await fetch(url, 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.
async function tauriApi(path) {
const [p, qs] = path.split('?');
const q = new URLSearchParams(qs || '');
const invoke = tauri.core.invoke;
try {
switch (p) {
case '/api/version':
return { version: await invoke('version') };
case '/api/ifaces':
return await invoke('list_ifaces');
case '/api/monitor':
await invoke('set_monitor', { iface: q.get('iface'), enable: q.get('enable') === 'true' });
return { ok: true };
case '/api/scan/start':
await invoke('start_scan', { iface: q.get('iface') });
return { ok: true };
case '/api/scan/stop':
await invoke('stop_scan', { iface: q.get('iface') });
return { ok: true };
case '/api/scan':
return await invoke('scan_results', { iface: q.get('iface') });
case '/api/update/check':
return await invoke('update_check');
default:
return { error: 'rota desconhecida' };
}
} catch (e) {
return { ok: false, error: String(e) };
}
}
function renderEmptyResult(message, hint) {
els.resultArea.innerHTML = `
${ICON_EMPTY}
${message}
${hint}
`;
}
function renderIfaceList() {
els.list.innerHTML = '';
for (const i of ifaces) {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.className = 'dev';
btn.setAttribute('aria-current', String(i.name === current));
btn.innerHTML = `${ICON_WIFI}${i.name}${i.mac}`;
btn.addEventListener('click', () => selectIface(i.name));
li.appendChild(btn);
els.list.appendChild(li);
}
}
function selectIface(name) {
current = name;
scanning = false;
renderIfaceList();
refreshModeUi();
renderEmptyResult('Pronto pra capturar', 'Ative o modo monitor e inicie a captura pra ver as redes no ar.');
}
function refreshModeUi() {
const iface = ifaces.find((i) => i.name === current);
const on = !!iface && iface.mode === 'Monitor';
els.ifaceSub.textContent = current ? `${current} · ${iface?.mac ?? ''}` : 'selecione uma interface';
els.modeLabel.textContent = on ? 'modo monitor' : 'modo gerenciado';
els.modePill.className = 'pill' + (on ? ' on' : '');
els.btnMonitor.disabled = !current;
els.btnMonitor.textContent = on ? 'desativar modo monitor' : 'ativar modo monitor';
els.btnScan.disabled = !on;
}
async function loadIfaces() {
ifaces = await api('/api/ifaces');
if (!current && ifaces.length) current = ifaces[0].name;
renderIfaceList();
refreshModeUi();
if (current) renderEmptyResult('Pronto pra capturar', 'Ative o modo monitor e inicie a captura pra ver as redes no ar.');
else renderEmptyResult('Nenhuma interface Wi-Fi', 'Conecte um adaptador compatível com modo monitor e recarregue.');
}
els.btnMonitor.addEventListener('click', async () => {
const iface = ifaces.find((i) => i.name === current);
const enable = !(iface && iface.mode === 'Monitor');
els.btnMonitor.disabled = true;
const res = await api(`/api/monitor?iface=${encodeURIComponent(current)}&enable=${enable}`, { method: 'POST' });
if (!res.ok) {
alert('Não foi possível trocar o modo: ' + (res.error || 'erro desconhecido'));
}
await loadIfaces();
});
els.btnScan.addEventListener('click', async () => {
if (!scanning) {
await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
scanning = true;
els.btnScan.textContent = 'parar captura';
els.btnScan.classList.add('live');
els.wave.classList.add('scanning');
} else {
await api(`/api/scan/stop?iface=${encodeURIComponent(current)}`, { method: 'POST' });
scanning = false;
els.btnScan.textContent = 'iniciar captura';
els.btnScan.classList.remove('live');
els.wave.classList.remove('scanning');
}
});
els.btnUpdate.addEventListener('click', async () => {
els.btnUpdate.textContent = 'checando…';
const status = await api('/api/update/check');
els.btnUpdate.textContent = status.update_available
? `atualizar para ${status.latest_version}`
: 'já atualizado';
});
function secClass(sec) {
if (!sec) return '';
return 'sec-' + sec.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
}
function signalBars(dbm) {
// dBm tipicamente entre -30 (ótimo) e -90 (péssimo)
let level = 0;
if (dbm != null) {
if (dbm >= -55) level = 4;
else if (dbm >= -65) level = 3;
else if (dbm >= -75) level = 2;
else level = 1;
}
const heights = [5, 8, 11, 14];
let bars = '';
for (let i = 0; i < 4; i++) {
bars += ``;
}
return `${bars}`;
}
function renderResults(aps) {
els.count.textContent = aps.length;
if (!aps.length) {
renderEmptyResult('Escutando o ar…', 'Nenhuma rede vista ainda. Isso pode levar alguns segundos enquanto os canais são varridos.');
return;
}
const rows = aps.map((ap) => `
| ${ap.ssid ? escapeHtml(ap.ssid) : 'oculta'} |
${ap.bssid} |
${ap.channel ?? '—'} |
${signalBars(ap.last_signal_dbm)}${ap.last_signal_dbm != null ? ap.last_signal_dbm + ' dBm' : '—'} |
${ap.security || '—'} |
${ap.clients.length} |
`).join('');
els.resultArea.innerHTML = `
| Rede | BSSID | Canal | Sinal | Segurança | Clientes |
${rows}
`;
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
let waveT = 0;
function animateWave() {
waveT += 0.12;
if (scanning) {
let d = 'M0 17 ';
for (let x = 0; x <= 600; x += 12) {
const y = 17 + Math.sin(x * 0.045 + waveT) * 9 * Math.sin(waveT * 0.6 + x * 0.002);
d += `L${x} ${y.toFixed(1)} `;
}
document.getElementById('waveLive').setAttribute('d', d);
}
requestAnimationFrame(animateWave);
}
async function poll() {
if (scanning && current) {
const aps = await api(`/api/scan?iface=${encodeURIComponent(current)}`);
renderResults(aps);
}
setTimeout(poll, 1000);
}
async function checkElevation() {
if (!tauri) return; // via `sabia ui` a elevação é responsabilidade do terminal (sudo)
const notice = document.getElementById('elevateNotice');
const msg = document.getElementById('elevateMsg');
const retry = document.getElementById('btnElevateRetry');
const elevated = await tauri.core.invoke('is_elevated').catch(() => true);
if (elevated) return;
notice.hidden = false;
msg.textContent = 'Pedindo privilégio de administrador — necessário pra modo monitor e captura…';
retry.hidden = true;
retry.addEventListener('click', async () => {
msg.textContent = 'Pedindo privilégio de administrador — necessário pra modo monitor e captura…';
retry.hidden = true;
await tauri.core.invoke('relaunch_elevated').catch((e) => {
msg.textContent = String(e);
retry.hidden = false;
});
});
tauri.event?.listen?.('elevate-failed', (e) => {
msg.textContent = String(e.payload || 'Não foi possível obter privilégio de administrador.');
retry.hidden = false;
});
}
(async function init() {
checkElevation();
const status = await api('/api/version');
els.verInfo.textContent = `v${status.version}`;
await loadIfaces();
animateWave();
poll();
})();