diff --git a/test/test_web_settings_ui.py b/test/test_web_settings_ui.py index 9ae4c1cb..89abb778 100644 --- a/test/test_web_settings_ui.py +++ b/test/test_web_settings_ui.py @@ -129,7 +129,7 @@ def test_search_index_endpoint(client): by_id = {f["anchorId"]: f for f in fields} # Representative fields across tabs must be present with usable text. for anchor in ("setting-general-timezone", "setting-display-brightness", - "setting-wifi-password"): + "setting-wifi-password", "setting-durations-clock"): assert anchor in by_id, f"{anchor} missing from search index" entry = by_id[anchor] assert entry["label"], f"{anchor} has no label" diff --git a/web_interface/blueprints/pages_v3.py b/web_interface/blueprints/pages_v3.py index 1966f7ea..8c3c1bbc 100644 --- a/web_interface/blueprints/pages_v3.py +++ b/web_interface/blueprints/pages_v3.py @@ -232,6 +232,7 @@ def settings_search_index(): core_tabs = [ ('general', 'General', _load_general_partial), ('display', 'Display', _load_display_partial), + ('durations', 'Durations', _load_durations_partial), ('schedule', 'Schedule', _load_schedule_partial), ('wifi', 'WiFi', _load_wifi_partial), ] diff --git a/web_interface/static/v3/app.css b/web_interface/static/v3/app.css index 66870db4..a1342955 100644 --- a/web_interface/static/v3/app.css +++ b/web_interface/static/v3/app.css @@ -838,6 +838,8 @@ button.bg-white { box-shadow: var(--shadow-lg); z-index: 50; padding: 0.25rem; + max-height: min(60vh, 24rem); + overflow-y: auto; } .ssr-group { diff --git a/web_interface/static/v3/js/settings-search.js b/web_interface/static/v3/js/settings-search.js index 61585076..397902ff 100644 --- a/web_interface/static/v3/js/settings-search.js +++ b/web_interface/static/v3/js/settings-search.js @@ -6,15 +6,16 @@ * .help-tip[data-tooltip]), so it can never drift from what is rendered: * * 1. Global search (header box): finds settings across ALL tabs, even ones - * not yet opened, by fetching each tab's partial once and scanning it. + * not yet opened, by fetching a single server-side JSON index + * (/v3/settings/search-index) built from all rendered partials. * Clicking a result switches to the tab, waits for the field to load, * then scrolls to and flashes it. * 2. Per-tab filter (the .settings-filter box under a partial title): * hides non-matching fields on the current tab. Delegated, so it keeps * working across HTMX swaps. * - * No backend endpoint is required — plugins are enumerated from - * window.installedPlugins and their config partials are fetched the same way. + * The server owns index generation (including plugin enumeration) and caches + * it per installed-plugin set, so the client makes exactly one JSON request. */ (function () { 'use strict'; @@ -64,8 +65,10 @@ return fields; }) .catch(function () { - window._settingsIndex = []; - return window._settingsIndex; + // Don't cache the failure: clear the in-flight promise so a + // later call can retry after a transient fetch error. + buildPromise = null; + return []; }); return buildPromise; } @@ -309,10 +312,12 @@ // --- Per-tab filter (delegated) ------------------------------------------- function filterScope(input) { + // Return the nearest tab/content container, or null — never `document`, + // which would let the filter hide setting fields across unrelated tabs. return input.closest('.plugin-config-tab') || input.closest('[id$="-content"]') || input.closest('.bg-white') || - document; + null; } function fieldHay(fg) { @@ -364,7 +369,8 @@ document.addEventListener('input', function (e) { var box = e.target.closest ? e.target.closest('.settings-filter') : null; if (!box) return; - applyTabFilter(filterScope(box), box.value); + var scope = filterScope(box); + if (scope) applyTabFilter(scope, box.value); }); // --- Boot -----------------------------------------------------------------