Antivirus/gui/dist/index.html
dev ec1fa4e26f 0.1.3: appearance settings, and stop detecting our own quarantine
A home scan reported two EICAR threats whose paths were Hound's own
vault. SKIP_PREFIXES held the system vault at /var/lib/hound/vault but
not the per-user one under $XDG_DATA_HOME/hound/quarantine, which is a
different absolute path for every user and so cannot be a literal. The
check now matches a `hound` component followed by `quarantine` or
`vault` anywhere in the path, with a test that ordinary paths merely
mentioning either word are still scanned. A scanner that detects its
own evidence locker reports threats that no longer exist anywhere they
can hurt anyone.

Appearance settings, the third pair of fields declared from the start
and wired to nothing (after close_to_tray and auto_update_signatures):

  - Theme: follow system | light | dark, previewing live rather than
    only on save, since a control that does nothing until you press
    another control feels broken.
  - Monochromatic tray icon: one glyph instead of the colour ladder.
    The tooltip still names the state, so only the colour is dropped.

The light theme had to be built — the stylesheet was dark-only. It is
not an inversion: the state colours are darkened until they hold their
contrast on white (the dark theme's green is 2.2:1 there, unreadable as
text) and the neutrals keep a slight violet bias so they read as chosen
rather than as a default grey. One hardcoded near-black on the log
panel would have been near-invisible in light mode; it is a token now.

Which monochrome tone to draw depends on the panel, and no portable way
exists to ask a panel what colour it is — so the webview resolves the
theme (including "follow system", which only prefers-color-scheme can
answer) and tells the tray. One place decides which theme is showing.

The tray now announces a release once per version rather than once per
poll, and its menu entry reads "Update available — install Hound X…".

Also removes ~120 lines: the GUI had its own download, hash-check and
staging implementation for updates. `hound update` does exactly that,
as root, and is the path with tests behind it, so the app runs it under
pkexec instead. That left `hound stage-update` with no caller, and dead
privileged code paths are liabilities, so it is gone.

Verified end to end: 0.1.2 discovered 0.1.3 and installed it
unattended. 380 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:28:05 -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">
<button class="btn primary" id="btn-settings-save">Save Settings</button>
<span class="muted" id="settings-msg"></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>