diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 10bf63c..64dbf33 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -535,6 +535,11 @@ async function refresh() { const displayedFetchMode = mcp.fetch_mode || mcp.previous_fetch_mode || configuration.graylog_fetch_mode || 'raw'; const displayedCoverage = mcp.coverage_status || mcp.previous_coverage_status || 'unknown'; const zeroRawWindow = mcp.status === 'refreshing' ? 'previous completed poll' : 'this poll'; + const nowSeconds = Math.floor(Date.now() / 1000); + const pollStartedAt = Number(mcp.poll_started_at || 0); + const pollCompletedAt = Number(mcp.poll_completed_at || mcp.previous_poll_completed_at || 0); + const pollRunningSeconds = pollStartedAt ? Math.max(0, nowSeconds - pollStartedAt) : 0; + const pollCompletedAge = pollCompletedAt ? Math.max(0, nowSeconds - pollCompletedAt) : 0; const rawCorrelations = data.cross_source_correlations || []; const correlationsCached = rawCorrelations.length === 0 && uiCache.correlations.length > 0; const correlations = rawCorrelations.length ? rawCorrelations : uiCache.correlations; @@ -573,6 +578,9 @@ async function refresh() { `Discovery fields recorded: ${esc((data.baseline || {}).discovery_fields_recorded || 0)}`, `Discovery cache events: ${esc((data.baseline || {}).discovery_cache_events || 0)}`, `MCP status: ${esc(mcp.status || 'unknown')}`, + mcp.status === 'refreshing' && pollStartedAt ? `MCP poll running: ${esc(pollRunningSeconds)}s` : '', + pollCompletedAt ? `Last completed MCP poll: ${esc(pollCompletedAge)}s ago` : '', + mcp.status === 'refreshing' && pollRunningSeconds > Math.max(120, Number(configuration.graylog_range_seconds || 300)) ? `MCP poll has been refreshing for ${esc(pollRunningSeconds)}s. It may be slow or stuck.` : '', mcp.error ? `MCP error: ${esc(mcp.error)}` : '', `Enabled streams: ${esc(enabledStreams.length)}`, `MCP fetch mode: ${esc(displayedFetchMode)}${mcp.status === 'refreshing' ? ' (refreshing, showing previous counters)' : ''}`, diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index 050ca07..4a71091 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -215,6 +215,7 @@ def build_status( "events_fetched": len(events), "raw_events_fetched": len(events), "aggregate_events": aggregate_events_total, + "poll_completed_at": int(time.time()), "fetch_mode": "aggregate" if use_aggregate else "raw", "range_seconds": range_seconds, "max_events_per_stream": max_events_per_stream, @@ -482,6 +483,7 @@ def write_status(status: dict[str, object], output: str) -> None: def write_refreshing_status(output: str, *, cache_path: str | None = None) -> None: output_path = Path(output) + now = int(time.time()) try: current = json.loads(output_path.read_text(encoding="utf-8")) if output_path.exists() else {} except (json.JSONDecodeError, OSError): @@ -495,8 +497,9 @@ def write_refreshing_status(output: str, *, cache_path: str | None = None) -> No current = cached used_cache = True previous_mcp = current.get("capabilities", {}).get("graylog_mcp", {}) if isinstance(current.get("capabilities"), dict) else {} + previous_completed_at = previous_mcp.get("poll_completed_at", current.get("generated_at", 0)) if isinstance(previous_mcp, dict) else current.get("generated_at", 0) current.setdefault("status_schema", 2) - current["generated_at"] = int(time.time()) + current["generated_at"] = now current["stale"] = False current["stale_reason"] = "" capabilities = current.setdefault("capabilities", {}) @@ -518,6 +521,8 @@ def write_refreshing_status(output: str, *, cache_path: str | None = None) -> No "events_fetched": previous_events, "raw_events_fetched": previous_raw_events, "aggregate_events": previous_aggregate_events, + "poll_started_at": now, + "previous_poll_completed_at": previous_completed_at, "fetch_mode": previous_fetch_mode, "coverage_status": previous_coverage_status or "refreshing", } diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 8632cf8..45d19de 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -80,7 +80,7 @@ class MonitorTests(unittest.TestCase): "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"}}, + "capabilities": {"graylog_mcp": {"status": "connected", "events_fetched": 99, "raw_events_fetched": 88, "aggregate_events": 1234, "poll_completed_at": 123456, "fetch_mode": "aggregate", "coverage_status": "complete_window"}}, "cross_source_correlations": [{"entity": "10.0.0.1", "entity_label": "host01 (10.0.0.1)"}], "llm_assessment": {"enabled": True, "status": "cached", "text": "previous assessment"}, }, @@ -96,6 +96,8 @@ class MonitorTests(unittest.TestCase): self.assertEqual(mcp["raw_events_fetched"], 88) self.assertEqual(mcp["aggregate_events"], 1234) self.assertEqual(mcp["previous_status"], "connected") + self.assertGreater(mcp["poll_started_at"], 123456) + self.assertEqual(mcp["previous_poll_completed_at"], 123456) self.assertTrue(status["llm_assessment"]["enabled"]) self.assertTrue(status["status_cache"]["served_from_cache"])