diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index 6a2188e..93fa091 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -226,6 +226,21 @@ function table(rows, columns, id = '') {
return `
`;
}
function capability(label, state, detail) { return `${esc(label)}: ${esc(detail)}`; }
+function mcpCapability(mcp, configuration, pollRunningSeconds=0) {
+ if (configuration.log_source !== 'graylog_mcp') return {state: 'warn', detail: 'not selected'};
+ const status = String(mcp.status || 'checking');
+ if (status === 'connected') return {state: 'on', detail: 'connected'};
+ if (status === 'partial') return {state: 'warn', detail: 'partial'};
+ if (status === 'refreshing') {
+ const previous = String(mcp.previous_status || '');
+ if (['connected', 'partial'].includes(previous) || Number(mcp.previous_events_fetched || mcp.events_fetched || 0) > 0 || Number(mcp.previous_aggregate_events || mcp.aggregate_events || 0) > 0) {
+ const suffix = pollRunningSeconds ? `polling ${pollRunningSeconds}s` : 'polling';
+ return {state: 'on', detail: previous === 'partial' ? `partial, ${suffix}` : `connected, ${suffix}`};
+ }
+ return {state: 'warn', detail: 'starting poll'};
+ }
+ return {state: 'warn', detail: status};
+}
function actionItem(severity, title, detail, action) {
return `${esc(title)}${esc(detail)}
Next: ${esc(action)}
`;
}
@@ -560,11 +575,12 @@ async function refresh() {
].join('');
const llm = data.llm_assessment || {};
const llmEnabled = Boolean(llm.enabled || configuration.llm_enabled);
+ const mcpBadge = mcpCapability(mcp, configuration, pollRunningSeconds);
document.getElementById('capabilities').innerHTML = [
capability('Baseline', baseline.enabled ? 'on' : 'warn', baseline.enabled ? `${baseline.sources_ready || 0} sources ready` : 'disabled'),
capability('Ollama', llmEnabled && llm.status !== 'error' ? 'on' : 'warn', llmEnabled ? (llm.status || 'starting') : 'disabled'),
capability('Threat Intel', threat.enabled && threat.configured ? 'on' : 'warn', threat.enabled ? `${threat.provider || 'unknown'}${threat.configured ? '' : ', key missing'}` : 'disabled'),
- capability('Graylog MCP', mcp.status === 'connected' ? 'on' : 'warn', configuration.log_source === 'graylog_mcp' ? (mcp.status || 'checking') : 'not selected')
+ capability('Graylog MCP', mcpBadge.state, mcpBadge.detail)
].join('');
document.getElementById('liveStatus').innerHTML = [
data.stale ? `Showing cached dashboard data because live MCP fetch failed.` : '',
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 2241386..2ee06d2 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -49,6 +49,10 @@ class DashboardTests(unittest.TestCase):
self.assertIn("function renderCorrelationExplorer", HTML)
self.assertIn("correlationHitboxes", HTML)
+ def test_dashboard_shows_refreshing_mcp_as_polling_badge(self):
+ self.assertIn("function mcpCapability", HTML)
+ self.assertIn("connected, ${suffix}", HTML)
+
if __name__ == "__main__":
unittest.main()