diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index 4f16068..ea67d43 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -215,6 +215,8 @@ const tableSort = {};
const uiCache = {correlations: [], fieldRows: [], correlationHitboxes: [], selectedCorrelationKey: '', entityLabels: {}};
window.availableStreams = [];
window.streamSelection = {};
+window.configuredStreams = [];
+window.streamProfiles = [];
window.currentCorrelations = [];
window.currentConfiguration = {};
function table(rows, columns, id = '') {
@@ -815,6 +817,7 @@ async function loadSettings() {
const vtKey = form.elements.namedItem('virustotal_api_key');
vtKey.placeholder = config.virustotal_api_key_configured ? 'Key configured; leave blank to keep it' : 'Paste VirusTotal API key';
window.streamProfiles = config.graylog_stream_profiles || [];
+ window.configuredStreams = config.graylog_streams || [];
if (config.graylog_mcp_token_configured && config.graylog_mcp_url) loadStreams();
loadOllamaModels();
}
@@ -842,10 +845,16 @@ async function loadStreams() {
renderStreamPicker(payload.error || `Could not load streams: ${response.status}`);
return;
}
- const selected = new Set((payload.selected || []).filter(item => item.enabled).map(item => item.id));
+ const configuredStreams = payload.selected || window.configuredStreams || [];
+ let selected = new Set(configuredStreams.filter(item => item.enabled).map(item => item.id));
+ let recoveryText = '';
+ if (!selected.size && (window.streamProfiles || []).length) {
+ selected = new Set((window.streamProfiles || []).map(item => item.stream_id).filter(Boolean));
+ recoveryText = 'No enabled streams were saved; streams with existing profiles are selected for recovery. Click Save streams to keep this selection.';
+ }
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 || '');
+ renderStreamPicker(recoveryText || payload.error || '');
}
document.getElementById('loadStreams').addEventListener('click', loadStreams);
async function saveStreamSelection() {
@@ -879,6 +888,7 @@ function renderStreamPicker(errorText='') {
.filter(stream => !previousEnabledOnly || window.streamSelection[stream.id]?.enabled);
const enabledCount = Object.values(window.streamSelection).filter(item => item.enabled).length;
target.innerHTML = `
+ ${errorText ? `
${esc(errorText)}
` : ''}
@@ -1027,7 +1037,10 @@ document.getElementById('settingsForm').addEventListener('submit', async event =
values.profile_advisor_enabled = form.elements.profile_advisor_enabled.checked;
values.threat_intel_enabled = form.elements.threat_intel_enabled.checked;
values.graylog_tls_verify = form.elements.graylog_tls_verify.checked;
- values.graylog_streams = Object.values(window.streamSelection || {}).length ? Object.values(window.streamSelection) : [...document.querySelectorAll('.graylog-stream')].map(item => ({id:item.dataset.id, title:item.dataset.title, enabled:item.checked}));
+ const streamValues = Object.values(window.streamSelection || {});
+ if (streamValues.length) {
+ values.graylog_streams = streamValues;
+ }
if (window.activeProfileStream) { let detectors={},fieldWeights={},relationshipFields=[]; try { detectors=form.elements.profile_detectors.value.trim() ? JSON.parse(form.elements.profile_detectors.value) : {}; } catch { document.getElementById('settingsResult').textContent='Detector thresholds must be valid JSON.'; return; } try { fieldWeights=form.elements.profile_field_weights.value.trim() ? JSON.parse(form.elements.profile_field_weights.value) : {}; } catch { document.getElementById('settingsResult').textContent='Field weights must be valid JSON.'; return; } try { relationshipFields=form.elements.profile_relationship_fields.value.trim() ? JSON.parse(form.elements.profile_relationship_fields.value) : []; } catch { document.getElementById('settingsResult').textContent='Behavior relationships must be valid JSON.'; return; } if (!Array.isArray(relationshipFields)) { document.getElementById('settingsResult').textContent='Behavior relationships must be a JSON list.'; return; } const entityFields=[...form.querySelectorAll('.profile-entity:checked')].map(item=>item.value); if (!entityFields.length) { document.getElementById('settingsResult').textContent='Select at least one Entity field for the active profile.'; return; } const profileTitle=window.activeProfileTitle || window.activeProfileStream; const profile={stream_id:window.activeProfileStream,name:form.elements.profile_name.value.trim() || `${profileTitle} profile`,entity_field:entityFields[0]||'',entity_fields:entityFields,timestamp_field:form.querySelector('[name="profile_timestamp"]:checked')?.value||'timestamp',categorical_fields:[...form.querySelectorAll('.profile-categorical:checked')].map(item=>item.value),numeric_fields:[...form.querySelectorAll('.profile-numeric:checked')].map(item=>item.value),detectors,field_weights:fieldWeights,relationship_fields:relationshipFields}; values.graylog_stream_profiles=[...(window.streamProfiles||[]).filter(item=>item.stream_id!==profile.stream_id),profile]; }
const savedProfile = window.activeProfileStream ? ` Profile saved for ${window.activeProfileTitle || window.activeProfileStream}.` : ' No profile editor active, so profiles were not changed.';
const response = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(values)});
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 38ceeee..674815e 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -57,6 +57,15 @@ class DashboardTests(unittest.TestCase):
self.assertIn("incidentNotice", HTML)
self.assertIn("Incident marked", HTML)
+ def test_dashboard_does_not_clear_streams_when_picker_is_unloaded(self):
+ self.assertIn("const streamValues = Object.values(window.streamSelection || {})", HTML)
+ self.assertIn("if (streamValues.length)", HTML)
+ self.assertIn("values.graylog_streams = streamValues", HTML)
+
+ def test_dashboard_can_recover_stream_selection_from_profiles(self):
+ self.assertIn("No enabled streams were saved", HTML)
+ self.assertIn("streams with existing profiles are selected for recovery", HTML)
+
if __name__ == "__main__":
unittest.main()