From 59ac227dfde045b71532d551975574e93a555416b212630fbef893b9204ff089 Mon Sep 17 00:00:00 2001 From: larssand Date: Tue, 8 Sep 2026 09:32:00 +0200 Subject: [PATCH] fix refresh so checkbox works --- src/fgai/dashboard.py | 14 ++++++++++++-- tests/test_dashboard.py | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 3bbc9d4..98db181 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -220,6 +220,8 @@ let pendingReview = null; let lastReviewAuthor = ''; let selectedDeviationKeys = new Set(); function deviationKey(r) { return `${r.entity}:${r.stream_id}:${r.field}:${r.value || ''}`; } +let openDetailIds = new Set(); +let refreshInFlight = false; window.availableStreams = []; window.streamSelection = {}; window.configuredStreams = []; @@ -560,8 +562,9 @@ function showRefreshError(error) { document.getElementById('liveStatus').innerHTML = `Dashboard render error:
${esc(message)}`; } async function refresh() { + if (refreshInFlight) return; + refreshInFlight = true; try { - const openDetails = new Set([...document.querySelectorAll('details[open][data-detail-id]')].map(item => item.dataset.detailId)); const res = await fetch('/api/status', {cache: 'no-store'}); if (!res.ok) throw new Error(`GET /api/status failed: ${res.status} ${res.statusText}`); const data = await res.json(); @@ -974,10 +977,17 @@ async function refresh() { '

Top Source Ports

' + table(d.top_source_ports || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) + '

Top Services

' + table(d.top_services || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) + '

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('details[data-detail-id]').forEach(item => { + if (openDetailIds.has(item.dataset.detailId)) item.open = true; + item.addEventListener('toggle', () => { + if (item.open) openDetailIds.add(item.dataset.detailId); else openDetailIds.delete(item.dataset.detailId); + }); + }); 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(); })); } catch (error) { showRefreshError(error); + } finally { + refreshInFlight = false; } } async function loadSettings() { diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index dc6f796..f9cf23d 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -86,6 +86,14 @@ class DashboardTests(unittest.TestCase): self.assertIn("/api/feedback/bulk", source) self.assertIn(".add_many(", source) + def test_dashboard_refresh_guards_against_overlapping_calls(self): + self.assertIn("refreshInFlight", HTML) + self.assertIn("if (refreshInFlight) return;", HTML) + + def test_dashboard_open_details_survive_refresh_via_toggle_listener(self): + self.assertIn("openDetailIds", HTML) + self.assertIn("addEventListener('toggle'", HTML) + def test_dashboard_has_bulk_review_bar(self): self.assertIn('id="bulkReviewBar"', HTML) self.assertIn('id="selectAllDeviations"', HTML)