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)