From 48b56852cf4798ff3b10679e62559d4ac24b3542b1d4229676e99ac933ba3f97 Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 2 Jul 2026 20:26:42 +0200 Subject: [PATCH] fix entriy view UI --- src/fgai/dashboard.py | 2 +- src/fgai/monitor.py | 32 ++++++++++++++++++++++++-------- tests/test_monitor.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index c245808..68f6010 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -561,7 +561,7 @@ async function refresh() { `MCP coverage: ${esc(displayedCoverage)}${mcp.partial_streams ? ` (${esc(mcp.partial_streams)} partial)` : ''}${mcp.truncated_streams ? ` (${esc(mcp.truncated_streams)} truncated)` : ''}${mcp.sample_limited_streams ? ` (${esc(mcp.sample_limited_streams)} sample capped)` : ''}`, configuration.log_source === 'graylog_mcp' && mcp.status === 'no_streams_enabled' ? `No Graylog streams are enabled. Open Settings, Load streams, tick streams, then click Save streams.` : '', configuration.log_source === 'graylog_mcp' && mcp.status !== 'refreshing' && enabledStreams.length > 0 && displayedRawEvents === 0 ? `No raw events fetched from enabled streams. Check poll window, Graylog query, stream permissions, and Diagnostics -> Stream Coverage.` : '', - enabledWithNoRawEvents.length ? `${esc(enabledWithNoRawEvents.length)} enabled stream(s) returned zero raw events in ${esc(zeroRawWindow)}.` : '', + enabledWithNoRawEvents.length && mcp.status !== 'refreshing' ? `${esc(enabledWithNoRawEvents.length)} enabled stream(s) returned zero raw events in ${esc(zeroRawWindow)}.` : '', mcp.coverage_warning ? `${esc(mcp.coverage_warning)}` : '', advisor.status === 'error' ? `Profile advisor error: ${esc(advisor.error || 'unknown error')}` : '' ].filter(Boolean).join('
'); diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index c4da825..6a827e8 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -480,7 +480,7 @@ def write_status(status: dict[str, object], output: str) -> None: tmp_path.replace(output_path) -def write_refreshing_status(output: str) -> None: +def write_refreshing_status(output: str, *, cache_path: str | None = None) -> None: output_path = Path(output) try: current = json.loads(output_path.read_text(encoding="utf-8")) if output_path.exists() else {} @@ -488,6 +488,12 @@ def write_refreshing_status(output: str) -> None: current = {} if not isinstance(current, dict): current = {} + used_cache = False + if cache_path: + cached = StatusSnapshotStore(cache_path).load("last_good") + if cached: + current = cached + used_cache = True previous_mcp = current.get("capabilities", {}).get("graylog_mcp", {}) if isinstance(current.get("capabilities"), dict) else {} current.setdefault("status_schema", 2) current["generated_at"] = int(time.time()) @@ -495,17 +501,27 @@ def write_refreshing_status(output: str) -> None: current["stale_reason"] = "" capabilities = current.setdefault("capabilities", {}) if isinstance(capabilities, dict): + previous_events = previous_mcp.get("events_fetched", 0) if isinstance(previous_mcp, dict) else 0 + previous_raw_events = previous_mcp.get("raw_events_fetched", 0) if isinstance(previous_mcp, dict) else 0 + previous_aggregate_events = previous_mcp.get("aggregate_events", 0) if isinstance(previous_mcp, dict) else 0 + previous_fetch_mode = previous_mcp.get("fetch_mode", "") if isinstance(previous_mcp, dict) else "" + previous_coverage_status = previous_mcp.get("coverage_status", "") if isinstance(previous_mcp, dict) else "" capabilities["graylog_mcp"] = { "status": "refreshing", "previous_status": previous_mcp.get("status", "") if isinstance(previous_mcp, dict) else "", "previous_error": previous_mcp.get("error", "") if isinstance(previous_mcp, dict) else "", - "previous_events_fetched": previous_mcp.get("events_fetched", 0) if isinstance(previous_mcp, dict) else 0, - "previous_raw_events_fetched": previous_mcp.get("raw_events_fetched", 0) if isinstance(previous_mcp, dict) else 0, - "previous_aggregate_events": previous_mcp.get("aggregate_events", 0) if isinstance(previous_mcp, dict) else 0, - "previous_fetch_mode": previous_mcp.get("fetch_mode", "") if isinstance(previous_mcp, dict) else "", - "previous_coverage_status": previous_mcp.get("coverage_status", "") if isinstance(previous_mcp, dict) else "", + "previous_events_fetched": previous_events, + "previous_raw_events_fetched": previous_raw_events, + "previous_aggregate_events": previous_aggregate_events, + "previous_fetch_mode": previous_fetch_mode, + "previous_coverage_status": previous_coverage_status, + "events_fetched": previous_events, + "raw_events_fetched": previous_raw_events, + "aggregate_events": previous_aggregate_events, + "fetch_mode": previous_fetch_mode, + "coverage_status": previous_coverage_status or "refreshing", } - current["status_cache"] = {"served_from_cache": False, "reason": "refreshing"} + current["status_cache"] = {"served_from_cache": used_cache, "reason": "refreshing"} write_status(current, output) @@ -534,7 +550,7 @@ def monitor_loop( effective_llm = bool(runtime.get("llm_enabled")) if runtime else llm effective_model = str(runtime.get("llm_model") or llm_model or "") if runtime.get("log_source") == "graylog_mcp": - write_refreshing_status(output) + write_refreshing_status(output, cache_path=status_cache_path) status = build_status( log_path, policy_path=policy_path, anomaly_limit=anomaly_limit, baseline_path=baseline_path, config_path=config_path, history_path=history_path, diff --git a/tests/test_monitor.py b/tests/test_monitor.py index f4c4752..1d7616e 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -68,6 +68,35 @@ class MonitorTests(unittest.TestCase): self.assertEqual(status["capabilities"]["graylog_mcp"]["previous_fetch_mode"], "aggregate") self.assertFalse(status["status_cache"]["served_from_cache"]) + def test_write_refreshing_status_uses_last_good_cache(self): + with tempfile.TemporaryDirectory() as tmp: + output = Path(tmp) / "status.json" + cache_path = str(Path(tmp) / "status-cache.sqlite3") + output.write_text( + json.dumps({"capabilities": {"graylog_mcp": {"status": "refreshing", "events_fetched": 0, "aggregate_events": 0}}}), + encoding="utf-8", + ) + StatusSnapshotStore(cache_path).save( + "last_good", + { + "summary": {"total": 1234}, + "capabilities": {"graylog_mcp": {"status": "connected", "events_fetched": 99, "raw_events_fetched": 88, "aggregate_events": 1234, "fetch_mode": "aggregate", "coverage_status": "complete_window"}}, + "cross_source_correlations": [{"entity": "10.0.0.1", "entity_label": "host01 (10.0.0.1)"}], + }, + ) + + write_refreshing_status(str(output), cache_path=cache_path) + + status = json.loads(output.read_text(encoding="utf-8")) + mcp = status["capabilities"]["graylog_mcp"] + self.assertEqual(status["summary"]["total"], 1234) + self.assertEqual(mcp["status"], "refreshing") + self.assertEqual(mcp["events_fetched"], 99) + self.assertEqual(mcp["raw_events_fetched"], 88) + self.assertEqual(mcp["aggregate_events"], 1234) + self.assertEqual(mcp["previous_status"], "connected") + self.assertTrue(status["status_cache"]["served_from_cache"]) + def test_profile_readiness_includes_stream_and_profile_names(self): with tempfile.TemporaryDirectory() as tmp: config_path = Path(tmp) / "config.json"