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>
483 lines
17 KiB
CSS
483 lines
17 KiB
CSS
:root {
|
|
/* surfaces */
|
|
--bg: #0B0E14;
|
|
--bg-raised: #12161F;
|
|
--bg-panel: #161B26;
|
|
--bg-hover: #1D2432;
|
|
--border: #232B3A;
|
|
--border-hi: #32405A;
|
|
|
|
/* text */
|
|
--fg: #E8ECF4;
|
|
--fg-dim: #8B96AB;
|
|
--fg-faint: #5A6478;
|
|
|
|
/* brand + state (dog head, 4-state ladder) */
|
|
--brand: #9896E0;
|
|
--ok: #22C55E;
|
|
--warn: #F59E0B;
|
|
--bad: #EF4444;
|
|
--off: #6B7280;
|
|
|
|
--radius: 14px;
|
|
--radius-sm: 9px;
|
|
--shadow: 0 8px 30px rgb(0 0 0 / 0.45);
|
|
--bg-log: #0A0D13;
|
|
}
|
|
|
|
/* Light theme.
|
|
*
|
|
* Not an inversion of the dark palette — a flat inversion turns a deep navy
|
|
* ground into a saturated cream and makes the brand violet unreadable. The
|
|
* neutrals keep a slight violet bias so they read as chosen rather than as
|
|
* the browser's default grey, and the state colours are darkened until they
|
|
* hold their contrast against white rather than glowing on it.
|
|
*
|
|
* Applied by [data-theme] only. The app writes that attribute from the
|
|
* user's setting, resolving "auto" against prefers-color-scheme itself, so
|
|
* there is exactly one place that decides which theme is showing. */
|
|
:root[data-theme="light"] {
|
|
--bg: #F7F7FB;
|
|
--bg-raised: #FFFFFF;
|
|
--bg-panel: #FFFFFF;
|
|
--bg-hover: #EEEEF6;
|
|
--border: #E0E0EC;
|
|
--border-hi: #C6C6DC;
|
|
|
|
--fg: #1A1B26;
|
|
--fg-dim: #5A5C72;
|
|
--fg-faint: #8A8CA3;
|
|
|
|
/* Darkened for contrast on a light ground: the dark theme's #22C55E is
|
|
2.2:1 on white and unreadable as text. */
|
|
--brand: #5A58C8;
|
|
--ok: #16A34A;
|
|
--warn: #B45309;
|
|
--bad: #DC2626;
|
|
--off: #6B7280;
|
|
|
|
--shadow: 0 8px 24px rgb(26 27 38 / 0.10);
|
|
/* A console keeps its recessed feel in light mode without going black. */
|
|
--bg-log: #F0F0F7;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
html, body {
|
|
height: 100%;
|
|
background: radial-gradient(1200px 700px at 20% -10%, #141B2B 0%, var(--bg) 55%);
|
|
color: var(--fg);
|
|
font: 15px/1.5 "Inter", "Cantarell", "Segoe UI", system-ui, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
#app {
|
|
max-width: 920px;
|
|
margin: 0 auto;
|
|
padding: 26px 26px 18px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* ── Header ─────────────────────────────────────────────────────── */
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
.brand-mark { width: 34px; height: 34px; filter: drop-shadow(0 2px 8px rgb(152 150 224 / .35)); }
|
|
.brand-text h1 { font-size: 20px; font-weight: 700; letter-spacing: .2px; }
|
|
.tagline { font-size: 12px; color: var(--fg-dim); }
|
|
|
|
.header-status { display: flex; align-items: center; gap: 8px; }
|
|
.engine-dot {
|
|
width: 9px; height: 9px; border-radius: 50%;
|
|
background: var(--off);
|
|
box-shadow: 0 0 8px rgb(107 114 128 / .6);
|
|
transition: background .3s, box-shadow .3s;
|
|
}
|
|
.engine-dot.ok { background: var(--ok); box-shadow: 0 0 10px rgb(34 197 94 / .7); }
|
|
.engine-dot.warn { background: var(--warn); box-shadow: 0 0 10px rgb(245 158 11 / .7); }
|
|
.engine-dot.bad { background: var(--bad); box-shadow: 0 0 10px rgb(239 68 68 / .7); }
|
|
.engine-label { font-size: 12.5px; color: var(--fg-dim); }
|
|
|
|
/* ── Tabs ───────────────────────────────────────────────────────── */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 5px;
|
|
overflow-x: auto;
|
|
}
|
|
.tab {
|
|
appearance: none;
|
|
border: 1px solid transparent;
|
|
background: transparent;
|
|
color: var(--fg-dim);
|
|
font: 600 13px/1 inherit;
|
|
border-radius: 10px;
|
|
padding: 9px 14px;
|
|
cursor: pointer;
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
white-space: nowrap;
|
|
transition: background .15s, color .15s, border-color .15s;
|
|
position: relative;
|
|
}
|
|
.tab:hover { background: var(--bg-hover); color: var(--fg); }
|
|
.tab.active {
|
|
background: linear-gradient(180deg, #232B40, #1B2233);
|
|
border-color: var(--border-hi);
|
|
color: var(--fg);
|
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / .04);
|
|
}
|
|
.tab svg { opacity: .8; }
|
|
.tab-badge {
|
|
min-width: 18px;
|
|
font-size: 10.5px;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
padding: 1px 5px;
|
|
border-radius: 999px;
|
|
background: var(--bad);
|
|
color: #fff;
|
|
line-height: 1.5;
|
|
}
|
|
.tab-badge.quiet { background: var(--warn); }
|
|
|
|
.tab-panel { display: none; flex-direction: column; gap: 18px; }
|
|
.tab-panel.active { display: flex; }
|
|
|
|
/* ── Hero ───────────────────────────────────────────────────────── */
|
|
.hero {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22px;
|
|
background: linear-gradient(180deg, var(--bg-panel), var(--bg-raised));
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 24px 26px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.shield {
|
|
flex: 0 0 auto;
|
|
width: 84px; height: 84px;
|
|
display: grid; place-items: center;
|
|
border-radius: 50%;
|
|
background: rgb(152 150 224 / .07);
|
|
border: 1px solid rgb(152 150 224 / .18);
|
|
transition: border-color .3s, background .3s;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); opacity: 1; }
|
|
50% { transform: scale(1.06); opacity: .85; }
|
|
}
|
|
.hero-copy h2 { font-size: 22px; font-weight: 700; }
|
|
.hero-copy p { color: var(--fg-dim); margin: 4px 0 12px; }
|
|
|
|
.hero[data-state="protected"] .shield { border-color: rgb(34 197 94 / .35); background: rgb(34 197 94 / .08); }
|
|
.hero[data-state="scanning"] .shield { border-color: rgb(245 158 11 / .4); background: rgb(245 158 11 / .09); }
|
|
.hero[data-state="threat"] .shield { border-color: rgb(239 68 68 / .45); background: rgb(239 68 68 / .1); }
|
|
.hero[data-state="paused"] .shield { border-color: rgb(107 114 128 / .4); background: rgb(107 114 128 / .09); }
|
|
.hero[data-state="scanning"] #shield-icon { animation: pulse 1.6s ease-in-out infinite; }
|
|
|
|
.pill {
|
|
display: inline-block;
|
|
font-size: 12px;
|
|
color: var(--fg-dim);
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
padding: 3px 11px;
|
|
margin-right: 8px;
|
|
white-space: nowrap;
|
|
}
|
|
.pill.stale { color: var(--warn); border-color: rgb(245 158 11 / .4); }
|
|
|
|
/* ── Actions ────────────────────────────────────────────────────── */
|
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
.btn {
|
|
appearance: none;
|
|
border: 1px solid var(--border-hi);
|
|
background: var(--bg-hover);
|
|
color: var(--fg);
|
|
font: 600 13.5px/1 inherit;
|
|
border-radius: var(--radius-sm);
|
|
padding: 11px 16px;
|
|
cursor: pointer;
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
transition: background .15s, border-color .15s, transform .05s;
|
|
}
|
|
.btn:hover { background: #232C3E; border-color: #41507a; }
|
|
.btn:active { transform: translateY(1px); }
|
|
.btn:disabled { opacity: .5; cursor: default; }
|
|
.btn.primary {
|
|
background: linear-gradient(180deg, #5A58D6, #4543C4);
|
|
border-color: #6a68e6;
|
|
}
|
|
.btn.primary:hover { background: linear-gradient(180deg, #6765e0, #4f4dd4); }
|
|
.btn.small { padding: 7px 12px; font-size: 12.5px; }
|
|
.btn.danger { border-color: rgb(239 68 68 / .5); }
|
|
.btn.danger:hover { background: rgb(239 68 68 / .15); border-color: var(--bad); }
|
|
|
|
/* ── Panels ─────────────────────────────────────────────────────── */
|
|
.panel {
|
|
background: var(--bg-panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 16px 18px;
|
|
}
|
|
.panel-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
gap: 12px;
|
|
}
|
|
.panel-head h3 { font-size: 14px; font-weight: 700; letter-spacing: .3px; text-transform: uppercase; color: var(--fg-dim); }
|
|
.muted { color: var(--fg-dim); font-size: 13px; }
|
|
.empty { padding: 6px 0 2px; }
|
|
.hidden { display: none !important; }
|
|
.row-actions { display: flex; align-items: center; gap: 10px; }
|
|
.scan-target { font-family: ui-monospace, monospace; font-size: 12.5px; color: var(--fg-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
/* scan progress */
|
|
.progress {
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: var(--bg-hover);
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
.progress-bar {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
|
border-radius: inherit;
|
|
transition: width .4s ease;
|
|
}
|
|
.progress.indeterminate .progress-bar {
|
|
width: 35%;
|
|
animation: slide 1.3s ease-in-out infinite alternate;
|
|
}
|
|
@keyframes slide { from { margin-left: 0; } to { margin-left: 65%; } }
|
|
|
|
/* results */
|
|
.results { display: flex; flex-direction: column; gap: 6px; }
|
|
.result-row {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 14px;
|
|
}
|
|
.result-row .sig { font-weight: 700; font-size: 13.5px; }
|
|
.result-row .path { color: var(--fg-dim); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, monospace; }
|
|
.result-row.clean { border-left: 3px solid var(--ok); }
|
|
.result-row.clean .sig { color: var(--ok); }
|
|
.result-row.infected { border-left: 3px solid var(--bad); }
|
|
.result-row.infected .sig { color: var(--bad); }
|
|
|
|
/* log */
|
|
.log {
|
|
background: var(--bg-log);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 12px 14px;
|
|
font: 12px/1.6 ui-monospace, "JetBrains Mono", monospace;
|
|
color: var(--fg-dim);
|
|
max-height: 220px;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
.log.ok { color: #7fd79a; }
|
|
.log.fail { color: #f2a2a2; }
|
|
|
|
/* ── Inputs & switches ──────────────────────────────────────────── */
|
|
.input {
|
|
appearance: none;
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border-hi);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--fg);
|
|
font: 13.5px/1.4 inherit;
|
|
padding: 9px 12px;
|
|
width: 100%;
|
|
}
|
|
.input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgb(152 150 224 / .15); }
|
|
.input.mono { font-family: ui-monospace, "JetBrains Mono", monospace; font-size: 12.5px; }
|
|
textarea.input { resize: vertical; }
|
|
select.input { cursor: pointer; }
|
|
|
|
.form-row { display: grid; grid-template-columns: 1fr 200px auto; gap: 10px; }
|
|
|
|
.field { margin: 14px 0; }
|
|
.field > label { display: block; font-size: 13px; font-weight: 600; color: var(--fg-dim); margin-bottom: 6px; }
|
|
|
|
.switch-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
cursor: pointer;
|
|
padding: 4px 0;
|
|
}
|
|
.switch-row.setting { padding: 10px 0; border-bottom: 1px solid var(--border); }
|
|
.switch-row.setting:last-of-type { border-bottom: none; }
|
|
.switch-row strong { display: block; font-size: 14px; font-weight: 600; }
|
|
.switch-row small { display: block; color: var(--fg-dim); font-size: 12px; margin-top: 2px; }
|
|
.switch-row input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
|
|
.switch {
|
|
flex: 0 0 auto;
|
|
width: 40px; height: 22px;
|
|
border-radius: 999px;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border-hi);
|
|
position: relative;
|
|
transition: background .2s, border-color .2s;
|
|
}
|
|
.switch::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2px; left: 2px;
|
|
width: 16px; height: 16px;
|
|
border-radius: 50%;
|
|
background: var(--fg-dim);
|
|
transition: transform .2s, background .2s;
|
|
}
|
|
.switch-row input:checked + .switch { background: rgb(90 88 214 / .55); border-color: #6a68e6; }
|
|
.switch-row input:checked + .switch::after { transform: translateX(18px); background: #fff; }
|
|
|
|
/* ── Quarantine ─────────────────────────────────────────────────── */
|
|
.qt-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.qt-row {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--bad);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 14px;
|
|
}
|
|
.qt-row.restored { border-left-color: var(--off); opacity: .75; }
|
|
.qt-row .qt-virus { font-weight: 700; font-size: 13px; color: var(--bad); white-space: nowrap; }
|
|
.qt-row.restored .qt-virus { color: var(--fg-dim); }
|
|
.qt-row .qt-path { font-family: ui-monospace, monospace; font-size: 12px; color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.qt-row .qt-meta { font-size: 11.5px; color: var(--fg-faint); white-space: nowrap; }
|
|
.qt-row .qt-actions { display: flex; gap: 6px; }
|
|
|
|
/* ── Realtime ───────────────────────────────────────────────────── */
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 10px;
|
|
margin: 12px 0 6px;
|
|
}
|
|
.stat {
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 12px 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.stat-val { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
.stat-val.alarm { color: var(--bad); }
|
|
.stat-val.watching { color: var(--warn); }
|
|
.stat-val.calm { color: var(--ok); }
|
|
.stat-key { font-size: 11.5px; color: var(--fg-faint); text-transform: uppercase; letter-spacing: .4px; }
|
|
|
|
.kv {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 8px 0;
|
|
border-top: 1px solid var(--border);
|
|
font-size: 13px;
|
|
}
|
|
.kv-k { flex: 0 0 150px; color: var(--fg-faint); }
|
|
.kv-v { color: var(--fg-dim); font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
|
|
|
|
.watch-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.watch-chip {
|
|
font-family: ui-monospace, monospace;
|
|
font-size: 12px;
|
|
color: var(--fg-dim);
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
padding: 4px 11px;
|
|
}
|
|
|
|
/* ── Rootkit ────────────────────────────────────────────────────── */
|
|
.verdict {
|
|
border-radius: var(--radius-sm);
|
|
padding: 12px 16px;
|
|
margin-bottom: 12px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
border: 1px solid;
|
|
}
|
|
.verdict.clean { background: rgb(34 197 94 / .08); border-color: rgb(34 197 94 / .35); color: #7fd79a; }
|
|
.verdict.dirty { background: rgb(239 68 68 / .08); border-color: rgb(239 68 68 / .4); color: #f2a2a2; }
|
|
|
|
.finding-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.finding-row {
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr;
|
|
gap: 12px;
|
|
align-items: center;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 14px;
|
|
}
|
|
.finding-row .sev {
|
|
font-size: 10.5px; font-weight: 700;
|
|
text-transform: uppercase; letter-spacing: .5px;
|
|
padding: 3px 9px; border-radius: 999px;
|
|
}
|
|
.sev.critical { background: rgb(239 68 68 / .15); color: var(--bad); }
|
|
.sev.warn { background: rgb(245 158 11 / .15); color: var(--warn); }
|
|
.sev.info { background: rgb(107 114 128 / .2); color: var(--fg-dim); }
|
|
.finding-row .check { font-family: ui-monospace, monospace; font-size: 12px; color: var(--fg-dim); }
|
|
.finding-row .detail { font-size: 13px; color: var(--fg); word-break: break-all; }
|
|
|
|
/* ── Alerts ─────────────────────────────────────────────────────── */
|
|
.event-list { display: flex; flex-direction: column; gap: 4px; max-height: 520px; overflow-y: auto; }
|
|
.event-row {
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
border-radius: var(--radius-sm);
|
|
padding: 8px 14px;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.event-row .ev-sev { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; width: 64px; }
|
|
.ev-sev.critical { color: var(--bad); }
|
|
.ev-sev.warn { color: var(--warn); }
|
|
.ev-sev.info { color: var(--fg-faint); }
|
|
.event-row .ev-kind { font-family: ui-monospace, monospace; font-size: 11.5px; color: var(--fg-dim); width: 96px; }
|
|
.event-row .ev-msg { font-size: 13px; color: var(--fg); word-break: break-word; }
|
|
.event-row .ev-ts { font-size: 11.5px; color: var(--fg-faint); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
|
|
/* footer */
|
|
.footer {
|
|
margin-top: auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
color: var(--fg-faint);
|
|
padding-top: 4px;
|
|
}
|