Add web frontend with JWT auth, RBAC, SSE dashboard, and config editor

- FastAPI + htmx + Jinja2 web frontend, started with --web flag
- JWT HS256 auth (WEB_SECRET_KEY) with httpOnly cookies; access (15 min) +
  refresh (7 day) tokens; refresh rotation + JTI revocation in data/web.db
- RBAC: superadmin > admin > reader enforced per route
- Live SSE dashboard fed by tui/events broadcast queue
- Config editor: keyword groups and channel list saved to data/runtime_config.json
  and hot-reloaded in-process (scorer.reload_from_config, signal_channel_changed)
- config.py migrated to load groups/channels from runtime_config.json;
  falls back to hardcoded defaults when file absent
- tui/events.py: subscribe/unsubscribe broadcast, set_bot_context/signal_channel_changed
- utils/scorer.py: import config as _config (fixes local binding); reload_from_config()
- utils/database.py: count_by_severity, recent_for_domains, count_by_severity_for_domains
- 53 new tests (events bus, JWT lifecycle, web DB CRUD, RBAC enforcement,
  config round-trip); total 141 passing
This commit is contained in:
2026-04-02 11:41:46 -03:00
parent b28168c846
commit 4c104cddd2
32 changed files with 2093 additions and 47 deletions

162
web/static/style.css Normal file
View File

@@ -0,0 +1,162 @@
/* ULPgrammer web UI — minimal, dark-ish */
:root {
--bg: #1a1a1a;
--surface: #252525;
--border: #3a3a3a;
--text: #e0e0e0;
--muted: #888;
--critical:#ff4444;
--high: #ff8800;
--medium: #ffcc00;
--low: #44bb44;
--accent: #4a90d9;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* Nav */
nav {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0.6rem 1.2rem;
}
.nav-brand { font-weight: 700; font-size: 1rem; color: var(--text); }
.nav-links { display: flex; gap: 1rem; align-items: center; }
main { padding: 1.2rem 1.4rem; max-width: 1200px; }
h2 { margin: 0.8rem 0 0.6rem; }
h3 { margin: 0.6rem 0 0.4rem; }
/* Stats bar */
.stats-bar {
display: flex;
gap: 1rem;
margin: 0.8rem 0;
padding: 0.5rem 0.8rem;
background: var(--surface);
border-radius: 6px;
border: 1px solid var(--border);
}
.stat { font-weight: 600; }
.stat.critical { color: var(--critical); }
.stat.high { color: var(--high); }
.stat.medium { color: var(--medium); }
.stat.low { color: var(--low); }
/* Groups bar */
.groups-bar { margin: 0.5rem 0 1rem; }
.group-pill {
display: inline-block;
margin: 0 0.3rem 0.3rem 0;
padding: 0.2rem 0.6rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
font-size: 0.85rem;
}
/* Hit cards */
.hit-card {
padding: 0.5rem 0.8rem;
margin: 0.4rem 0;
border-left: 4px solid var(--border);
background: var(--surface);
border-radius: 0 4px 4px 0;
}
.hit-card.sev-critical { border-color: var(--critical); }
.hit-card.sev-high { border-color: var(--high); }
.hit-card.sev-medium { border-color: var(--medium); }
.hit-card.sev-low { border-color: var(--low); }
.sev-badge {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
padding: 0.1rem 0.4rem;
border-radius: 3px;
margin-right: 0.5rem;
background: var(--border);
}
.sev-critical .sev-badge { background: var(--critical); color: #fff; }
.sev-high .sev-badge { background: var(--high); color: #000; }
.sev-medium .sev-badge { background: var(--medium); color: #000; }
.sev-low .sev-badge { background: var(--low); color: #000; }
code.raw { font-family: monospace; font-size: 0.9rem; word-break: break-all; }
.meta { color: var(--muted); font-size: 0.8rem; margin-left: 0.5rem; }
.reasons { margin: 0.2rem 0 0 1.5rem; color: var(--muted); font-size: 0.8rem; }
/* Tables */
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 0.8rem;
}
.data-table th, .data-table td {
padding: 0.4rem 0.6rem;
border: 1px solid var(--border);
text-align: left;
}
.data-table th { background: var(--surface); }
/* Forms */
label { display: block; margin: 0.4rem 0; }
label input, label select { display: block; width: 100%; margin-top: 0.2rem; }
input, select, textarea {
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.3rem 0.5rem;
border-radius: 4px;
}
input:focus, select:focus { outline: 1px solid var(--accent); }
button { cursor: pointer; padding: 0.3rem 0.7rem; border-radius: 4px; border: 1px solid var(--border); background: var(--surface); color: var(--text); }
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-danger { background: var(--critical); color: #fff; border-color: var(--critical); }
.btn-add { margin-top: 0.3rem; }
.btn-link { background: none; border: none; color: var(--accent); padding: 0; }
/* Tabs */
.tabs { display: flex; gap: 0.5rem; margin: 0.6rem 0; }
.tab { padding: 0.3rem 0.8rem; border-radius: 4px; background: var(--surface); border: 1px solid var(--border); }
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
/* Config fieldsets */
.group-fieldset { border: 1px solid var(--border); padding: 0.7rem; margin: 0.6rem 0; border-radius: 4px; }
.group-fieldset legend input { background: transparent; border: none; font-size: 1rem; font-weight: 600; color: var(--text); }
.patterns-table { width: 100%; border-collapse: collapse; }
.patterns-table td { padding: 0.2rem 0.4rem; }
.patterns-table input { width: 100%; }
/* Login */
.login-body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-box { width: 320px; padding: 2rem; background: var(--surface); border-radius: 8px; border: 1px solid var(--border); }
.login-box h1 { margin-bottom: 1rem; text-align: center; }
.login-box button { width: 100%; margin-top: 0.8rem; }
/* Misc */
.error { color: var(--critical); margin: 0.4rem 0; }
.hint { color: var(--muted); font-size: 0.85rem; margin: 0.3rem 0; }
.empty { color: var(--muted); font-style: italic; }
dialog { background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; min-width: 340px; }
dialog::backdrop { background: rgba(0,0,0,0.6); }
.dialog-actions { display: flex; gap: 0.5rem; margin-top: 0.8rem; }
.patterns-list { margin: 0.5rem 0; }
details summary { cursor: pointer; }