Address CodeRabbit review on settings search/tooltips

- pages_v3: include Durations tab in the search index so
  setting-durations-* fields are actually indexed
- test_web_settings_ui: assert setting-durations-clock is present in
  the search-index endpoint response
- settings-search.js: on index fetch failure, reset buildPromise
  instead of caching an empty (truthy) index so search can retry
- settings-search.js: filterScope returns null (not document) when no
  tab container matches, and the caller guards, so the per-tab filter
  can't hide fields across unrelated tabs
- settings-search.js: refresh the stale header comment to describe the
  server-side JSON index flow
- app.css: cap #settings-search-results height with overflow-y so the
  dropdown scrolls instead of overflowing small screens

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz
This commit is contained in:
Claude
2026-07-08 17:20:03 +00:00
parent a2fb3d180f
commit 497a103d8c
4 changed files with 17 additions and 8 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ def test_search_index_endpoint(client):
by_id = {f["anchorId"]: f for f in fields} by_id = {f["anchorId"]: f for f in fields}
# Representative fields across tabs must be present with usable text. # Representative fields across tabs must be present with usable text.
for anchor in ("setting-general-timezone", "setting-display-brightness", 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" assert anchor in by_id, f"{anchor} missing from search index"
entry = by_id[anchor] entry = by_id[anchor]
assert entry["label"], f"{anchor} has no label" assert entry["label"], f"{anchor} has no label"
+1
View File
@@ -232,6 +232,7 @@ def settings_search_index():
core_tabs = [ core_tabs = [
('general', 'General', _load_general_partial), ('general', 'General', _load_general_partial),
('display', 'Display', _load_display_partial), ('display', 'Display', _load_display_partial),
('durations', 'Durations', _load_durations_partial),
('schedule', 'Schedule', _load_schedule_partial), ('schedule', 'Schedule', _load_schedule_partial),
('wifi', 'WiFi', _load_wifi_partial), ('wifi', 'WiFi', _load_wifi_partial),
] ]
+2
View File
@@ -838,6 +838,8 @@ button.bg-white {
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
z-index: 50; z-index: 50;
padding: 0.25rem; padding: 0.25rem;
max-height: min(60vh, 24rem);
overflow-y: auto;
} }
.ssr-group { .ssr-group {
+13 -7
View File
@@ -6,15 +6,16 @@
* .help-tip[data-tooltip]), so it can never drift from what is rendered: * .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 * 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, * Clicking a result switches to the tab, waits for the field to load,
* then scrolls to and flashes it. * then scrolls to and flashes it.
* 2. Per-tab filter (the .settings-filter box under a partial title): * 2. Per-tab filter (the .settings-filter box under a partial title):
* hides non-matching fields on the current tab. Delegated, so it keeps * hides non-matching fields on the current tab. Delegated, so it keeps
* working across HTMX swaps. * working across HTMX swaps.
* *
* No backend endpoint is required plugins are enumerated from * The server owns index generation (including plugin enumeration) and caches
* window.installedPlugins and their config partials are fetched the same way. * it per installed-plugin set, so the client makes exactly one JSON request.
*/ */
(function () { (function () {
'use strict'; 'use strict';
@@ -64,8 +65,10 @@
return fields; return fields;
}) })
.catch(function () { .catch(function () {
window._settingsIndex = []; // Don't cache the failure: clear the in-flight promise so a
return window._settingsIndex; // later call can retry after a transient fetch error.
buildPromise = null;
return [];
}); });
return buildPromise; return buildPromise;
} }
@@ -309,10 +312,12 @@
// --- Per-tab filter (delegated) ------------------------------------------- // --- Per-tab filter (delegated) -------------------------------------------
function filterScope(input) { 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') || return input.closest('.plugin-config-tab') ||
input.closest('[id$="-content"]') || input.closest('[id$="-content"]') ||
input.closest('.bg-white') || input.closest('.bg-white') ||
document; null;
} }
function fieldHay(fg) { function fieldHay(fg) {
@@ -364,7 +369,8 @@
document.addEventListener('input', function (e) { document.addEventListener('input', function (e) {
var box = e.target.closest ? e.target.closest('.settings-filter') : null; var box = e.target.closest ? e.target.closest('.settings-filter') : null;
if (!box) return; if (!box) return;
applyTabFilter(filterScope(box), box.value); var scope = filterScope(box);
if (scope) applyTabFilter(scope, box.value);
}); });
// --- Boot ----------------------------------------------------------------- // --- Boot -----------------------------------------------------------------