diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index 4c9d511..a3d2bb8 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -245,6 +245,16 @@ function mcpCapability(mcp, configuration, pollRunningSeconds=0) {
}
return {state: 'warn', detail: status};
}
+function llmCapability(llm, configuration) {
+ const enabled = Boolean(llm.enabled || configuration.llm_enabled);
+ if (!enabled) return {state: 'warn', detail: 'disabled'};
+ const status = String(llm.status || 'starting');
+ if (status === 'ok') return {state: 'on', detail: 'ok'};
+ if (status === 'cached') return {state: 'on', detail: 'cached'};
+ if (status === 'error' && String(llm.text || '').trim()) return {state: 'warn', detail: 'cached, assessment error'};
+ if (status === 'error') return {state: 'warn', detail: 'assessment error'};
+ return {state: 'warn', detail: status};
+}
function actionItem(severity, title, detail, action) {
return `
${esc(title)}${esc(detail)}
Next: ${esc(action)}
`;
}
@@ -415,9 +425,10 @@ function sharedFieldLabel(item) {
}
function profileAdvisorLabel(row, advisor) {
const rowAdvisor = row.profile_advisor || {};
- if (rowAdvisor.status === 'heuristic' && rowAdvisor.error) return 'heuristic (advisor error)';
+ if (rowAdvisor.status === 'heuristic' && rowAdvisor.error) return 'heuristic fallback';
if (rowAdvisor.status) return rowAdvisor.status;
- if (advisor.enabled && advisor.status === 'error') return 'heuristic (advisor error)';
+ if (advisor.enabled && advisor.status === 'heuristic_fallback') return 'heuristic fallback';
+ if (advisor.enabled && advisor.status === 'error') return 'advisor error';
return advisor.enabled ? (advisor.status || 'checking') : 'heuristic';
}
function drawTrend(history) {
@@ -590,9 +601,10 @@ async function refresh() {
const llm = data.llm_assessment || {};
const llmEnabled = Boolean(llm.enabled || configuration.llm_enabled);
const mcpBadge = mcpCapability(mcp, configuration, pollRunningSeconds);
+ const llmBadge = llmCapability(llm, configuration);
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('Ollama', llmBadge.state, llmBadge.detail),
capability('Threat Intel', threat.enabled && threat.configured ? 'on' : 'warn', threat.enabled ? `${threat.provider || 'unknown'}${threat.configured ? '' : ', key missing'}` : 'disabled'),
capability('Graylog MCP', mcpBadge.state, mcpBadge.detail)
].join('');
@@ -607,6 +619,7 @@ async function refresh() {
`Baseline DB size: ${esc(bytes((data.baseline || {}).size_bytes || 0))}`,
`Discovery fields recorded: ${esc((data.baseline || {}).discovery_fields_recorded || 0)}`,
`Discovery cache events: ${esc((data.baseline || {}).discovery_cache_events || 0)}`,
+ llm.status === 'error' ? `Ollama assessment error: ${esc(llm.error || 'unknown error')}` : '',
`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` : '',
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index b78c470..2070879 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -69,6 +69,12 @@ class DashboardTests(unittest.TestCase):
def test_dashboard_shows_profile_advisor_fallback_as_notice(self):
self.assertIn("heuristic_fallback", HTML)
self.assertIn("Profile advisor: heuristic fallback", HTML)
+ self.assertIn("heuristic fallback", HTML)
+
+ def test_dashboard_labels_ollama_assessment_errors(self):
+ self.assertIn("function llmCapability", HTML)
+ self.assertIn("assessment error", HTML)
+ self.assertIn("Ollama assessment error", HTML)
if __name__ == "__main__":