Antivirus/gui/dist/index.html
dev b8744c6bdf 0.1.4: settings apply when you change them, and the app reopens itself
Three reports, one shape: the app told the user something had happened
when it had not.

**Appearance was stored in the wrong place.** theme and tray_icon_style
lived in the daemon's settings, which need root to write. So toggling
to a monochrome tray produced a rejected write, no visible change, and
a switch still sitting where it had been put — the screen disagreeing
with reality. Appearance is a per-user preference: it belongs to the
person, not the machine, and putting it in the daemon would also have
made one user's choice everyone's on a shared box. It is client-side
now, applies the instant the control moves, and never prompts for a
password.

**There was a Save button.** A settings screen with one lets you walk
away with your changes discarded, and here it also hid the failure
above. It is gone. Every control writes on change, batched behind a
short delay so toggling three switches is one request and one
authentication prompt rather than three, and typed fields wait for a
pause rather than firing per keystroke. A write the daemon refuses now
reloads the real settings, so a control never keeps a value that was
not accepted.

**The app did not notice being replaced.** After an update the running
process is still the old binary showing the old front-end, which is
indistinguishable from an update that did nothing — the Appearance
panel was in the installed package the whole time and could not be
found without quitting and relaunching. The daemon restarts on upgrade,
so its reported version is the authority on what is installed; when it
stops matching this process's own, the app says so and reopens. One
attempt only, so a version that never matches cannot become a restart
loop.

Also: the tray repaints immediately on an appearance change rather than
waiting for the next poll, and the stored appearance is applied before
first paint so the window does not flash the wrong theme on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:40:59 -05:00

319 lines
16 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hound Antivirus</title>
<link rel="stylesheet" href="styles.css" />
<link rel="icon" type="image/png" href="favicon-32.png" />
</head>
<body>
<div id="app">
<!-- ── Header ─────────────────────────────────────────────── -->
<header class="header">
<div class="brand">
<img src="hound-32.png" alt="" class="brand-mark" />
<div class="brand-text">
<h1>Hound</h1>
<span class="tagline">Antivirus for Linux</span>
</div>
</div>
<div class="header-status">
<span class="engine-dot" id="engine-dot" title="Engine status"></span>
<span class="engine-label" id="engine-label">connecting…</span>
</div>
</header>
<!-- ── Tabs ───────────────────────────────────────────────── -->
<nav class="tabs" role="tablist">
<button class="tab active" data-tab="protection" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Protection
</button>
<button class="tab" data-tab="quarantine" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8"/><path d="M1 3h22v5H1z"/><path d="M10 12h4"/></svg>
Quarantine
<span class="tab-badge hidden" id="badge-quarantine"></span>
</button>
<button class="tab" data-tab="realtime" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 12h4l3-9 4 18 3-9h6"/></svg>
Realtime
</button>
<button class="tab" data-tab="rootkit" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
Rootkit
</button>
<button class="tab" data-tab="alerts" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
Alerts
<span class="tab-badge hidden" id="badge-alerts"></span>
</button>
<button class="tab" data-tab="settings" role="tab">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
Settings
</button>
</nav>
<!-- ══ Protection ═══════════════════════════════════════════ -->
<section class="tab-panel active" id="panel-protection">
<section class="hero" id="hero">
<div class="shield" id="shield">
<img id="shield-icon" src="state-protected-48.png" alt="" width="48" height="48" />
</div>
<div class="hero-copy">
<h2 id="hero-title">Protected</h2>
<p id="hero-sub">Your system looks healthy.</p>
<div class="hero-meta">
<span class="pill" id="pill-db">signatures: —</span>
<span class="pill" id="pill-os"></span>
<span class="pill" id="pill-engine">engine: —</span>
</div>
</div>
</section>
<section class="actions">
<button class="btn primary" id="btn-scan-home">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
Scan Home
</button>
<button class="btn primary" id="btn-scan-custom">Scan Folder…</button>
<button class="btn" id="btn-update">Update Signatures</button>
<button class="btn" id="btn-pause">Pause Protection</button>
</section>
<section class="panel hidden" id="scan-panel">
<div class="panel-head">
<h3 id="scan-title">Scanning…</h3>
<span class="scan-target" id="scan-target"></span>
</div>
<div class="progress">
<div class="progress-bar" id="progress-bar"></div>
</div>
<p class="muted" id="scan-note">Working through the queue. This can take a while on large folders.</p>
</section>
<section class="panel" id="results-panel">
<div class="panel-head">
<h3>Last Scan Results</h3>
<span class="muted" id="results-meta"></span>
</div>
<div id="results-body" class="results">
<p class="muted empty">No scans yet. Hit <strong>Scan Home</strong> to start.</p>
</div>
</section>
<section class="panel hidden" id="update-panel">
<div class="panel-head">
<h3>Signature Update</h3>
</div>
<pre class="log" id="update-log"></pre>
</section>
</section>
<!-- ══ Quarantine ═══════════════════════════════════════════ -->
<section class="tab-panel" id="panel-quarantine">
<section class="panel">
<div class="panel-head">
<h3>Quarantine Vault</h3>
<div class="row-actions">
<span class="muted" id="qt-count"></span>
<button class="btn small" id="btn-qt-refresh">Refresh</button>
</div>
</div>
<div id="qt-body" class="qt-list">
<p class="muted empty">Loading…</p>
</div>
</section>
<section class="panel">
<div class="panel-head"><h3>Add Manually</h3></div>
<div class="form-row">
<input type="text" id="qt-add-path" class="input" placeholder="/path/to/suspicious/file" />
<input type="text" id="qt-add-virus" class="input" placeholder="label (e.g. manual-suspicious)" value="manual" />
<button class="btn" id="btn-qt-add">Quarantine</button>
</div>
<p class="muted" style="margin-top:8px">The file is moved into the vault; its original location is left empty.</p>
</section>
</section>
<!-- ══ Realtime ═════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-realtime">
<section class="panel">
<div class="panel-head">
<h3>Realtime Monitor</h3>
<label class="switch-row">
<span id="rt-enabled-label" class="muted">loading…</span>
<input type="checkbox" id="rt-toggle" />
<span class="switch"></span>
</label>
</div>
<div class="stat-grid" id="rt-stats">
<div class="stat"><span class="stat-val" id="rt-seen"></span><span class="stat-key">file events seen</span></div>
<div class="stat"><span class="stat-val" id="rt-quar"></span><span class="stat-key">auto-quarantined</span></div>
<div class="stat"><span class="stat-val" id="rt-uptime"></span><span class="stat-key">monitor uptime</span></div>
<div class="stat"><span class="stat-val" id="rt-ransom"></span><span class="stat-key">ransomware guard</span></div>
</div>
<div class="kv"><span class="kv-k">watching</span><span class="kv-v" id="rt-watch"></span></div>
<div class="kv"><span class="kv-k">last file event</span><span class="kv-v" id="rt-last"></span></div>
<div class="kv"><span class="kv-k">on detection</span><span class="kv-v" id="rt-action"></span></div>
</section>
<section class="panel">
<div class="panel-head"><h3>Watch Folders</h3></div>
<div id="rt-watch-list" class="watch-list"></div>
<p class="muted" style="margin-top:8px">Edit in Settings → Realtime. Restart required for the monitor to pick up new folders.</p>
</section>
</section>
<!-- ══ Rootkit ══════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-rootkit">
<section class="panel">
<div class="panel-head">
<h3>Rootkit Hunter</h3>
<button class="btn primary" id="btn-rootkit">Run Scan</button>
</div>
<div id="rootkit-banner" class="verdict hidden"></div>
<div id="rootkit-body" class="finding-list">
<p class="muted empty">Runs userspace heuristics: deleted-but-open executables, world-writable binaries, unexpected setuid, hidden /proc entries.</p>
</div>
</section>
</section>
<!-- ══ Alerts ═══════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-alerts">
<section class="panel">
<div class="panel-head">
<h3>Event Log</h3>
<div class="row-actions">
<span class="muted" id="alerts-count"></span>
<button class="btn small" id="btn-alerts-refresh">Refresh</button>
<button class="btn small danger" id="btn-alerts-clear">Clear Log</button>
</div>
</div>
<div id="alerts-body" class="event-list">
<p class="muted empty">Loading…</p>
</div>
</section>
</section>
<!-- ══ Settings ═════════════════════════════════════════════ -->
<section class="tab-panel" id="panel-settings">
<section class="panel">
<div class="panel-head"><h3>Appearance</h3></div>
<label class="switch-row setting">
<span><strong>Theme</strong><small>Follow your desktop, or pick one.</small></span>
<select id="set-theme" class="input compact">
<option value="auto">Follow system</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<label class="switch-row setting">
<span><strong>Monochromatic tray icon</strong><small>A single-tone glyph that follows your panel, instead of the colour states.</small></span>
<input type="checkbox" id="set-monochrome" /><span class="switch"></span>
</label>
</section>
<section class="panel">
<div class="panel-head"><h3>Protection</h3></div>
<label class="switch-row setting">
<span><strong>Pause all protection</strong><small>Suspends realtime and grays the tray.</small></span>
<input type="checkbox" id="set-paused" /><span class="switch"></span>
</label>
<label class="switch-row setting">
<span><strong>Auto-update signatures</strong><small>Daemon runs freshclam on a schedule.</small></span>
<input type="checkbox" id="set-autoupdate" /><span class="switch"></span>
</label>
<label class="switch-row setting">
<span><strong>Desktop notifications</strong><small>Pop an alert for critical events.</small></span>
<input type="checkbox" id="set-notify" /><span class="switch"></span>
</label>
</section>
<section class="panel">
<div class="panel-head"><h3>Realtime</h3></div>
<label class="switch-row setting">
<span><strong>Realtime monitor</strong><small>Watch folders and quarantine on the fly.</small></span>
<input type="checkbox" id="set-realtime" /><span class="switch"></span>
</label>
<div class="field">
<label for="set-watch">Watch folders (one per line)</label>
<textarea id="set-watch" class="input mono" rows="4" spellcheck="false"></textarea>
</div>
<div class="field">
<label for="set-ondetect">Action on detection</label>
<select id="set-ondetect" class="input">
<option value="quarantine">Quarantine automatically</option>
<option value="alert">Alert only (leave file in place)</option>
</select>
</div>
</section>
<section class="panel">
<div class="panel-head"><h3>Scanning</h3></div>
<div class="field">
<label for="set-maxsize">Max file size (MB)</label>
<input type="number" id="set-maxsize" class="input" min="1" />
</div>
<div class="field">
<label for="set-excludes">Excluded paths (one per line, exact or prefix)</label>
<textarea id="set-excludes" class="input mono" rows="4" spellcheck="false"></textarea>
</div>
<label class="switch-row setting">
<span><strong>Recursive by default</strong></span>
<input type="checkbox" id="set-recursive" /><span class="switch"></span>
</label>
</section>
<section class="panel">
<div class="panel-head"><h3>Ransomware Guard</h3></div>
<label class="switch-row setting">
<span><strong>Enable guard</strong><small>Write-burst heuristic trips an alarm under attack.</small></span>
<input type="checkbox" id="set-ransom" /><span class="switch"></span>
</label>
<div class="field">
<label for="set-ransom-thresh">Threshold (file-writes / minute)</label>
<input type="number" id="set-ransom-thresh" class="input" min="10" />
</div>
</section>
<section class="panel">
<div class="panel-head"><h3>Rootkit</h3></div>
<label class="switch-row setting">
<span><strong>Include in scans</strong><small>Run rootkit heuristics as part of the health check.</small></span>
<input type="checkbox" id="set-rootkit" /><span class="switch"></span>
</label>
</section>
<div class="actions">
<span class="muted" id="settings-msg"></span>
<span class="muted">Changes apply as you make them.</span>
</div>
</section>
<footer class="footer">
<span id="foot-ver">houndd —</span>
<span class="muted" id="foot-engine">engine —</span>
</footer>
</div>
<script type="module" src="app.js"></script>
<script>
// A front-end that fails to load looks identical to a daemon that never
// answered: the window just sits on its placeholder text. It happened
// once (an unresolvable bare module specifier) and cost real debugging
// time, so failure now announces itself.
setTimeout(function () {
if (window.__houndBooted) return;
var sub = document.getElementById("hero-sub");
if (sub) {
sub.textContent =
"The Hound interface failed to start. This is a bug in the app, not " +
"a problem with your system \u2014 please report it. Scanning still " +
"works from the terminal: hound scan ~/Downloads";
}
var label = document.getElementById("engine-label");
if (label) label.textContent = "interface error";
}, 5000);
</script>
</body>
</html>