import { setLang, currentLang } from './i18n.js'; 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'), langSel: document.getElementById('langSel'), }; function applyLangPref() { let pref = 'auto'; try { pref = localStorage.getItem('sabia-lang') || 'auto'; } catch { /* ignora */ } 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 */ } setLang(pref === 'auto' ? undefined : pref); }); 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 = `
${rows}
RedeBSSIDCanalSinalSegurançaClientes
`; } 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() { applyLangPref(); checkElevation(); const status = await api('/api/version'); els.verInfo.textContent = `v${status.version}`; await loadIfaces(); animateWave(); poll(); })();