From a8c00c35b25bafcf79c3a8412297f5b0d84ea596f126df56cf7babf1cb7a1157 Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 2 Jul 2026 10:51:56 +0200 Subject: [PATCH] add test to see if fail for streams and evetns --- src/fgai/dashboard.py | 24 ++++++++++++++++++++++++ src/fgai/monitor.py | 10 +++++++++- tests/test_dashboard.py | 4 ++++ tests/test_monitor.py | 20 ++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 35e5393..b21e073 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -534,12 +534,33 @@ async function loadOllamaModels() { async function loadStreams() { const response = await fetch('/api/graylog/streams'); const payload = await response.json(); + if (!response.ok) { + renderStreamPicker(payload.error || `Could not load streams: ${response.status}`); + return; + } const selected = new Set((payload.selected || []).filter(item => item.enabled).map(item => item.id)); window.availableStreams = payload.streams || []; window.streamSelection = Object.fromEntries(window.availableStreams.map(stream => [stream.id, {id:stream.id, title:stream.title, enabled:selected.has(stream.id)}])); renderStreamPicker(payload.error || ''); } document.getElementById('loadStreams').addEventListener('click', loadStreams); +async function saveStreamSelection() { + const result = document.getElementById('settingsResult'); + const streams = Object.values(window.streamSelection || {}); + if (!streams.length) { + result.textContent = 'Load streams before saving stream selection.'; + return false; + } + const response = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({graylog_streams: streams})}); + let payload = {}; + try { payload = await response.json(); } catch {} + if (!response.ok) { + result.textContent = `Could not save enabled streams: ${payload.error || response.statusText || response.status}`; + return false; + } + result.textContent = `Saved ${streams.filter(item => item.enabled).length} enabled stream(s). Monitor will use them on the next cycle.`; + return true; +} function renderStreamPicker(errorText='') { const target = document.getElementById('streamPicker'); if (!window.availableStreams.length) { @@ -559,6 +580,7 @@ function renderStreamPicker(errorText='') { +
${esc(enabledCount)} enabled, ${esc(streams.length)} visible, ${esc(window.availableStreams.length)} total
${streams.map(stream => { @@ -569,6 +591,7 @@ function renderStreamPicker(errorText='') { document.getElementById('streamEnabledOnly').addEventListener('change', () => renderStreamPicker()); document.getElementById('enableVisibleStreams').addEventListener('click', () => { streams.forEach(stream => { window.streamSelection[stream.id].enabled = true; }); renderStreamPicker(); }); document.getElementById('disableVisibleStreams').addEventListener('click', () => { streams.forEach(stream => { window.streamSelection[stream.id].enabled = false; }); renderStreamPicker(); }); + document.getElementById('saveVisibleStreams').addEventListener('click', saveStreamSelection); } function mergeProfile(existing, recommended) { const mergeList = (left, right) => [...new Set([...(left || []), ...(right || [])].filter(Boolean))]; @@ -670,6 +693,7 @@ document.getElementById('streamPicker').addEventListener('change', event => { if (!checkbox || !window.streamSelection[checkbox.dataset.id]) return; window.streamSelection[checkbox.dataset.id].enabled = checkbox.checked; document.querySelector('.stream-counts').textContent = `${Object.values(window.streamSelection).filter(item => item.enabled).length} enabled, ${document.querySelectorAll('.stream-row').length} visible, ${window.availableStreams.length} total`; + document.getElementById('settingsResult').textContent = 'Stream selection changed. Click Save streams or Save configuration.'; }); document.getElementById('loadFields').addEventListener('click', async () => { if (window.activeProfileStream) { editStreamProfile(window.activeProfileStream, window.activeProfileTitle); return; } diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py index 8120e75..1065f6b 100644 --- a/src/fgai/monitor.py +++ b/src/fgai/monitor.py @@ -135,7 +135,13 @@ def build_status( stream_configs = [item for item in configured_streams if isinstance(item, dict) and item.get("enabled") and item.get("id")] stream_ids = [str(item.get("id")) for item in stream_configs] if not stream_ids: - stream_configs = [{"id": str(runtime_values.get("graylog_stream", "")), "title": "Graylog"}] + legacy_stream = str(runtime_values.get("graylog_stream", "") or "") + if legacy_stream: + stream_configs = [{"id": legacy_stream, "title": "Graylog"}] + else: + mcp_status = {"status": "no_streams_enabled", "streams": [], "events_fetched": 0, "coverage_status": "no_streams_enabled"} + events = [] + raise StopIteration stream_statuses = [] events = [] range_seconds = _range_seconds(runtime_values.get("graylog_range_seconds", 300)) @@ -193,6 +199,8 @@ def build_status( "coverage_status": "partial" if partial_streams else "truncated" if truncated_streams else "complete_window", "coverage_warning": " ".join(warnings), } + except StopIteration: + pass except RuntimeError as exc: mcp_status = {"status": "error", "error": str(exc)} events = [] diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 1eebb53..d7a2ad6 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -32,6 +32,10 @@ class DashboardTests(unittest.TestCase): self.assertIn("Update profile", HTML) self.assertIn("function mergeProfile", HTML) + def test_dashboard_has_direct_stream_selection_save(self): + self.assertIn("Save streams", HTML) + self.assertIn("function saveStreamSelection", HTML) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_monitor.py b/tests/test_monitor.py index c45580d..5adb97c 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -106,6 +106,26 @@ class MonitorTests(unittest.TestCase): self.assertEqual(coverage["firewall"]["readiness"], "1/1") self.assertEqual(coverage["dns"]["health"], "missing_profile") + def test_graylog_mcp_without_enabled_streams_reports_clear_status(self): + with tempfile.TemporaryDirectory() as tmp: + config_path = Path(tmp) / "config.json" + config_path.write_text( + json.dumps( + { + "log_source": "graylog_mcp", + "graylog_mcp_url": "https://graylog.example/api/mcp", + "graylog_mcp_token": "token", + "graylog_streams": [{"id": "disabled", "title": "Disabled", "enabled": False}], + } + ), + encoding="utf-8", + ) + + status = build_status(str(Path(tmp) / "missing.log"), config_path=str(config_path), incident_path=str(Path(tmp) / "incidents.json")) + + self.assertEqual(status["capabilities"]["graylog_mcp"]["status"], "no_streams_enabled") + self.assertEqual(status["summary"]["total"], 0) + def test_add_llm_assessment_records_error_without_ollama(self): status = {"summary": {}, "anomalies": []}