diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index e73e341..4c9d511 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -628,6 +628,7 @@ async function refresh() {
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 && 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 === 'heuristic_fallback' ? `Profile advisor: heuristic fallback (${esc(advisor.error || 'advisor unavailable')})` : '',
advisor.status === 'error' ? `Profile advisor error: ${esc(advisor.error || 'unknown error')}` : ''
].filter(Boolean).join('
');
document.getElementById('health').innerHTML = [
diff --git a/src/fgai/monitor.py b/src/fgai/monitor.py
index fb0ef55..420a9fe 100644
--- a/src/fgai/monitor.py
+++ b/src/fgai/monitor.py
@@ -432,7 +432,13 @@ def build_status(
profile_suggestions = apply_profile_advice(profile_suggestions, advice)
profile_advisor_status = {"enabled": True, "status": "ok" if advice else "empty", "profiles_returned": len(advice), "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")}
except Exception as exc:
- profile_advisor_status = {"enabled": True, "status": "error", "error": str(exc), "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b")}
+ profile_advisor_status = {
+ "enabled": True,
+ "status": "heuristic_fallback",
+ "error": str(exc),
+ "model": str(runtime_values.get("profile_advisor_model", "") or "qwen3:8b"),
+ "candidates": len(advisor_candidates),
+ }
for suggestion in profile_suggestions:
suggestion.setdefault("profile_advisor", {"status": "heuristic", "error": str(exc)})
intel_ips = sorted(
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 674815e..b78c470 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -66,6 +66,10 @@ class DashboardTests(unittest.TestCase):
self.assertIn("No enabled streams were saved", HTML)
self.assertIn("streams with existing profiles are selected for recovery", HTML)
+ def test_dashboard_shows_profile_advisor_fallback_as_notice(self):
+ self.assertIn("heuristic_fallback", HTML)
+ self.assertIn("Profile advisor: heuristic fallback", HTML)
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_monitor.py b/tests/test_monitor.py
index f32cb49..7543726 100644
--- a/tests/test_monitor.py
+++ b/tests/test_monitor.py
@@ -196,7 +196,7 @@ class MonitorTests(unittest.TestCase):
self.assertEqual(status["llm_assessment"]["status"], "ok")
self.assertEqual(status["llm_assessment"]["text"], "looks noisy")
- def test_profile_advisor_status_records_error(self):
+ def test_profile_advisor_status_records_heuristic_fallback(self):
with tempfile.TemporaryDirectory() as tmp:
log_path = Path(tmp) / "events.log"
log_path.write_text("fgai_stream_id=windows fgai_stream=Windows username=alice eventid=4625 action=failure\n", encoding="utf-8")
@@ -207,8 +207,9 @@ class MonitorTests(unittest.TestCase):
status = build_status(str(log_path), config_path=str(config_path), incident_path=str(Path(tmp) / "incidents.json"))
advisor = status["capabilities"]["profile_advisor"]
- self.assertEqual(advisor["status"], "error")
+ self.assertEqual(advisor["status"], "heuristic_fallback")
self.assertIn("timeout", advisor["error"])
+ self.assertEqual(advisor["candidates"], 1)
self.assertEqual(status["profile_suggestions"][0]["profile_advisor"]["status"], "heuristic")
self.assertIn("timeout", status["profile_suggestions"][0]["profile_advisor"]["error"])