Files
argus/src/main.js
T

1272 lines
37 KiB
JavaScript

const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
var theme = (() => {
var STORAGE_KEY = 'argus-theme';
function get() {
try {
return localStorage.getItem(STORAGE_KEY) || 'cyber';
} catch (e) {
return 'cyber';
}
}
function set(name) {
if (name === 'cyber') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', name);
}
try {
localStorage.setItem(STORAGE_KEY, name);
} catch (e) { }
}
function init() {
var saved = get();
set(saved);
var picker = document.getElementById('theme-picker');
picker.value = saved;
picker.addEventListener('change', function () {
set(picker.value);
});
}
// Apply immediately (before DOMContentLoaded) to prevent flash
set(get());
return { init };
})();
const table = (() => {
var dirty = false;
var flushTimer = null;
var ipIndex = {};
let data = [];
let filtered = [];
let sortKey = 'ip';
let sortDir = 'asc';
let filterText = '';
let checkedIps = new Set();
var ROW_HEIGHT = 32;
var BUFFER_ROWS = 20;
var scrollContainer = null;
function ipToNum(ip) {
const parts = String(ip).split('.');
if (parts.length !== 4) return 0;
return parts.reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0;
}
function getSortVal(host, key) {
switch (key) {
case 'ip': return ipToNum(host.ip);
case 'status': return host.status || '';
case 'rtt': return host.ping ? host.ping.rtt_ms : 99999;
case 'hostname': return (host.hostname || '').toLowerCase();
case 'ports': return host.ports ? host.ports.filter((p) => p.state === 'Open').length : 0;
default: return '';
}
}
function formatPorts(host) {
if (!host.ports || host.ports.length === 0) return '-';
const open = host.ports.filter((p) => p.state === 'Open');
if (open.length === 0) return '-';
return open.map((p) => {
const svc = p.service ? p.service : p.port;
return '<span class="port-tag">' + p.port + '/' + (p.service || 'unknown') + '</span>';
}).join(' ');
}
function buildDetailHtml(host) {
let html = '<div class="detail-panel">';
html += '<div class="detail-header">';
html += '<span class="detail-ip">' + host.ip + '</span>';
if (host.hostname) html += '<span class="detail-hostname">' + host.hostname + '</span>';
if (host.ping) html += '<span class="detail-rtt">' + host.ping.rtt_ms.toFixed(1) + ' ms</span>';
html += '</div>';
if (host.netbios) {
html += '<div class="detail-netbios">';
if (host.netbios.computer_name)
html += '<span class="detail-tag">Computer: <strong>' + host.netbios.computer_name + '</strong></span>';
if (host.netbios.workgroup)
html += '<span class="detail-tag">Workgroup: <strong>' + host.netbios.workgroup + '</strong></span>';
if (host.netbios.mac_from_netbios)
html += '<span class="detail-tag">MAC: <strong>' + host.netbios.mac_from_netbios + '</strong></span>';
html += '</div>';
}
if (host.ports && host.ports.length > 0) {
const open = host.ports.filter((p) => p.state === 'Open');
const closed = host.ports.filter((p) => p.state === 'Closed');
const filtered = host.ports.filter((p) => p.state === 'Filtered');
if (open.length > 0) {
html += '<table class="detail-table">';
html += '<thead><tr>';
html += '<th>Port</th><th>Protocol</th><th>State</th><th>Service</th><th>Banner</th>';
html += '</tr></thead><tbody>';
for (const p of open) {
html += '<tr>';
html += '<td>' + p.port + '</td>';
html += '<td>' + p.protocol + '</td>';
html += '<td class="status-up">' + p.state + '</td>';
html += '<td>' + (p.service || '-') + '</td>';
html += '<td>' + (p.banner || '-') + '</td>';
html += '</tr>';
}
html += '</tbody></table>';
}
html += '<div class="detail-summary">';
html += '<span>' + open.length + ' open</span>';
html += '<span>' + closed.length + ' closed</span>';
html += '<span>' + filtered.length + ' filtered</span>';
html += '</div>';
} else {
html += '<p class="detail-empty">No port scan data. Enable "Simple Port Scan" and rescan.</p>';
}
html += '</div>';
return html;
}
let expandedIp = null;
function handleRowClick(e) {
const row = e.target.closest('tr');
if (!row || !row.dataset.ip) return;
const ip = row.dataset.ip;
// Remove any existing detail row
const existing = document.querySelector('.detail-row');
if (existing) existing.remove();
// If clicking the same row, just collapse
if (expandedIp === ip) {
expandedIp = null;
row.classList.remove('selected');
return;
}
// Deselect previous
const prev = document.querySelector('#results-body tr.selected');
if (prev) prev.classList.remove('selected');
// Find the host data
const host = data.find((h) => String(h.ip) === ip);
if (!host) return;
// Mark selected
row.classList.add('selected');
expandedIp = ip;
// Insert detail row after the clicked row
const detailRow = document.createElement('tr');
detailRow.className = 'detail-row';
const td = document.createElement('td');
td.colSpan = document.querySelectorAll('#results-table th').length;
td.innerHTML = buildDetailHtml(host);
detailRow.appendChild(td);
row.after(detailRow);
}
function refresh() {
if (filterText) {
const q = filterText.toLowerCase();
filtered = data.filter((h) =>
String(h.ip).includes(q) ||
(h.hostname || '').toLowerCase().includes(q) ||
(h.status || '').toLowerCase().includes(q)
);
} else {
filtered = [...data];
}
filtered.sort((a, b) => {
const va = getSortVal(a, sortKey);
const vb = getSortVal(b, sortKey);
let cmp = 0;
if (typeof va === 'number' && typeof vb === 'number') {
cmp = va - vb;
} else {
cmp = String(va).localeCompare(String(vb));
}
return sortDir === 'asc' ? cmp : -cmp;
});
render();
}
function render() {
var tbody = document.getElementById('results-body');
var count = document.getElementById('host-count');
if (!tbody) return;
expandedIp = null;
if (!scrollContainer) {
scrollContainer = document.getElementById('results-table-container');
scrollContainer.addEventListener('scroll', onScroll);
}
renderVisible();
if (count) {
var up = data.filter(function (h) { return h.status === 'Up'; }).length;
count.textContent = up + ' up / ' + data.length + ' total';
}
if (columns && columns.apply) columns.apply();
}
function renderVisible() {
var tbody = document.getElementById('results-body');
if (!tbody || !scrollContainer) return;
var totalRows = filtered.length;
var viewHeight = scrollContainer.clientHeight;
var scrollTop = scrollContainer.scrollTop;
var headerHeight = 36;
var startIdx = Math.max(0, Math.floor((scrollTop - headerHeight) / ROW_HEIGHT) - BUFFER_ROWS);
var visibleCount = Math.ceil(viewHeight / ROW_HEIGHT) + (BUFFER_ROWS * 2);
var endIdx = Math.min(totalRows, startIdx + visibleCount);
var topSpace = startIdx * ROW_HEIGHT;
var bottomSpace = (totalRows - endIdx) * ROW_HEIGHT;
var colCount = document.querySelectorAll('#results-table th').length;
var html = '';
if (topSpace > 0) {
html += '<tr class="virtual-spacer"><td colspan="' + colCount + '" style="height:' + topSpace + 'px"></td></tr>';
}
for (var i = startIdx; i < endIdx; i++) {
var host = filtered[i];
var ip = host.ip;
var status = host.status || 'Unknown';
var cls = 'status-' + status.toLowerCase();
var rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
var hostname = host.hostname || '-';
var isChecked = checkedIps.has(ip) ? 'checked' : '';
html += '<tr data-ip="' + ip + '">'
+ '<td class="col-check"><input type="checkbox" class="row-check" ' + isChecked + ' /></td>'
+ '<td>' + ip + '</td>'
+ '<td class="' + cls + '">' + status + '</td>'
+ '<td>' + rtt + '</td>'
+ '<td>' + hostname + '</td>'
+ '<td class="ports-cell">' + formatPorts(host) + '</td>'
+ '</tr>';
}
if (bottomSpace > 0) {
html += '<tr class="virtual-spacer"><td colspan="' + colCount + '" style="height:' + bottomSpace + 'px"></td></tr>';
}
tbody.innerHTML = html;
}
var scrollTimeout = null;
function onScroll() {
if (scrollTimeout) return;
scrollTimeout = requestAnimationFrame(function () {
scrollTimeout = null;
renderVisible();
if (columns && columns.apply) columns.apply();
});
}
function upsert(host) {
var idx = data.findIndex(function (h) { return h.ip === host.ip; });
if (idx >= 0) {
data[idx] = host;
} else {
ipIndex[host.ip] = data.length;
data.push(host);
}
dirty = true;
if (!flushTimer) {
flushTimer = setTimeout(flush, 200);
}
}
function flush() {
flushTimer = null;
if (!dirty) return;
dirty = false;
refresh();
}
function clear() {
data = [];
filtered = [];
checkedIps.clear();
ipIndex = {};
document.getElementById('check-all').checked = false;
refresh();
}
function setSort(key) {
if (sortKey === key) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortDir = 'asc';
}
document.querySelectorAll('#results-table th').forEach((th) => {
th.classList.remove('sorted-asc', 'sorted-desc');
if (th.dataset.sort === sortKey) {
th.classList.add(sortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
}
});
refresh();
}
function setFilter(text) {
filterText = text;
refresh();
}
return {
upsert, clear, setSort, setFilter, handleRowClick,
init: () => {
document.getElementById('results-body')
.addEventListener('click', (e) => {
// Don't expand detail panel when clicking a checkbox
if (e.target.classList.contains('row-check')) {
const row = e.target.closest('tr');
if (row && row.dataset.ip) {
if (e.target.checked) {
checkedIps.add(row.dataset.ip);
} else {
checkedIps.delete(row.dataset.ip);
}
}
return;
}
handleRowClick(e);
});
// Select all checkbox
document.getElementById('check-all').addEventListener('change', (e) => {
if (e.target.checked) {
filtered.forEach((h) => checkedIps.add(String(h.ip)));
} else {
checkedIps.clear();
}
render();
});
},
getAll: () => [...data],
getUp: () => data.filter((h) => h.status === 'Up'),
getIPs: () => data.map((h) => String(h.ip)),
getUpIPs: () => data.filter((h) => h.status === 'Up').map((h) => String(h.ip)),
getChecked: () => data.filter((h) => checkedIps.has(String(h.ip))),
getCheckedIPs: () => [...checkedIps],
};
})();
const scanner = (() => {
let scanning = false;
const els = {
input: () => document.getElementById('ip-range-input'),
btnScan: () => document.getElementById('btn-scan'),
btnStop: () => document.getElementById('btn-stop'),
status: () => document.getElementById('status-text'),
bar: () => document.getElementById('progress-bar'),
fill: () => document.getElementById('progress-fill'),
};
function setScanning(active) {
scanning = active;
els.btnScan().disabled = active;
els.btnStop().disabled = !active;
els.input().disabled = active;
els.bar().hidden = !active;
if (!active) els.fill().style.width = '0%';
}
async function start() {
const range = els.input().value.trim();
if (!range) {
els.status().textContent = 'Enter an IP range';
return;
}
table.clear();
setScanning(true);
els.status().textContent = 'Scanning...';
try {
const portScan = document.getElementById('chk-portscan').checked;
await invoke('start_scan', {
config: {
range: range,
timeout_ms: 1500,
port_scan: portScan,
ports: null,
port_timeout_ms: 1000,
},
});
} catch (err) {
els.status().textContent = 'Error: ' + err;
setScanning(false);
}
}
async function stop() {
try {
await invoke('stop_scan');
} catch (err) {
console.error('stop_scan error:', err);
}
els.status().textContent = 'Scan stopped';
setScanning(false);
}
async function setupListeners() {
await listen('scan:host-found', (e) => {
table.upsert(e.payload);
});
await listen('scan:progress', (e) => {
const p = e.payload;
const pct = Math.round((p.done / p.total) * 100);
els.fill().style.width = pct + '%';
els.status().textContent = 'Scanning... ' + p.done + ' / ' + p.total;
});
await listen('scan:complete', (e) => {
els.status().textContent = 'Scan complete (' + e.payload.total_hosts + ' hosts)';
setScanning(false);
});
}
function init() {
els.btnScan().addEventListener('click', start);
els.btnStop().addEventListener('click', stop);
els.input().addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !scanning) start();
});
setupListeners();
}
return { init };
})();
const exporter = (() => {
async function csvAll() {
const data = table.getAll();
if (data.length === 0) return;
await saveCSV(data);
}
async function csvUp() {
const data = table.getUp();
if (data.length === 0) return;
await saveCSV(data);
}
async function saveCSV(data) {
const { save } = window.__TAURI__.dialog;
const path = await save({
defaultPath: 'argus-scan.csv',
filters: [{ name: 'CSV', extensions: ['csv'] }],
});
if (!path) return;
try {
await invoke('export_csv', { request: { path: path, hosts: data } });
} catch (err) {
console.error('Export error:', err);
}
}
return { csvAll, csvUp };
})();
const columns = (() => {
const STORAGE_KEY = 'argus-columns';
const allCols = ['ip', 'status', 'rtt', 'hostname', 'ports'];
function getVisible() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) return JSON.parse(saved);
} catch (e) { }
return [...allCols];
}
function save(visible) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(visible));
}
function apply() {
const visible = getVisible();
allCols.forEach((col, i) => {
const show = visible.includes(col);
// Table headers
const th = document.querySelector('#results-table th[data-sort="' + col + '"]');
if (th) th.style.display = show ? '' : 'none';
// Table body cells (by column index)
const colIndex = allCols.indexOf(col) + 2;
document.querySelectorAll('#results-table tbody td:nth-child(' + colIndex + ')').forEach((td) => {
td.style.display = show ? '' : 'none';
});
});
}
function init() {
const btn = document.getElementById('btn-columns');
const picker = document.getElementById('col-picker');
const visible = getVisible();
// Set initial checkbox state from saved prefs
picker.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
if (!cb.disabled) {
cb.checked = visible.includes(cb.dataset.col);
}
});
// Toggle picker
btn.addEventListener('click', (e) => {
e.stopPropagation();
picker.hidden = !picker.hidden;
});
// Close picker when clicking outside
document.addEventListener('click', (e) => {
if (!picker.contains(e.target) && e.target !== btn) {
picker.hidden = true;
}
});
// Handle checkbox changes
picker.addEventListener('change', (e) => {
const cb = e.target;
if (!cb.dataset.col) return;
const current = getVisible();
if (cb.checked) {
if (!current.includes(cb.dataset.col)) current.push(cb.dataset.col);
} else {
const idx = current.indexOf(cb.dataset.col);
if (idx >= 0) current.splice(idx, 1);
}
save(current);
apply();
});
// Apply saved preferences on load
apply();
}
return { init, apply };
})();
const sender = (() => {
function init() {
const menus = [
{ btn: 'btn-send-nmap', menu: 'send-nmap-menu' },
{ btn: 'btn-send-monitor', menu: 'send-monitor-menu' },
];
menus.forEach(({ btn, menu }) => {
const btnEl = document.getElementById(btn);
const menuEl = document.getElementById(menu);
btnEl.addEventListener('click', (e) => {
e.stopPropagation();
// Close any other open send menus
document.querySelectorAll('.send-menu').forEach((m) => {
if (m !== menuEl) m.hidden = true;
});
menuEl.hidden = !menuEl.hidden;
});
});
// Close menus on outside click
document.addEventListener('click', () => {
document.querySelectorAll('.send-menu').forEach((m) => m.hidden = true);
});
// Handle send actions
document.querySelectorAll('.send-menu .ctx-item').forEach((item) => {
item.addEventListener('click', (e) => {
const action = e.target.dataset.send;
let hosts;
if (action.endsWith('-all')) hosts = table.getAll();
else if (action.endsWith('-up')) hosts = table.getUp();
else if (action.endsWith('-checked')) hosts = table.getChecked();
if (!hosts || hosts.length === 0) {
console.log('No hosts to send');
return;
}
if (action.startsWith('nmap')) {
nmapTab.setTarget(hosts.map((h) => String(h.ip)));
} else if (action.startsWith('monitor')) {
monitorTab.setTargets(hosts);
}
document.querySelectorAll('.send-menu').forEach((m) => m.hidden = true);
});
});
}
return { init };
})();
const ctxMenu = (() => {
const menu = () => document.getElementById('context-menu');
let targetHost = null;
function show(e, host) {
e.preventDefault();
targetHost = host;
// Show/hide HTTP/HTTPS based on open ports
const openPorts = (host.ports || [])
.filter((p) => p.state === 'Open')
.map((p) => p.port);
const hasHttp = openPorts.some((p) => [80, 8080, 8088].includes(p));
const hasHttps = openPorts.some((p) => [443, 8443].includes(p));
menu().querySelector('[data-action="open-http"]').style.display = hasHttp ? '' : 'none';
menu().querySelector('[data-action="open-https"]').style.display = hasHttps ? '' : 'none';
const el = menu();
el.hidden = false;
// Position near the cursor, but keep it on screen
let x = e.clientX;
let y = e.clientY;
const rect = el.getBoundingClientRect();
if (x + rect.width > window.innerWidth) x = window.innerWidth - rect.width - 4;
if (y + rect.height > window.innerHeight) y = window.innerHeight - rect.height - 4;
el.style.left = x + 'px';
el.style.top = y + 'px';
}
function hide() {
menu().hidden = true;
targetHost = null;
}
async function handleAction(action) {
if (!targetHost) return;
const ip = String(targetHost.ip);
const hostname = targetHost.hostname || '';
switch (action) {
case 'copy-ip':
await navigator.clipboard.writeText(ip);
break;
case 'copy-hostname':
await navigator.clipboard.writeText(hostname || ip);
break;
case 'open-http':
await invoke('launch_url', { url: 'http://' + ip });
break;
case 'open-https':
await invoke('launch_url', { url: 'https://' + ip });
break;
case 'traceroute':
traceroute.run(ip);
break;
case 'send-monitor':
var monHost = table.getAll().find(function (h) { return String(h.ip) === ip; });
monitorTab.setTargets(monHost ? [monHost] : [ip]);
break;
case 'send-nmap':
nmapTab.setTarget([ip]);
break;
}
hide();
}
function init() {
// Hide on any click outside
document.addEventListener('click', hide);
// Handle menu item clicks
menu().addEventListener('click', (e) => {
const item = e.target.closest('.ctx-item');
if (item) handleAction(item.dataset.action);
});
// Right-click on table rows
document.getElementById('results-body').addEventListener('contextmenu', (e) => {
const row = e.target.closest('tr');
if (!row || !row.dataset.ip) return;
const host = table.getAll().find((h) => String(h.ip) === row.dataset.ip);
if (host) show(e, host);
});
}
return { init };
})();
const traceroute = (() => {
const modal = () => document.getElementById('traceroute-modal');
const output = () => document.getElementById('traceroute-output');
const target = () => document.getElementById('traceroute-target');
let running = false;
async function run(ip) {
modal().classList.add('visible');
output().innerHTML = '';
output().classList.add('traceroute-running');
target().textContent = ip;
running = true;
try {
await invoke('run_traceroute', { ip: ip });
} catch (err) {
appendLine('Error: ' + err);
}
}
function appendLine(text) {
const el = output();
const line = document.createElement('div');
line.className = 'traceroute-line';
if (text.includes('reached')) line.classList.add('hop-reached');
line.textContent = text;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
function close() {
modal().classList.remove('visible');
output().classList.remove('traceroute-running');
running = false;
}
async function setupListeners() {
await listen('traceroute:hop', (e) => {
appendLine(e.payload);
});
await listen('traceroute:complete', () => {
output().classList.remove('traceroute-running');
running = false;
});
await listen('traceroute:error', (e) => {
appendLine('Error: ' + e.payload);
output().classList.remove('traceroute-running');
running = false;
});
}
function init() {
document.getElementById('traceroute-close').addEventListener('click', close);
// Close on overlay click
modal().addEventListener('click', (e) => {
if (e.key === 'Escape' && !modal().hidden) close();
});
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modal().hidden) close();
});
setupListeners();
}
return { init, run };
})();
const nmapTab = (() => {
let running = false;
const els = {
target: () => document.getElementById('nmap-target'),
preset: () => document.getElementById('nmap-preset'),
custom: () => document.getElementById('nmap-custom-args'),
btnScan: () => document.getElementById('btn-nmap-scan'),
btnStop: () => document.getElementById('btn-nmap-stop'),
output: () => document.getElementById('nmap-output'),
};
function getArgs() {
const preset = els.preset().value;
switch (preset) {
case 'quick': return ['-sT', '-T4'];
case 'service': return ['-sT', '-sV'];
case 'os': return ['-sT', '-O'];
case 'full': return ['-sT', '-sV', '-sC'];
case 'custom':
return els.custom().value.trim().split(/\s+/).filter(Boolean);
default: return ['-sT'];
}
}
function setRunning(active) {
try {
running = active;
els.btnScan().disabled = active;
els.btnStop().disabled = !active;
els.target().disabled = active;
els.preset().disabled = active;
if (active) {
els.output().classList.add('nmap-running');
} else {
els.output().classList.remove('nmap-running');
}
var nmapTabEl = document.querySelector('[data-tab="nmap"]');
console.log('found tab:', nmapTabEl);
if (active) {
nmapTabEl.classList.add('tab-running');
} else {
nmapTabEl.classList.remove('tab-running');
}
console.log('tab class now:', nmapTabEl.classList.toString());
} catch (err) {
console.error('setRunning error:', err);
}
}
function appendLine(text) {
const el = els.output();
const line = document.createElement('div');
line.className = 'nmap-line';
if (text.match(/^\d+\/tcp\s+open/)) {
line.classList.add('nmap-line-open');
} else if (text.match(/^\d+\/tcp\s+closed/)) {
line.classList.add('nmap-line-closed');
} else if (text.startsWith('Nmap scan report') || text.startsWith('PORT')) {
line.classList.add('nmap-line-header');
} else if (text.startsWith('|') || text.startsWith('Service Info')) {
line.classList.add('nmap-line-info');
}
line.textContent = text;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
async function start() {
const target = els.target().value.trim();
if (!target) return;
els.output().innerHTML = '';
setRunning(true);
try {
const args = getArgs();
if (document.getElementById('chk-nmap-verbose').checked) {
args.unshift('-v');
}
await invoke('run_nmap', {
config: { target: target, args: args },
});
} catch (err) {
appendLine('Error: ' + err);
setRunning(false);
}
}
async function stop() {
try {
await invoke('stop_nmap');
} catch (err) {
console.error('stop_nmap error:', err);
}
}
function setTarget(ips) {
els.target().value = ips.join(' ');
// Switch to nmap tab
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
document.querySelectorAll('.view').forEach((v) => v.classList.remove('active'));
document.querySelector('[data-tab="nmap"]').classList.add('active');
document.getElementById('view-nmap').classList.add('active');
}
async function setupListeners() {
await listen('nmap:line', (e) => {
appendLine(e.payload);
});
await listen('nmap:complete', (e) => {
appendLine('');
appendLine(e.payload);
setRunning(false);
});
await listen('nmap:error', (e) => {
appendLine('Error: ' + e.payload);
setRunning(false);
});
}
function init() {
els.btnScan().addEventListener('click', start);
els.btnStop().addEventListener('click', stop);
els.target().addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !running) start();
});
// Show/hide custom args field
els.preset().addEventListener('change', () => {
els.custom().hidden = els.preset().value !== 'custom';
});
setupListeners();
}
return { init, setTarget };
})();
const monitorTab = (() => {
let running = false;
let entries = {};
var els = {
input: () => document.getElementById('monitor-input'),
interval: () => document.getElementById('monitor-interval'),
btnAdd: () => document.getElementById('btn-monitor-add'),
btnStart: () => document.getElementById('btn-monitor-start'),
btnStop: () => document.getElementById('btn-monitor-stop'),
btnClear: () => document.getElementById('btn-monitor-clear'),
body: () => document.getElementById('monitor-body'),
};
function setRunning(active) {
running = active;
els.btnStart().disabled = active;
els.btnStop().disabled = !active;
els.interval().disabled = active;
var monTabEl = document.querySelector('[data-tab="monitor"]');
if (active) {
monTabEl.classList.add('tab-running');
} else {
monTabEl.classList.remove('tab-running');
}
}
function render() {
var body = els.body();
var ips = Object.keys(entries).sort();
var rows = ips.map(function (ip) {
var e = entries[ip];
var statusClass = 'monitor-status-' + e.status;
var lastRtt = e.last_rtt_ms !== null ? e.last_rtt_ms.toFixed(1) : '-';
var minRtt = e.min_rtt_ms !== null ? e.min_rtt_ms.toFixed(1) : '-';
var avgRtt = e.avg_rtt_ms !== null ? e.avg_rtt_ms.toFixed(1) : '-';
var maxRtt = e.max_rtt_ms !== null ? e.max_rtt_ms.toFixed(1) : '-';
var loss = e.loss_pct.toFixed(1);
var label = e.label || '-';
return '<tr data-ip="' + ip + '">'
+ '<td>' + ip + '</td>'
+ '<td class="editable-label" data-ip="' + ip + '">' + label + '</td>'
+ '<td class="' + statusClass + '">' + e.status + '</td>'
+ '<td>' + lastRtt + '</td>'
+ '<td>' + minRtt + '</td>'
+ '<td>' + avgRtt + '</td>'
+ '<td>' + maxRtt + '</td>'
+ '<td>' + e.packets_sent + '</td>'
+ '<td>' + e.packets_received + '</td>'
+ '<td>' + loss + '</td>'
+ '<td>' + e.consecutive_failures + '</td>'
+ '<td><button class="btn-remove" data-ip="' + ip + '">&times;</button></td>'
+ '</tr>';
});
body.innerHTML = rows.join('');
}
async function addHosts() {
var input = els.input().value.trim();
if (!input) return;
var hosts = input.split(',').map(function (s) {
return { ip: s.trim(), label: null };
}).filter(function (h) { return h.ip.length > 0; });
try {
await invoke('monitor_add_hosts', { hosts: hosts });
for (var i = 0; i < hosts.length; i++) {
try {
var hostname = await invoke('monitor_resolve_host', { ip: hosts[i].ip });
if (hostname) {
await invoke('monitor_update_label', { ip: hosts[i].ip, label: hostname });
}
} catch (e) { }
}
var updated = await invoke('monitor_get_entries');
entries = {};
updated.forEach(function (e) { entries[e.ip] = e; });
render();
els.input().value = '';
} catch (err) {
console.error('Failed to add hosts:', err);
}
}
async function removeHost(ip) {
try {
await invoke('monitor_remove_host', { ip: ip });
delete entries[ip];
render();
} catch (err) {
console.error('Failed to remove host:', err);
}
}
async function start() {
if (Object.keys(entries).length === 0) return;
var interval = parseInt(els.interval().value, 10);
setRunning(true);
try {
await invoke('monitor_start', { intervalMs: interval });
} catch (err) {
console.error('Monitor start error:', err);
setRunning(false);
}
}
async function stop() {
try {
await invoke('monitor_stop');
} catch (err) {
console.error('Monitor stop error:', err);
}
setRunning(false);
}
async function clearAll() {
try {
if (running) await stop();
await invoke('monitor_clear');
entries = {};
render();
} catch (err) {
console.error('Monitor clear error:', err);
}
}
function setTargets(hosts) {
var items = hosts.map(function (h) {
if (typeof h === 'string') return { ip: h, label: null };
return { ip: String(h.ip), label: h.hostname || null };
});
invoke('monitor_add_hosts', { hosts: items }).then(function () {
return invoke('monitor_get_entries');
}).then(function (updated) {
entries = {};
updated.forEach(function (e) { entries[e.ip] = e; });
render();
});
document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); });
document.querySelectorAll('.view').forEach(function (v) { v.classList.remove('active'); });
document.querySelector('[data-tab="monitor"]').classList.add('active');
document.getElementById('view-monitor').classList.add('active');
}
async function setupListeners() {
await listen('monitor:update', function (e) {
var entry = e.payload;
entries[entry.ip] = entry;
render();
});
await listen('monitor:state-change', function (e) {
var data = e.payload;
console.log('State change:', data.ip, data.old, '->', data.new);
});
}
function init() {
els.btnAdd().addEventListener('click', addHosts);
els.btnStart().addEventListener('click', start);
els.btnStop().addEventListener('click', stop);
els.btnClear().addEventListener('click', clearAll);
els.input().addEventListener('keydown', function (e) {
if (e.key === 'Enter') addHosts();
});
// Remove button delegation
els.body().addEventListener('click', function (e) {
var btn = e.target.closest('.btn-remove');
if (btn) removeHost(btn.dataset.ip);
});
els.body().addEventListener('dblclick', function (e) {
var cell = e.target.closest('.editable-label');
if (!cell) return;
var ip = cell.dataset.ip;
var currentLabel = cell.textContent === '-' ? '' : cell.textContent;
var input = document.createElement('input');
input.type = 'text';
input.value = currentLabel;
input.className = 'label-edit-input';
cell.textContent = '';
cell.appendChild(input);
input.focus();
input.select();
function save() {
var newLabel = input.value.trim();
invoke('monitor_update_label', { ip: ip, label: newLabel }).then(function () {
return invoke('monitor_get_entries');
}).then(function (updated) {
entries = {};
updated.forEach(function (e) { entries[e.ip] = e; });
render();
});
}
input.addEventListener('blur', save);
input.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter') save();
if (ev.key === 'Escape') render();
});
});
setupListeners();
}
return { init, setTargets };
})();
var updater = (() => {
function showBanner(info) {
var banner = document.getElementById('update-banner');
var msg = document.getElementById('update-message');
var link = document.getElementById('update-link');
var text = 'Argus v' + info.version + ' is available.';
msg.textContent = text;
link.href = info.url;
link.addEventListener('click', function (e) {
e.preventDefault();
invoke('launch_url', { url: info.url });
});
banner.classList.add('visible');
}
async function check() {
try {
var update = await invoke('check_for_update');
if (update) showBanner(update);
} catch (err) {
console.log('Update check skipped:', err);
}
}
function init() {
document.getElementById('update-dismiss').addEventListener('click', function () {
document.getElementById('update-banner').classList.remove('visible');
});
// Check on startup after a short delay
setTimeout(check, 3000);
}
return { init };
})();
var changelog = (() => {
var modal = function () { return document.getElementById('changelog-modal'); };
var loaded = false;
async function open() {
modal().classList.add('visible');
if (!loaded) {
try {
var html = await invoke('get_changelog');
document.getElementById('changelog-content').innerHTML = html;
loaded = true;
} catch (err) {
document.getElementById('changelog-content').textContent = 'Failed to load changelog.';
}
}
}
function close() {
modal().classList.remove('visible');
}
function init() {
document.getElementById('btn-changelog').addEventListener('click', open);
document.getElementById('changelog-close').addEventListener('click', close);
modal().addEventListener('click', function (e) {
if (e.target === modal()) close();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && modal().classList.contains('visible')) close();
});
}
return { init };
})();
window.addEventListener('DOMContentLoaded', () => {
// Tab switching
document.querySelectorAll('.tab').forEach((tab) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
document.querySelectorAll('.view').forEach((v) => v.classList.remove('active'));
tab.classList.add('active');
const view = document.getElementById('view-' + tab.dataset.tab);
if (view) view.classList.add('active');
});
});
// Sort on header click
document.querySelectorAll('#results-table th.sortable').forEach((th) => {
th.addEventListener('click', () => table.setSort(th.dataset.sort));
});
// Filter input
document.getElementById('filter-input').addEventListener('input', (e) => {
table.setFilter(e.target.value);
});
// Boot scanner
table.init();
scanner.init();
ctxMenu.init();
traceroute.init();
columns.init();
sender.init();
nmapTab.init();
monitorTab.init();
updater.init();
changelog.init();
theme.init();
document.getElementById('btn-export-all').addEventListener('click', exporter.csvAll);
document.getElementById('btn-export-up').addEventListener('click', exporter.csvUp);
console.log('[Argus] Ready');
// Set version from Tauri config
if (window.__TAURI__ && window.__TAURI__.app) {
window.__TAURI__.app.getVersion().then(function (version) {
document.getElementById('status-bar-version').textContent = 'Argus v' + version;
});
}
// Check privilege status
invoke('check_privileges').then(function (elevated) {
var el = document.getElementById('privilege-status');
if (elevated) {
el.textContent = 'elevated';
el.classList.add('privilege-elevated');
} else {
el.textContent = 'unprivileged';
}
});
});