From 4d5906e1e92220bde00fc2c0f52e1370da460fb8 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 11 May 2026 13:00:11 -0700 Subject: [PATCH] feat: severity overrides + last_ingested timestamp on dashboard --- app/rest.py | 23 +++++- app/services/search.py | 65 +++++++++++++---- web/src/views/DashboardView.vue | 26 ++++++- web/src/views/SettingsView.vue | 119 +++++++++++++++++++++++++++++++- 4 files changed, 214 insertions(+), 19 deletions(-) diff --git a/app/rest.py b/app/rest.py index 156ea36..09c3903 100644 --- a/app/rest.py +++ b/app/rest.py @@ -62,10 +62,18 @@ def _startup() -> None: ensure_schema(DB_PATH) -_PREFS_DEFAULTS: dict[str, str] = { +_PREFS_DEFAULTS: dict = { "entry_point_style": "topbar", "llm_url": "http://localhost:11434", "llm_model": "llama3.1:8b", + "severity_overrides": [ + { + "name": "PAM auth noise", + "pattern": r"pam_unix.*auth(?:entication)?\s+fail|auth could not identify", + "override_severity": "WARN", + "enabled": True, + } + ], } @@ -89,10 +97,18 @@ class DiagnoseRequest(BaseModel): until: str | None = None +class SeverityOverride(BaseModel): + name: str + pattern: str + override_severity: str + enabled: bool = True + + class SettingsBody(BaseModel): entry_point_style: str | None = None llm_url: str | None = None llm_model: str | None = None + severity_overrides: list[SeverityOverride] | None = None class IncidentCreate(BaseModel): @@ -244,6 +260,8 @@ def patch_settings(body: SettingsBody) -> dict: prefs["llm_url"] = body.llm_url if body.llm_model is not None: prefs["llm_model"] = body.llm_model + if body.severity_overrides is not None: + prefs["severity_overrides"] = [o.model_dump() for o in body.severity_overrides] _save_prefs(prefs) return prefs @@ -257,7 +275,8 @@ def list_sources() -> dict: def get_stats( window: Annotated[int, Query(ge=1, le=168, description="Hours to look back")] = 24, ) -> dict: - return _stats(DB_PATH, window_hours=window) + prefs = _load_prefs() + return _stats(DB_PATH, window_hours=window, severity_overrides=prefs.get("severity_overrides", [])) @router.post("/api/incidents") diff --git a/app/services/search.py b/app/services/search.py index 9334605..1665eb2 100644 --- a/app/services/search.py +++ b/app/services/search.py @@ -317,11 +317,33 @@ def list_sources(db_path: Path) -> list[dict]: ] -def stats_summary(db_path: Path, window_hours: int = 24) -> dict: +def _compile_overrides(overrides: list[dict]) -> list[tuple[re.Pattern[str], str]]: + """Return (compiled_pattern, override_severity) pairs for enabled rules.""" + compiled = [] + for rule in overrides: + if not rule.get("enabled", True): + continue + try: + compiled.append((re.compile(rule["pattern"], re.IGNORECASE), rule["override_severity"])) + except re.error: + pass + return compiled + + +def _apply_overrides(text: str, original_severity: str, rules: list[tuple[re.Pattern[str], str]]) -> str: + for pattern, new_sev in rules: + if pattern.search(text): + return new_sev + return original_severity + + +def stats_summary(db_path: Path, window_hours: int = 24, severity_overrides: list[dict] | None = None) -> dict: """Return aggregate health stats for the dashboard. Queries plain log_entries (not FTS) so it works even before the index is built. """ + rules = _compile_overrides(severity_overrides or []) + conn = sqlite3.connect(str(db_path)) conn.execute("PRAGMA journal_mode=WAL") conn.row_factory = sqlite3.Row @@ -365,25 +387,36 @@ def stats_summary(db_path: Path, window_hours: int = 24) -> dict: for r in source_rows ] - # 5 most recent critical entries + # Fetch candidate criticals (fetch more so filtering doesn't leave us with too few) crit_rows = conn.execute(""" - SELECT id as entry_id, source_id, sequence, timestamp_iso, severity, - repeat_count, out_of_order, matched_patterns, text, 0.0 as rank + SELECT id as entry_id, source_id, timestamp_iso, severity, text FROM log_entries WHERE severity = 'CRITICAL' AND repeat_count = 1 ORDER BY timestamp_iso DESC - LIMIT 5 + LIMIT 25 """).fetchall() - recent_criticals = [ - { - "entry_id": r["entry_id"], - "source_id": r["source_id"], - "timestamp_iso": r["timestamp_iso"], - "severity": r["severity"], - "text": r["text"], - } - for r in crit_rows - ] + + # Apply overrides: skip entries whose effective severity is no longer CRITICAL + suppressed = 0 + recent_criticals = [] + for r in crit_rows: + effective = _apply_overrides(r["text"], r["severity"], rules) + if effective == "CRITICAL": + recent_criticals.append({ + "entry_id": r["entry_id"], + "source_id": r["source_id"], + "timestamp_iso": r["timestamp_iso"], + "severity": r["severity"], + "text": r["text"], + }) + if len(recent_criticals) == 5: + break + else: + suppressed += 1 + + # When did we last ingest anything? + last_row = conn.execute("SELECT MAX(ingest_time) AS t FROM log_entries").fetchone() + last_ingested: str | None = last_row["t"] if last_row else None conn.close() @@ -394,6 +427,8 @@ def stats_summary(db_path: Path, window_hours: int = 24) -> dict: "errors_24h": errors_24h, "source_health": source_health, "recent_criticals": recent_criticals, + "suppressed_criticals": suppressed, + "last_ingested": last_ingested, } diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 9b88ff4..21805bc 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -1,6 +1,15 @@