diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py
index bc50206..2cbdda9 100644
--- a/src/fgai/dashboard.py
+++ b/src/fgai/dashboard.py
@@ -669,7 +669,7 @@ async function refresh() {
{label:'Common denominators', render:r => esc((r.common_fields || []).slice(0,5).map(item => `${item.field} ${(item.coverage*100).toFixed(0)}%`).join(', ') || '-')},
{label:'Shared fields', render:r => esc((r.shared_fields || []).slice(0,4).map(sharedFieldLabel).join(', ') || '-')},
{label:'Discovery', render:r => profileDiscoveryDetails(r)},
- {label:'Action', render:r => ``}
+ {label:'Action', render:r => `${r.profile_exists ? ` ` : ''}`}
], 'profile-suggestions') : '
No missing stream profiles. Enable "show existing profiles" to inspect already configured profiles.
';
const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '
') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');
document.getElementById('llmAssessment').innerHTML = `Status: ${esc(llm.status || (llmEnabled ? 'starting' : 'disabled'))}
${llmText}
`;
@@ -843,7 +843,6 @@ async function refresh() {
'Local-in Failures
' + table(d.local_in_failures || [], [{label:'Source', key:'src_ip', render:r => entityCell(r, 'src_ip')}, {label:'Service', key:'service'}, {label:'Policy', key:'policy'}, {label:'Count', key:'count'}]);
document.querySelectorAll('details[data-detail-id]').forEach(item => { if (openDetails.has(item.dataset.detailId)) item.open = true; });
document.querySelectorAll('[data-sort-table]').forEach(button => button.addEventListener('click', () => { const current=tableSort[button.dataset.sortTable]; tableSort[button.dataset.sortTable]={key:button.dataset.sortKey,direction:current && current.key===button.dataset.sortKey ? -current.direction : 1}; refresh(); }));
- document.querySelectorAll('.apply-suggested-profile').forEach(button => button.addEventListener('click', () => applySuggestedProfile(button.dataset.streamId)));
} catch (error) {
showRefreshError(error);
}
@@ -945,7 +944,8 @@ function renderStreamPicker(errorText='') {
${esc(enabledCount)} enabled, ${esc(streams.length)} visible, ${esc(window.availableStreams.length)} total
`;
document.getElementById('streamFilter').addEventListener('input', () => renderStreamPicker());
document.getElementById('streamEnabledOnly').addEventListener('change', () => renderStreamPicker());
@@ -1043,6 +1043,32 @@ async function applySuggestedProfile(streamId) {
if (button) button.disabled = false;
}
}
+async function deleteStreamProfile(streamId, streamName='') {
+ const notice = document.getElementById('profileApplyResult') || document.getElementById('settingsResult');
+ const title = streamName || streamId;
+ if (!confirm(`Delete profile for ${title}? Baseline data is not deleted, but this stream will stop using the profile until you apply or create a new one.`)) return;
+ try {
+ if (notice) notice.textContent = `Deleting profile for ${title}...`;
+ const configResponse = await fetch('/api/config', {cache: 'no-store'});
+ const config = await configResponse.json();
+ const profiles = (config.graylog_stream_profiles || []).filter(item => String(item.stream_id) !== String(streamId));
+ const response = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({graylog_stream_profiles: profiles})});
+ let result = {};
+ try { result = await response.json(); } catch {}
+ if (!response.ok) throw new Error(result.error || response.statusText || response.status);
+ window.streamProfiles = result.graylog_stream_profiles || profiles;
+ if (window.activeProfileStream === streamId) {
+ window.activeProfileStream = '';
+ window.activeProfileTitle = '';
+ document.getElementById('profileEditorStatus').textContent = 'Profile deleted. Select a stream to edit or apply a recommended profile.';
+ document.getElementById('fieldPicker').textContent = 'No profile selected for editing.';
+ }
+ if (notice) notice.textContent = `Deleted profile for ${title}. Monitor will stop using it on the next cycle.`;
+ loadSettings();
+ } catch (error) {
+ if (notice) notice.textContent = `Could not delete profile: ${error}`;
+ }
+}
async function editStreamProfile(streamId, title) {
document.getElementById('profileEditorStatus').innerHTML = `Editing profile for ${esc(title || streamId)}`;
document.getElementById('fieldPicker').textContent = 'Loading stream fields...';
@@ -1059,10 +1085,25 @@ async function editStreamProfile(streamId, title) {
window.activeProfileTitle = title || streamId;
}
document.getElementById('streamPicker').addEventListener('click', event => {
+ const deleteButton = event.target.closest('.delete-stream-profile');
+ if (deleteButton) {
+ deleteStreamProfile(deleteButton.dataset.streamId, deleteButton.dataset.streamName);
+ return;
+ }
const button = event.target.closest('.edit-profile');
if (!button) return;
editStreamProfile(button.dataset.id, button.dataset.title);
});
+document.getElementById('profileSuggestions').addEventListener('click', event => {
+ const deleteButton = event.target.closest('.delete-stream-profile');
+ if (deleteButton) {
+ deleteStreamProfile(deleteButton.dataset.streamId, deleteButton.dataset.streamName);
+ return;
+ }
+ const applyButton = event.target.closest('.apply-suggested-profile');
+ if (!applyButton) return;
+ applySuggestedProfile(applyButton.dataset.streamId);
+});
document.getElementById('streamPicker').addEventListener('change', event => {
const checkbox = event.target.closest('.graylog-stream');
if (!checkbox || !window.streamSelection[checkbox.dataset.id]) return;
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 181ab9c..556a9ab 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -34,6 +34,11 @@ class DashboardTests(unittest.TestCase):
self.assertIn("Update profile", HTML)
self.assertIn("function mergeProfile", HTML)
+ def test_dashboard_can_delete_stream_profiles(self):
+ self.assertIn("Delete profile", HTML)
+ self.assertIn("function deleteStreamProfile", HTML)
+ self.assertIn("delete-stream-profile", HTML)
+
def test_dashboard_has_direct_stream_selection_save(self):
self.assertIn("Save streams", HTML)
self.assertIn("function saveStreamSelection", HTML)