diff --git a/templates/index.html b/templates/index.html
index 249f2b4..d3877d0 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -4162,6 +4162,9 @@
// Initialize dropdown nav active state
updateDropdownActiveState();
+ // Restore nav group open/closed state from localStorage
+ initNavGroupState();
+
// Start SDR device status polling
startSdrStatusPolling();
@@ -4189,6 +4192,45 @@
if (!isOpen) {
dropdown.classList.add('open');
}
+ saveNavGroupState();
+ }
+
+ function initNavGroupState() {
+ const NAV_STATE_KEY = 'intercept_nav_groups';
+ let savedState = {};
+ try {
+ savedState = JSON.parse(localStorage.getItem(NAV_STATE_KEY) || '{}');
+ } catch (e) {
+ savedState = {};
+ }
+
+ document.querySelectorAll('.mode-nav-dropdown[data-group]').forEach(dropdown => {
+ const group = dropdown.dataset.group;
+ // If saved state says closed AND this group has no active item, close it
+ if (savedState[group] === false) {
+ const hasActive = dropdown.classList.contains('has-active');
+ if (!hasActive) {
+ dropdown.classList.remove('open');
+ const btn = dropdown.querySelector('.mode-nav-dropdown-btn');
+ if (btn) btn.setAttribute('aria-expanded', 'false');
+ }
+ } else if (savedState[group] === true) {
+ dropdown.classList.add('open');
+ const btn = dropdown.querySelector('.mode-nav-dropdown-btn');
+ if (btn) btn.setAttribute('aria-expanded', 'true');
+ }
+ });
+ }
+
+ function saveNavGroupState() {
+ const NAV_STATE_KEY = 'intercept_nav_groups';
+ const state = {};
+ document.querySelectorAll('.mode-nav-dropdown[data-group]').forEach(dropdown => {
+ state[dropdown.dataset.group] = dropdown.classList.contains('open');
+ });
+ try {
+ localStorage.setItem(NAV_STATE_KEY, JSON.stringify(state));
+ } catch (e) { /* storage full or unavailable */ }
}
function closeAllDropdowns() {
diff --git a/tests/test_nav_state.py b/tests/test_nav_state.py
new file mode 100644
index 0000000..d8a858d
--- /dev/null
+++ b/tests/test_nav_state.py
@@ -0,0 +1,25 @@
+"""Tests for nav group localStorage persistence (JS logic verified via structure check)."""
+
+
+def _logged_in_get(client, path):
+ """Make a GET request with a pre-seeded logged-in session."""
+ with client.session_transaction() as sess:
+ sess["logged_in"] = True
+ return client.get(path)
+
+
+def test_index_page_includes_nav_state_init(client):
+ """nav group init function must be present in the index page."""
+ resp = _logged_in_get(client, "/")
+ assert resp.status_code == 200
+ html = resp.data.decode()
+ assert "initNavGroupState" in html
+ assert "localStorage" in html
+
+
+def test_nav_groups_have_data_group_attributes(client):
+ """Each nav group must have a data-group attribute for state keying."""
+ resp = _logged_in_get(client, "/")
+ html = resp.data.decode()
+ for group in ["signals", "tracking", "space", "wireless", "intel"]:
+ assert f'data-group="{group}"' in html, f"Missing data-group={group}"