17 Commits
Author SHA1 Message Date
mysticmomba 518f8cfe36 Merge pull request 'Development' (#8) from development into master
Reviewed-on: #8
2026-10-08 11:25:29 -04:00
mysticmomba 29e0ae9d48 Updated Changelog 2026-10-08 11:23:41 -04:00
mysticmomba 82c07ad81a Updated Version 2026-10-08 11:23:19 -04:00
mysticmomba 1fe24d22ed feat: Added New Theme 2026-10-08 11:22:28 -04:00
mysticmomba aaeb7636a1 feat: Added Always on Top Toggle. Closes #5 2026-10-08 11:00:40 -04:00
mysticmomba 1df9c2f00c fix: Fixed Screen Refresh Bug on Monitor Tab. Now When You Go To Change a Label While the Monitor Is Running. The Page Stops Refreshing Until You’re Done Changing the Name. This Is To Prevent Your Text Box From Being Deselected When the Table Refreshes. 2026-10-08 10:52:50 -04:00
mysticmomba d6ba6270c5 Updated Changelog 2026-10-08 10:43:46 -04:00
mysticmomba c5931c723f feat: Added Auto Labeling to Montior Tab. When sending hosts from the scan tab, it will bring their hostnames over to the label section. When adding a one off host to the montior tab, it will auto query it's hostname. You can also edit labels by double clicking. Closes #3 2026-10-06 16:49:04 -04:00
mysticmomba 8cc8189820 fix: Removed unused function. 2026-10-06 16:36:30 -04:00
mysticmomba dbf98d598d fix: Status Colors on Monitor tab now reflect status of host. Closes #7 2026-10-06 16:27:32 -04:00
mysticmomba dbe8f2fb68 Merge pull request 'Fixed main.js' (#2) from development into master
Reviewed-on: #2
2026-09-24 10:28:40 -04:00
mysticmomba 595d73a41d Fixed main.js 2026-09-24 10:21:46 -04:00
mysticmomba af5f65f166 Merge pull request '[0.2.0] - 2026-09-24' (#1) from development into master
Reviewed-on: #1
2026-09-24 10:18:36 -04:00
mysticmomba 4c72cdab29 Updated Changelog 2026-09-24 10:07:58 -04:00
mysticmomba 5b3aa3bf37 fix: Fixed updater to only check for newer version using proper semver checking. Before it only checked if the current in use version was different than what was available. 2026-09-24 10:07:13 -04:00
mysticmomba f3dd13d831 Updated changelog 2026-09-24 10:04:20 -04:00
mysticmomba 4596b93bb6 Updated Change log and Version 2026-09-24 10:02:09 -04:00
12 changed files with 380 additions and 55 deletions
+19 -1
View File
@@ -1,7 +1,25 @@
## [unreleased]
## [0.3.0] - 2026-10-08
### 🚀 Features
- Added Auto Labeling to Montior Tab. When sending hosts from the scan tab, it will bring their hostnames over to the label section. When adding a one off host to the montior tab, it will auto query it's hostname. You can also edit labels by double clicking. Closes #3
- Added Always on Top Toggle. Closes #5
- Added New Theme
### 🐛 Bug Fixes
- Status Colors on Monitor tab now reflect status of host. Closes #7
- Removed unused function.
- Fixed Screen Refresh Bug on Monitor Tab. Now When You Go To Change a Label While the Monitor Is Running. The Page Stops Refreshing Until You’re Done Changing the Name. This Is To Prevent Your Text Box From Being Deselected When the Table Refreshes.
## [0.2.0] - 2026-09-24
### 🚀 Features
- Added changelog
- Added Themes
- Added Virtual Scrolling. This allows us to scan a whole /16 subnet without overloading the UI or using a lot of RAM.
### 🐛 Bug Fixes
- Fixed updater to only check for newer version using proper semver checking. Before it only checked if the current in use version was different than what was available.
## [0.1.0] - 2026-09-23
+2 -1
View File
@@ -49,7 +49,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "argus"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"csv",
"dns-lookup",
@@ -60,6 +60,7 @@ dependencies = [
"ping-async",
"pulldown-cmark",
"reqwest 0.12.28",
"semver",
"serde",
"serde_json",
"tauri",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "argus"
version = "0.1.0"
version = "0.3.0"
description = "An All in One Network Utility"
authors = ["James Brotosky <james.brotosky@racooncity.org"]
edition = "2021"
@@ -34,6 +34,7 @@ open = "5.4.4"
reqwest = { version = "0.12", features = ["json"] }
libc = "0.2.189"
pulldown-cmark = { version = "0.13.4", features = ["html"] }
semver = "1.0.28"
[target.'cfg(windows)'.dependencies]
is_elevated = "0.1"
+2 -1
View File
@@ -6,6 +6,7 @@
"permissions": [
"core:default",
"opener:default",
"dialog:default"
"dialog:default",
"core:window:allow-set-always-on-top"
]
}
+16
View File
@@ -180,3 +180,19 @@ pub async fn monitor_stop() -> Result<(), String> {
}
Ok(())
}
#[tauri::command]
pub async fn monitor_update_label(ip: String, label: String) -> Result<(), String> {
let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?;
let state = get_monitor();
let mut guard = state.lock().await;
if let Some(entry) = guard.entries.get_mut(&parsed) {
entry.label = if label.is_empty() { None } else { Some(label) };
}
Ok(())
}
#[tauri::command]
pub async fn monitor_resolve_host(ip: String) -> Result<Option<String>, String> {
let parsed: IpAddr = ip.parse().map_err(|e| format!("Invalid IP: {}", e))?;
Ok(crate::net::dns::reverse_lookup(parsed).await)
}
+9 -6
View File
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
const UPDATE_URL: &str = "https://git.racooncity.org/api/v1/repos/mysticmomba/argus/releases/latest";
const UPDATE_URL: &str =
"https://git.racooncity.org/api/v1/repos/mysticmomba/argus/releases/latest";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInfo {
@@ -29,10 +30,7 @@ pub async fn check_for_update(app: AppHandle) -> Result<Option<UpdateInfo>, Stri
.as_str()
.ok_or_else(|| "No tag_name in response".to_string())?;
let html_url = body["html_url"]
.as_str()
.unwrap_or("")
.to_string();
let html_url = body["html_url"].as_str().unwrap_or("").to_string();
let notes = body["body"]
.as_str()
@@ -42,7 +40,12 @@ pub async fn check_for_update(app: AppHandle) -> Result<Option<UpdateInfo>, Stri
let remote_version = tag.strip_prefix('v').unwrap_or(tag);
let current = app.package_info().version.to_string();
if remote_version != current {
let remote_semver = semver::Version::parse(remote_version)
.map_err(|e| format!("Invalid remote version '{}': {}", remote_version, e))?;
let current_semver = semver::Version::parse(&current)
.map_err(|e| format!("Invalid current version '{}': {}", current, e))?;
if remote_semver > current_semver {
Ok(Some(UpdateInfo {
version: remote_version.to_string(),
url: html_url,
+2
View File
@@ -32,6 +32,8 @@ pub fn run() {
commands::updater::check_for_update,
commands::actions::check_privileges,
commands::changelog::get_changelog,
commands::monitor::monitor_update_label,
commands::monitor::monitor_resolve_host
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
-30
View File
@@ -1,5 +1,4 @@
use std::net::IpAddr;
use tokio::task::JoinSet;
/// Perform reverse DNS lookup on a single IP.
/// Returns the hostname if found, None otherwise.
@@ -19,32 +18,3 @@ pub async fn reverse_lookup(ip: IpAddr) -> Option<String> {
_ => None,
}
}
/// Perform reverse DNS lookups on a batch of IPs concurrently.
/// Returns a Vec of (IpAddr, Option<hostname>) pairs.
pub async fn reverse_lookup_batch(
ips: &[IpAddr],
max_concurrent: usize,
) -> Vec<(IpAddr, Option<String>)> {
let mut results = Vec::with_capacity(ips.len());
for chunk in ips.chunks(max_concurrent) {
let mut join_set = JoinSet::new();
for &ip in chunk {
join_set.spawn(async move {
let hostname = reverse_lookup(ip).await;
(ip, hostname)
});
}
while let Some(result) = join_set.join_next().await {
match result {
Ok(pair) => results.push(pair),
Err(e) => tracing::warn!("DNS lookup task panicked: {}", e),
}
}
}
results
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "argus",
"version": "0.1.0",
"version": "0.3.0",
"identifier": "io.argus.app",
"build": {
"frontendDist": "../src"
+6 -1
View File
@@ -200,12 +200,17 @@
<div id="status-bar">
<span class="status-bar-item" id="status-bar-version">Argus</span>
<button id="btn-changelog" class="status-bar-btn">Changelog</button>
<label class="toggle-label status-bar-toggle">
<input type="checkbox" id="chk-always-on-top" />
<span>Always on Top</span>
</label>
<select id="theme-picker" class="theme-picker">
<option value="cyber">Cyber</option>
<option value="midnight">Midnight</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option>
<option value="solarized">Solarized Dark</option>
<option value="mocha">Catppuccin Mocha</option>
</select>
<span class="status-bar-item" id="privilege-status">unprivileged</span>
</div>
@@ -243,7 +248,7 @@
<button class="ctx-item" data-action="send-monitor">Send to Monitor</button>
<button class="ctx-item" data-action="send-nmap">Send to Nmap</button>
</div>
<div id="crt-overlay"></div>
<script src="main.js"></script>
</body>
+120 -9
View File
@@ -22,15 +22,63 @@ var theme = (() => {
} catch (e) { }
}
var HACKER_KEY = 'argus-hacker-unlocked';
function isHackerUnlocked() {
try {
return localStorage.getItem(HACKER_KEY) === 'true';
} catch (e) {
return false;
}
}
function unlockHacker() {
try {
localStorage.setItem(HACKER_KEY, 'true');
} catch (e) { }
addHackerOption();
}
function addHackerOption() {
var picker = document.getElementById('theme-picker');
if (!picker.querySelector('option[value="hacker"]')) {
var opt = document.createElement('option');
opt.value = 'hacker';
opt.textContent = '\u2588 H4CK3R';
picker.appendChild(opt);
}
}
function init() {
var saved = get();
set(saved);
// Add hacker option if previously unlocked
if (isHackerUnlocked()) {
addHackerOption();
}
var picker = document.getElementById('theme-picker');
picker.value = saved;
picker.addEventListener('change', function () {
set(picker.value);
});
// Easter egg: listen on the scan input
var scanInput = document.getElementById('ip-range-input');
scanInput.addEventListener('input', function () {
var val = scanInput.value.toLowerCase().trim();
if (val === 'all your base are belong to us') {
if (!isHackerUnlocked()) {
unlockHacker();
set('hacker');
picker.value = 'hacker';
scanInput.value = '';
console.log('%c> ACCESS GRANTED', 'color: #00ff41; font-size: 20px; font-family: monospace;');
console.log('%c> HACKER THEME UNLOCKED', 'color: #00ff41; font-size: 14px; font-family: monospace;');
}
}
});
}
// Apply immediately (before DOMContentLoaded) to prevent flash
@@ -609,7 +657,7 @@ const sender = (() => {
if (action.startsWith('nmap')) {
nmapTab.setTarget(hosts.map((h) => String(h.ip)));
} else if (action.startsWith('monitor')) {
monitorTab.setTargets(hosts.map(function (h) { return String(h.ip); }));
monitorTab.setTargets(hosts);
}
document.querySelectorAll('.send-menu').forEach((m) => m.hidden = true);
@@ -675,7 +723,8 @@ const ctxMenu = (() => {
traceroute.run(ip);
break;
case 'send-monitor':
monitorTab.setTargets([ip]);
var monHost = table.getAll().find(function (h) { return String(h.ip) === ip; });
monitorTab.setTargets(monHost ? [monHost] : [ip]);
break;
case 'send-nmap':
nmapTab.setTarget([ip]);
@@ -926,6 +975,7 @@ const nmapTab = (() => {
const monitorTab = (() => {
let running = false;
let entries = {};
var editingLabel = false;
var els = {
input: () => document.getElementById('monitor-input'),
@@ -952,6 +1002,7 @@ const monitorTab = (() => {
}
function render() {
if (editingLabel) return;
var body = els.body();
var ips = Object.keys(entries).sort();
@@ -967,7 +1018,7 @@ const monitorTab = (() => {
return '<tr data-ip="' + ip + '">'
+ '<td>' + ip + '</td>'
+ '<td>' + label + '</td>'
+ '<td class="editable-label" data-ip="' + ip + '">' + label + '</td>'
+ '<td class="' + statusClass + '">' + e.status + '</td>'
+ '<td>' + lastRtt + '</td>'
+ '<td>' + minRtt + '</td>'
@@ -994,6 +1045,16 @@ const monitorTab = (() => {
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; });
@@ -1048,9 +1109,20 @@ const monitorTab = (() => {
}
}
function setTargets(ips) {
els.input().value = ips.join(', ');
// Switch to monitor tab
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');
@@ -1085,6 +1157,41 @@ const monitorTab = (() => {
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;
editingLabel = true;
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();
editingLabel = false;
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')
editingLabel = false;
render();
});
});
setupListeners();
}
@@ -1097,7 +1204,6 @@ var updater = (() => {
var msg = document.getElementById('update-message');
var link = document.getElementById('update-link');
var text = 'Argus v' + info.version + ' is available.';
if (info.notes) text += ' ' + info.notes;
msg.textContent = text;
link.href = info.url;
link.addEventListener('click', function (e) {
@@ -1197,8 +1303,13 @@ window.addEventListener('DOMContentLoaded', () => {
updater.init();
changelog.init();
theme.init();
columns.init();
sender.init();
// Always on top toggle
document.getElementById('chk-always-on-top').addEventListener('change', function (e) {
if (window.__TAURI__ && window.__TAURI__.window) {
var current = window.__TAURI__.window.getCurrentWindow();
current.setAlwaysOnTop(e.target.checked);
}
});
document.getElementById('btn-export-all').addEventListener('click', exporter.csvAll);
document.getElementById('btn-export-up').addEventListener('click', exporter.csvUp);
console.log('[Argus] Ready');
+200 -3
View File
@@ -1146,17 +1146,17 @@ body {
text-shadow: 0 0 6px var(--status-up-glow);
}
.monitor-status-Down {
#monitor-table .monitor-status-Down {
color: var(--status-down);
text-shadow: 0 0 6px var(--status-down-glow);
}
.monitor-status-Degraded {
#monitor-table .monitor-status-Degraded {
color: var(--status-filtered);
text-shadow: 0 0 6px var(--status-filtered-glow);
}
.monitor-status-Unknown {
#monitor-table .monitor-status-Unknown {
color: var(--status-unknown);
}
@@ -1478,6 +1478,174 @@ body {
--danger-hover: #c62825;
}
/* ===== Theme: Catppuccin Mocha ===== */
:root[data-theme="mocha"] {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--bg-tertiary: #313244;
--bg-hover: #45475a;
--bg-active: #585b70;
--border: #313244;
--border-bright: #45475a;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--text-muted: #6c7086;
--cyan: #89b4fa;
--cyan-hover: #7ba4eb;
--cyan-dim: rgba(137, 180, 250, 0.08);
--cyan-glow: rgba(137, 180, 250, 0.2);
--cyan-subtle: rgba(137, 180, 250, 0.03);
--status-up: #a6e3a1;
--status-up-glow: rgba(166, 227, 161, 0.15);
--status-down: #f38ba8;
--status-down-glow: rgba(243, 139, 168, 0.15);
--status-filtered: #f9e2af;
--status-filtered-glow: rgba(249, 226, 175, 0.15);
--status-unknown: #6c7086;
--danger: #f38ba8;
--danger-hover: #e57a97;
}
:root[data-theme="mocha"] .toolbar-circuit {
filter: hue-rotate(40deg) saturate(0.6) brightness(0.9);
}
:root[data-theme="latte"] .port-tag {
background: rgba(30, 102, 245, 0.1);
border-color: rgba(30, 102, 245, 0.2);
}
:root[data-theme="latte"] #privilege-status::before {
background: var(--status-up);
box-shadow: 0 0 4px rgba(64, 160, 43, 0.3);
}
:root[data-theme="latte"] ::-webkit-scrollbar-track {
background: var(--bg-primary);
}
:root[data-theme="latte"] ::-webkit-scrollbar-thumb {
background: var(--border-bright);
}
/* ===== Theme: Hacker (Easter Egg) ===== */
:root[data-theme="hacker"] {
--bg-primary: #000000;
--bg-secondary: #0a0a0a;
--bg-tertiary: #0d0d0d;
--bg-hover: #1a1a1a;
--bg-active: #222222;
--border: #0a3a0a;
--border-bright: #0f4f0f;
--text-primary: #00ff41;
--text-secondary: #00cc33;
--text-muted: #006b1d;
--cyan: #00ff41;
--cyan-hover: #00dd38;
--cyan-dim: rgba(0, 255, 65, 0.06);
--cyan-glow: rgba(0, 255, 65, 0.3);
--cyan-subtle: rgba(0, 255, 65, 0.02);
--status-up: #00ff41;
--status-up-glow: rgba(0, 255, 65, 0.2);
--status-down: #ff0040;
--status-down-glow: rgba(255, 0, 64, 0.2);
--status-filtered: #ffff00;
--status-filtered-glow: rgba(255, 255, 0, 0.15);
--status-unknown: #006b1d;
--danger: #ff0040;
--danger-hover: #dd0038;
--font-mono: "Courier New", "Lucida Console", monospace;
--font-sans: "Courier New", "Lucida Console", monospace;
}
:root[data-theme="hacker"] .toolbar-circuit {
filter: hue-rotate(-60deg) saturate(3) brightness(0.8);
}
:root[data-theme="hacker"] body {
font-family: "Courier New", "Lucida Console", monospace;
}
:root[data-theme="hacker"] .app-title {
letter-spacing: 5px;
}
:root[data-theme="hacker"] .btn-primary {
text-shadow: 0 0 8px rgba(0, 255, 65, 0.5);
}
:root[data-theme="hacker"] #results-table td,
:root[data-theme="hacker"] #monitor-table td {
text-shadow: 0 0 3px rgba(0, 255, 65, 0.15);
}
#crt-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9999;
}
:root[data-theme="hacker"] #crt-overlay {
display: block;
background:
repeating-linear-gradient(0deg,
rgba(0, 0, 0, 0.4) 0px,
rgba(0, 0, 0, 0.4) 1px,
transparent 1px,
transparent 4px),
linear-gradient(180deg,
transparent 0%,
rgba(0, 255, 65, 0.03) 50%,
transparent 100%);
animation: crt-flicker 3s infinite;
}
@keyframes crt-flicker {
0% {
opacity: 1;
}
5% {
opacity: 0.85;
}
10% {
opacity: 1;
}
40% {
opacity: 0.95;
}
45% {
opacity: 1;
}
80% {
opacity: 0.9;
}
85% {
opacity: 1;
}
}
/* ===== Circuit art theme tinting ===== */
:root[data-theme="dracula"] .toolbar-circuit {
@@ -1495,3 +1663,32 @@ body {
:root[data-theme="midnight"] .toolbar-circuit {
filter: brightness(0.8);
}
.editable-label {
cursor: pointer;
}
.editable-label:hover {
background: var(--cyan-dim);
}
.label-edit-input {
width: 100%;
padding: 2px 6px;
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-primary);
background: var(--bg-primary);
border: 1px solid var(--cyan);
border-radius: var(--radius);
outline: none;
}
.status-bar-toggle {
font-size: 11px;
color: var(--text-muted);
}
.status-bar-toggle input:checked+span {
color: var(--cyan);
}