13 Commits
Author SHA1 Message Date
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 595d73a41d Fixed main.js 2026-09-24 10:21:46 -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
mysticmomba adc061ae26 feat: Added Virtual Scrolling. This allows us to scan a whole /16 subnet without overloading the UI or using a lot of RAM. 2026-09-24 09:43:55 -04:00
mysticmomba d84902c111 feat: Added Themes 2026-09-24 09:12:18 -04:00
mysticmomba 5be8e31483 feat: Added changelog 2026-09-24 09:07:31 -04:00
mysticmomba eb1d0ba0fd Updated README 2026-09-23 10:23:27 -04:00
mysticmomba 293c3b137d Fixed popup window 2026-09-23 10:22:06 -04:00
19 changed files with 749 additions and 84 deletions
+3
View File
@@ -0,0 +1,3 @@
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
+18
View File
@@ -0,0 +1,18 @@
## [unreleased]
### 🐛 Bug Fixes
- Status Colors on Monitor tab now reflect status of host. Closes #7
- Removed unused function.
## [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
+3
View File
@@ -61,6 +61,9 @@ cargo tauri dev
# Production build
cargo tauri build
# Cross Compiling Windows on Linux
cargo tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc
```
## Privileges
+94
View File
@@ -0,0 +1,94 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# A Tera template to be rendered for each release in the changelog.
# See https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}
"""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = true
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = true
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit message using https://github.com/crate-ci/typos.
# If the spelling is incorrect, it will be fixed automatically.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# Fail on a commit that is not matched by any commit parser.
fail_on_unmatched_commit = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order commits topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "oldest"
# Process submodules commits
recurse_submodules = false
+43 -1
View File
@@ -49,7 +49,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "argus"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"csv",
"dns-lookup",
@@ -58,7 +58,9 @@ dependencies = [
"libc",
"open",
"ping-async",
"pulldown-cmark",
"reqwest 0.12.28",
"semver",
"serde",
"serde_json",
"tauri",
@@ -1367,6 +1369,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -3006,6 +3017,25 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags 2.13.2",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quick-xml"
version = "0.42.0"
@@ -4738,6 +4768,12 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.26"
@@ -4750,6 +4786,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "untrusted"
version = "0.9.0"
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "argus"
version = "0.1.0"
version = "0.2.0"
description = "An All in One Network Utility"
authors = ["James Brotosky <james.brotosky@racooncity.org"]
edition = "2021"
@@ -33,6 +33,8 @@ csv = "1"
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"
+16
View File
@@ -0,0 +1,16 @@
use pulldown_cmark::{Options, Parser, html};
const CHANGELOG_MD: &str = include_str!("../../../CHANGELOG.md");
#[tauri::command]
pub async fn get_changelog() -> Result<String, String> {
let options = Options::ENABLE_HEADING_ATTRIBUTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TABLES;
let parser = Parser::new_ext(CHANGELOG_MD, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
Ok(html_output)
}
+1
View File
@@ -1,4 +1,5 @@
pub mod actions;
pub mod changelog;
pub mod export;
pub mod monitor;
pub mod nmap;
+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)
}
+6 -1
View File
@@ -48,7 +48,12 @@ pub async fn run_nmap(app: AppHandle, config: NmapConfig) -> Result<(), String>
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
let mut child = cmd
.spawn()
.map_err(|e| format!("Failed to start nmap: {}", e))?;
+24 -10
View File
@@ -15,18 +15,30 @@ pub async fn run_traceroute(app: AppHandle, ip: String) -> Result<(), String> {
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start tracepath: {}. Is iputils-tracepath installed?", e))?;
.map_err(|e| {
format!(
"Failed to start tracepath: {}. Is iputils-tracepath installed?",
e
)
})?;
#[cfg(target_os = "windows")]
let mut child = Command::new("tracert")
.arg("-d")
.arg(&ip)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start tracert: {}", e))?;
let mut child = {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
Command::new("tracert")
.arg("-d")
.arg(&ip)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| format!("Failed to start tracert: {}", e))?
};
let stdout = child.stdout.take()
let stdout = child
.stdout
.take()
.ok_or_else(|| "Failed to capture stdout".to_string())?;
let mut reader = BufReader::new(stdout).lines();
@@ -35,7 +47,9 @@ pub async fn run_traceroute(app: AppHandle, ip: String) -> Result<(), String> {
let _ = app.emit("traceroute:hop", &line);
}
let status = child.wait().await
let status = child
.wait()
.await
.map_err(|e| format!("tracepath failed: {}", e))?;
if status.success() {
+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,
+3
View File
@@ -31,6 +31,9 @@ pub fn run() {
commands::monitor::monitor_stop,
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
@@ -76,7 +76,7 @@ pub async fn ping_sweep(
while let Some(result) = join_set.join_next().await {
done += 1;
match result {
Ok(mut host_result) => {
Ok(host_result) => {
// Emit immediately so the host shows up in the table
let _ = app.emit("scan:host-found", &host_result);
+8 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "argus",
"version": "0.1.0",
"version": "0.2.0",
"identifier": "io.argus.app",
"build": {
"frontendDist": "../src"
@@ -10,7 +10,7 @@
"withGlobalTauri": true,
"windows": [
{
"title": "argus",
"title": "Argus",
"width": 800,
"height": 600
}
@@ -22,6 +22,12 @@
"bundle": {
"active": true,
"targets": "all",
"copyright": "Copyright \u00A9 2026 James Brotosky. Licensed under AGPLv3. All rights reserved.",
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",
+18
View File
@@ -199,6 +199,14 @@
<!-- Status Bar -->
<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>
<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>
</select>
<span class="status-bar-item" id="privilege-status">unprivileged</span>
</div>
@@ -212,6 +220,16 @@
<div id="traceroute-output" class="traceroute-output"></div>
</div>
</div>
<!-- Changelog Modal -->
<div id="changelog-modal" class="modal-overlay">
<div class="modal-panel" style="width: 700px;">
<div class="modal-header">
<span class="modal-title">Changelog</span>
<button id="changelog-close" class="modal-close">&times;</button>
</div>
<div id="changelog-content" class="changelog-content"></div>
</div>
</div>
<!-- Context Menu -->
<div id="context-menu" class="context-menu" hidden>
<button class="ctx-item" data-action="copy-ip">Copy IP</button>
+222 -26
View File
@@ -1,12 +1,56 @@
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('.');
@@ -155,20 +199,61 @@ const table = (() => {
}
function render() {
const tbody = document.getElementById('results-body');
const count = document.getElementById('host-count');
var tbody = document.getElementById('results-body');
var count = document.getElementById('host-count');
if (!tbody) return;
expandedIp = null;
const rows = filtered.map((host) => {
const ip = host.ip;
const status = host.status || 'Unknown';
const cls = 'status-' + status.toLowerCase();
const rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-';
const hostname = host.hostname || '-';
if (!scrollContainer) {
scrollContainer = document.getElementById('results-table-container');
scrollContainer.addEventListener('scroll', onScroll);
}
const isChecked = checkedIps.has(ip) ? 'checked' : '';
return '<tr data-ip="' + ip + '">'
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>'
@@ -176,24 +261,43 @@ const table = (() => {
+ '<td>' + hostname + '</td>'
+ '<td class="ports-cell">' + formatPorts(host) + '</td>'
+ '</tr>';
});
tbody.innerHTML = rows.join('');
if (count) {
const up = data.filter((h) => h.status === 'Up').length;
count.textContent = up + ' up / ' + data.length + ' total';
}
if (columns && columns.apply) columns.apply();
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) {
const idx = data.findIndex((h) => h.ip === host.ip);
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();
}
@@ -201,6 +305,7 @@ const table = (() => {
data = [];
filtered = [];
checkedIps.clear();
ipIndex = {};
document.getElementById('check-all').checked = false;
refresh();
}
@@ -504,7 +609,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);
@@ -570,7 +675,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]);
@@ -862,7 +968,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>'
@@ -889,6 +995,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; });
@@ -943,9 +1059,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');
@@ -980,6 +1107,38 @@ 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;
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();
}
@@ -992,7 +1151,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) {
@@ -1022,6 +1180,42 @@ var updater = (() => {
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) => {
@@ -1054,6 +1248,8 @@ window.addEventListener('DOMContentLoaded', () => {
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');
+258 -3
View File
@@ -409,6 +409,7 @@ body {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 32px;
}
#results-table tbody tr {
@@ -428,6 +429,15 @@ body {
box-shadow: inset 3px 0 0 var(--cyan);
}
.virtual-spacer {
border: none !important;
}
.virtual-spacer td {
padding: 0 !important;
border: none !important;
}
/* ===== Status Colors ===== */
.status-up {
@@ -1136,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);
}
@@ -1260,3 +1270,248 @@ body {
color: var(--text-muted);
margin-top: 4px;
}
/* ===== Changelog ===== */
.changelog-content {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.7;
color: var(--text-primary);
max-height: 70vh;
}
.changelog-content h1 {
font-size: 20px;
color: var(--cyan);
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.changelog-content h2 {
font-size: 16px;
color: var(--cyan);
margin: 24px 0 12px 0;
}
.changelog-content h3 {
font-size: 14px;
color: var(--text-primary);
margin: 16px 0 8px 0;
}
.changelog-content ul {
padding-left: 20px;
margin: 8px 0;
}
.changelog-content li {
margin: 4px 0;
color: var(--text-secondary);
}
.changelog-content code {
font-family: var(--font-mono);
font-size: 12px;
padding: 2px 5px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 2px;
color: var(--cyan);
}
.changelog-content a {
color: var(--cyan);
text-decoration: none;
}
.changelog-content a:hover {
text-decoration: underline;
}
.theme-picker {
padding: 1px 6px;
font-size: 11px;
font-family: var(--font-mono);
color: var(--text-muted);
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
-webkit-appearance: none;
appearance: none;
outline: none;
}
.theme-picker:hover {
color: var(--text-secondary);
border-color: var(--border-bright);
}
.theme-picker option {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* ===== Theme: Midnight ===== */
:root[data-theme="midnight"] {
--bg-primary: #000000;
--bg-secondary: #0a0a0a;
--bg-tertiary: #141414;
--bg-hover: #1e1e1e;
--bg-active: #282828;
--border: #1a1a1a;
--border-bright: #2a2a2a;
--text-primary: #e0e0e0;
--text-secondary: #808080;
--text-muted: #4a4a4a;
--cyan: #00bfff;
--cyan-hover: #009ed9;
--cyan-dim: rgba(0, 191, 255, 0.08);
--cyan-glow: rgba(0, 191, 255, 0.25);
--cyan-subtle: rgba(0, 191, 255, 0.03);
}
/* ===== Theme: Dracula ===== */
:root[data-theme="dracula"] {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--bg-tertiary: #2d2f3d;
--bg-hover: #44475a;
--bg-active: #4e5166;
--border: #44475a;
--border-bright: #555869;
--text-primary: #f8f8f2;
--text-secondary: #6272a4;
--text-muted: #4d5b86;
--cyan: #bd93f9;
--cyan-hover: #a87cee;
--cyan-dim: rgba(189, 147, 249, 0.08);
--cyan-glow: rgba(189, 147, 249, 0.25);
--cyan-subtle: rgba(189, 147, 249, 0.03);
--status-up: #50fa7b;
--status-up-glow: rgba(80, 250, 123, 0.15);
--status-down: #ff5555;
--status-down-glow: rgba(255, 85, 85, 0.15);
--status-filtered: #f1fa8c;
--status-filtered-glow: rgba(241, 250, 140, 0.15);
--status-unknown: #6272a4;
--danger: #ff5555;
--danger-hover: #e64545;
}
/* ===== Theme: Nord ===== */
:root[data-theme="nord"] {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--bg-tertiary: #434c5e;
--bg-hover: #4c566a;
--bg-active: #556070;
--border: #3b4252;
--border-bright: #4c566a;
--text-primary: #eceff4;
--text-secondary: #d8dee9;
--text-muted: #616e88;
--cyan: #88c0d0;
--cyan-hover: #7bb4c4;
--cyan-dim: rgba(136, 192, 208, 0.08);
--cyan-glow: rgba(136, 192, 208, 0.25);
--cyan-subtle: rgba(136, 192, 208, 0.03);
--status-up: #a3be8c;
--status-up-glow: rgba(163, 190, 140, 0.15);
--status-down: #bf616a;
--status-down-glow: rgba(191, 97, 106, 0.15);
--status-filtered: #ebcb8b;
--status-filtered-glow: rgba(235, 203, 139, 0.15);
--status-unknown: #616e88;
--danger: #bf616a;
--danger-hover: #a9545c;
}
/* ===== Theme: Solarized Dark ===== */
:root[data-theme="solarized"] {
--bg-primary: #002b36;
--bg-secondary: #073642;
--bg-tertiary: #0a3f4c;
--bg-hover: #124a56;
--bg-active: #1a5560;
--border: #0d4450;
--border-bright: #1a5c6a;
--text-primary: #93a1a1;
--text-secondary: #839496;
--text-muted: #586e75;
--cyan: #2aa198;
--cyan-hover: #25908a;
--cyan-dim: rgba(42, 161, 152, 0.1);
--cyan-glow: rgba(42, 161, 152, 0.25);
--cyan-subtle: rgba(42, 161, 152, 0.03);
--status-up: #859900;
--status-up-glow: rgba(133, 153, 0, 0.15);
--status-down: #dc322f;
--status-down-glow: rgba(220, 50, 47, 0.15);
--status-filtered: #b58900;
--status-filtered-glow: rgba(181, 137, 0, 0.15);
--status-unknown: #586e75;
--danger: #dc322f;
--danger-hover: #c62825;
}
/* ===== Circuit art theme tinting ===== */
:root[data-theme="dracula"] .toolbar-circuit {
filter: hue-rotate(70deg);
}
:root[data-theme="nord"] .toolbar-circuit {
filter: hue-rotate(20deg) saturate(0.5);
}
:root[data-theme="solarized"] .toolbar-circuit {
filter: hue-rotate(160deg) saturate(0.6);
}
: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;
}