Browse Source

Expand Settings search with module-level registry

  Search field at the top of Settings now finds Sidebar Links,
  Spoolman, Spool/Color Catalog, all four Failure Detection
  sections, Email auth (Advanced + SMTP test), 2FA (TOTP, Email
  OTP, Linked Accounts), SSO/OIDC, LDAP Server Config, and the
  four Backup sub-cards (GitHub, History, Local, Scheduled).

  Replaces the hardcoded searchIndex array in SettingsPage.tsx
  with a module-level registry (frontend/src/lib/settingsSearch.ts).
  Each settings card calls registerSettingsSearch(...) at module
  scope, so adding a new card means adding one colocated line
  instead of editing a distant central array. Anchor ids were
  added to the corresponding Card elements in the affected
  components so scrollIntoView lands on the right section.
maziggy 1 month ago
parent
commit
180db8e8ad

+ 3 - 0
CHANGELOG.md

@@ -12,6 +12,9 @@ All notable changes to Bambuddy will be documented in this file.
   - **Force Refresh** menu entry in the printer card kebab menu (RotateCw icon) that re-requests a full `pushall` MQTT status report from the printer without forcing a reconnect.
 - **AI Print-Failure Detection via self-hosted Obico ML API** ([#172](https://github.com/maziggy/bambuddy/issues/172)) — New Settings → Failure Detection tab wires Bambuddy to a self-hosted [Obico](https://github.com/TheSpaghettiDetective/obico-server) `ml_api` container (no Obico account, no cloud, no WebSocket). While a print is running, the detection service periodically hands the printer's camera snapshot URL to the ML API, which returns YOLO failure-detection scores. Scores are smoothed over time using Obico's own EWM + short/long rolling-mean math (30-frame warmup, alpha = 2/13, short window ≈ 5 min at 10s/frame, long window ≈ 20 h) so a single noisy frame cannot trigger an action. Sensitivity (Low / Medium / High) scales the LOW/HIGH thresholds; when the smoothed score crosses HIGH, the configured action runs exactly once per print: *Notify only*, *Pause print* (MQTT pause command), or *Pause and cut power* (pause + turn off any smart plug linked to that printer). A per-printer toggle lets you monitor all connected printers or just a subset. The Status card shows whether the service is running, the active thresholds, each monitored print's current verdict (safe / warning / failure), and a live rolling detection history. Requires that the External URL setting (General tab) points to a hostname/IP reachable from the ML API container, since the ML API fetches snapshots by URL.
 
+### Improved
+- **Settings Search Finds More Cards** — The cross-tab search field at the top of Settings now finds Sidebar Links, Spoolman, Spool Catalog, Color Catalog, all four Failure Detection sections, Advanced Email Authentication, SMTP Test, Authenticator App (TOTP), Email OTP, 2FA Linked Accounts, Single Sign-On (OIDC), LDAP Server Configuration, and the four Backup sub-cards (GitHub, History, Local, Scheduled). Powered by a new module-level registry (`frontend/src/lib/settingsSearch.ts`) so future settings register themselves next to their component instead of being forgotten in a central array.
+
 ### Fixed
 - **Clear Plate Confirmation Bypassed on Power Cycle** ([#961](https://github.com/maziggy/bambuddy/issues/961)) — With Auto Off enabled and another job queued, the smart plug would cut power when a print finished and immediately re-power when the scheduler saw the queue, at which point the printer booted fresh into `IDLE` and the next job auto-dispatched without the "Clear Plate & Start Next" confirmation. Root cause: the plate-cleared gate lived only in the in-memory `PrinterManager._plate_cleared` set, and the scheduler's idle check treated `IDLE` as always-idle regardless of whether a previous finish had been acknowledged — so the gate was lost across both Bambuddy restarts and the IDLE-on-boot state transition. The gate is now an `awaiting_plate_clear` column on the `printers` table, set by `on_print_complete` when a print finishes or fails, cleared by the `/printers/{id}/clear-plate` endpoint and by the scheduler when it dispatches the next job, and rehydrated from the DB into `PrinterManager` on startup. `_is_printer_idle` now short-circuits to not-idle whenever `require_plate_clear` is on and the printer is awaiting ack, regardless of the currently reported state — so the prompt survives Auto Off cycles, Bambuddy restarts, and the printer booting back into `IDLE`. The clear-plate endpoint no longer requires the printer to currently report `FINISH`/`FAILED` (it accepts the ack whenever the awaiting flag is set), and the Printers page widget prompts based on the flag rather than the reported state. Thanks to @miaopas for reporting.
 - **Insecure Temp File Creation in Backup Export** — The manual backup download endpoint used `tempfile.mktemp()`, which is vulnerable to a symlink race condition (CWE-377). Replaced with `tempfile.mkstemp()` which atomically creates the file, eliminating the TOCTOU window.

+ 1 - 1
frontend/src/components/ColorCatalogSettings.tsx

@@ -312,7 +312,7 @@ export function ColorCatalogSettings() {
   };
 
   return (
-    <Card>
+    <Card id="card-color-catalog">
       <CardHeader>
         <div className="flex items-center gap-2 mb-3">
           <Palette className="w-5 h-5 text-bambu-gray" />

+ 3 - 3
frontend/src/components/EmailSettings.tsx

@@ -176,7 +176,7 @@ export function EmailSettings() {
   return (
     <div className="space-y-3">
       {/* Advanced Authentication Toggle - Always visible */}
-      <Card>
+      <Card id="card-email-advanced-auth">
         <CardHeader>
           <div className="flex items-center justify-between">
             <div className="flex items-center gap-2">
@@ -245,7 +245,7 @@ export function EmailSettings() {
       {/* SMTP Config + Test SMTP side-by-side on lg+ */}
       <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
         <div className="lg:col-span-2">
-        <Card>
+        <Card id="card-smtp-config">
           <CardHeader>
             <h2 className="text-lg font-semibold text-white">
               {t('settings.email.smtpSettings') || 'SMTP Configuration'}
@@ -393,7 +393,7 @@ export function EmailSettings() {
 
         {/* Test SMTP */}
         <div>
-        <Card>
+        <Card id="card-email-test">
           <CardHeader>
             <h2 className="text-lg font-semibold text-white">
               {t('settings.email.testConnection') || 'Test SMTP Connection'}

+ 1 - 1
frontend/src/components/ExternalLinksSettings.tsx

@@ -82,7 +82,7 @@ export function ExternalLinksSettings() {
 
   return (
     <>
-      <Card>
+      <Card id="card-sidebar-links">
         <CardHeader>
           <div className="flex items-center justify-between">
             <div className="flex items-center gap-2">

+ 4 - 4
frontend/src/components/FailureDetectionSettings.tsx

@@ -126,7 +126,7 @@ export function FailureDetectionSettings() {
   return (
     <div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
       <div className="space-y-3 flex-1 lg:max-w-xl">
-        <Card>
+        <Card id="card-fd-ml">
           <CardHeader>
             <div className="flex items-center justify-between">
               <div className="flex items-center gap-2">
@@ -233,7 +233,7 @@ export function FailureDetectionSettings() {
           </CardContent>
         </Card>
 
-        <Card>
+        <Card id="card-fd-perprinter">
           <CardHeader>
             <h2 className="text-lg font-semibold text-white">{t('failureDetection.perPrinterTitle')}</h2>
             <p className="text-sm text-bambu-gray mt-1">{t('failureDetection.perPrinterHint')}</p>
@@ -268,7 +268,7 @@ export function FailureDetectionSettings() {
       </div>
 
       <div className="space-y-3 flex-1 lg:max-w-xl">
-        <Card>
+        <Card id="card-fd-status">
           <CardHeader>
             <h2 className="text-lg font-semibold text-white">{t('failureDetection.statusTitle')}</h2>
           </CardHeader>
@@ -329,7 +329,7 @@ export function FailureDetectionSettings() {
           </CardContent>
         </Card>
 
-        <Card>
+        <Card id="card-fd-history">
           <CardHeader>
             <div className="flex items-center justify-between">
               <h2 className="text-lg font-semibold text-white">{t('failureDetection.historyTitle')}</h2>

+ 4 - 4
frontend/src/components/GitHubBackupSettings.tsx

@@ -464,7 +464,7 @@ export function GitHubBackupSettings() {
     <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
       {/* Left Column - GitHub Backup */}
       <div className="space-y-6">
-        <Card>
+        <Card id="card-backup-github">
           <CardHeader>
             <div className="flex items-center justify-between">
               <div className="flex items-center gap-2">
@@ -736,7 +736,7 @@ export function GitHubBackupSettings() {
 
         {/* Backup History - only show if configured and has logs */}
         {logs && logs.length > 0 && (
-          <Card>
+          <Card id="card-backup-history">
             <CardHeader>
               <div className="flex items-center justify-between">
                 <div className="flex items-center gap-2">
@@ -796,7 +796,7 @@ export function GitHubBackupSettings() {
 
       {/* Right Column - Local Backup */}
       <div className="space-y-6">
-        <Card>
+        <Card id="card-backup-local">
           <CardHeader>
             <div className="flex items-center gap-2">
               <Database className="w-5 h-5 text-gray-400" />
@@ -924,7 +924,7 @@ export function GitHubBackupSettings() {
         </Card>
 
         {/* Scheduled Local Backups */}
-        <Card>
+        <Card id="card-backup-scheduled">
           <CardHeader>
             <div className="flex items-center justify-between">
               <div className="flex items-center gap-2">

+ 2 - 2
frontend/src/components/LDAPSettings.tsx

@@ -175,7 +175,7 @@ export function LDAPSettings() {
   return (
     <div className="space-y-3">
       {/* LDAP Toggle */}
-      <Card>
+      <Card id="card-ldap-toggle">
         <CardHeader>
           <div className="flex items-center justify-between">
             <div className="flex items-center gap-2">
@@ -239,7 +239,7 @@ export function LDAPSettings() {
       </Card>
 
       {/* LDAP Server Configuration */}
-      <Card>
+      <Card id="card-ldap-server">
         <CardHeader>
           <h2 className="text-lg font-semibold text-white">
             {t('settings.ldap.serverConfig') || 'LDAP Server Configuration'}

+ 2 - 2
frontend/src/components/OIDCProviderSettings.tsx

@@ -186,7 +186,7 @@ export function OIDCProviderSettings() {
   return (
     <div className="space-y-6">
       {/* Header */}
-      <Card>
+      <Card id="card-oidc">
         <CardHeader>
           <div className="flex items-center justify-between">
             <div>
@@ -219,7 +219,7 @@ export function OIDCProviderSettings() {
 
       {/* Provider list */}
       {providers && providers.length === 0 && !showCreate && (
-        <Card>
+        <Card id="card-oidc-empty">
           <CardContent>
             <div className="text-center py-8 space-y-3">
               <Globe className="w-12 h-12 text-bambu-gray mx-auto" />

+ 1 - 1
frontend/src/components/SpoolCatalogSettings.tsx

@@ -199,7 +199,7 @@ export function SpoolCatalogSettings() {
   };
 
   return (
-    <Card>
+    <Card id="card-spool-catalog">
       <CardHeader>
         <div className="flex items-center gap-2 mb-3">
           <Database className="w-5 h-5 text-bambu-gray" />

+ 2 - 2
frontend/src/components/SpoolmanSettings.tsx

@@ -161,7 +161,7 @@ export function SpoolmanSettings() {
 
   if (settingsLoading) {
     return (
-      <Card>
+      <Card id="card-spoolman">
         <CardHeader>
           <div className="flex items-center gap-2">
             <Database className="w-5 h-5 text-bambu-green" />
@@ -178,7 +178,7 @@ export function SpoolmanSettings() {
   }
 
   return (
-    <Card>
+    <Card id="card-spoolman">
       <CardHeader>
         <div className="flex items-center justify-between">
           <div className="flex items-center gap-2">

+ 3 - 3
frontend/src/components/TwoFactorSettings.tsx

@@ -286,7 +286,7 @@ export function TwoFactorSettings() {
   return (
     <div className="space-y-6">
       {/* ── TOTP ─────────────────────────────────────────────────────────── */}
-      <Card>
+      <Card id="card-2fa-totp">
         <CardHeader>
           <div className="flex items-center gap-3">
             <div className={`w-10 h-10 rounded-full flex items-center justify-center ${status?.totp_enabled ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
@@ -391,7 +391,7 @@ export function TwoFactorSettings() {
       </Card>
 
       {/* ── Email OTP ─────────────────────────────────────────────────────── */}
-      <Card>
+      <Card id="card-2fa-emailotp">
         <CardHeader>
           <div className="flex items-center gap-3">
             <div className={`w-10 h-10 rounded-full flex items-center justify-center ${status?.email_otp_enabled ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
@@ -513,7 +513,7 @@ export function TwoFactorSettings() {
 
       {/* ── Linked SSO accounts ───────────────────────────────────────────── */}
       {oidcLinks && oidcLinks.length > 0 && (
-        <Card>
+        <Card id="card-2fa-linked">
           <CardHeader>
             <h3 className="text-white font-semibold">{t('settings.twoFa.linkedAccounts')}</h3>
             <p className="text-bambu-gray text-sm">{t('settings.twoFa.linkedAccountsDesc')}</p>

+ 48 - 0
frontend/src/lib/settingsSearch.ts

@@ -0,0 +1,48 @@
+// Settings search registry.
+//
+// Each settings card/section registers itself at module-import time by calling
+// `registerSettingsSearch(...)` at module scope (NOT inside a component).
+// SettingsPage reads the accumulated registry to power its cross-tab search.
+//
+// Convention: co-locate the registration call with the component/section that
+// owns the `anchor` id. When you add a new settings card, add one call here
+// next to it — no central index to forget to update.
+
+export type SettingsSearchTab =
+  | 'general'
+  | 'plugs'
+  | 'notifications'
+  | 'queue'
+  | 'filament'
+  | 'network'
+  | 'apikeys'
+  | 'virtual-printer'
+  | 'spoolbuddy'
+  | 'users'
+  | 'backup'
+  | 'failure-detection';
+
+export type SettingsSearchSubTab = 'users' | 'email' | 'ldap' | 'oidc' | 'twofa';
+
+export interface SettingsSearchEntry {
+  /** i18n key for the label. Resolved with t() at render time. */
+  labelKey: string;
+  /** Fallback label if the i18n key is missing. */
+  labelFallback?: string;
+  tab: SettingsSearchTab;
+  subTab?: SettingsSearchSubTab;
+  /** Space-separated extra search terms (lowercase). */
+  keywords: string;
+  /** DOM id attached to the target card — used for scrollIntoView. */
+  anchor: string;
+}
+
+const entries = new Map<string, SettingsSearchEntry>();
+
+export function registerSettingsSearch(entry: SettingsSearchEntry): void {
+  entries.set(entry.anchor, entry);
+}
+
+export function getSettingsSearchEntries(): SettingsSearchEntry[] {
+  return Array.from(entries.values());
+}

+ 75 - 58
frontend/src/pages/SettingsPage.tsx

@@ -39,11 +39,78 @@ import { useToast } from '../contexts/ToastContext';
 import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, type ThemeAccent } from '../contexts/ThemeContext';
 import { useState, useEffect, useRef, useCallback } from 'react';
 import { Palette } from 'lucide-react';
+import { registerSettingsSearch, getSettingsSearchEntries } from '../lib/settingsSearch';
 
 const validTabs = ['general', 'plugs', 'notifications', 'queue', 'filament', 'network', 'apikeys', 'virtual-printer', 'spoolbuddy', 'failure-detection', 'users', 'backup'] as const;
 type TabType = typeof validTabs[number];
 type UsersSubTab = 'users' | 'email' | 'ldap' | 'twofa' | 'oidc';
 
+// Cross-tab search registrations for cards rendered inline in this file.
+// Adding a new settings card? Register it here (or, if the card lives in its
+// own component file, call registerSettingsSearch at that file's module scope).
+registerSettingsSearch({ labelKey: 'settings.general', tab: 'general', keywords: 'language date time format printer model printers cards', anchor: 'card-general' });
+registerSettingsSearch({ labelKey: 'settings.appearance', tab: 'general', keywords: 'theme dark light mode colors', anchor: 'card-appearance' });
+registerSettingsSearch({ labelKey: 'settings.archiveSettings', tab: 'general', keywords: 'archive auto save thumbnails captures', anchor: 'card-archive' });
+registerSettingsSearch({ labelKey: 'settings.camera', tab: 'general', keywords: 'camera external video stream', anchor: 'card-camera' });
+registerSettingsSearch({ labelKey: 'settings.costTracking', tab: 'general', keywords: 'currency filament cost energy kwh price', anchor: 'card-cost' });
+registerSettingsSearch({ labelKey: 'settings.fileManager', tab: 'general', keywords: 'file manager archive mode disk warning storage', anchor: 'card-filemanager' });
+registerSettingsSearch({ labelKey: 'settings.updates', tab: 'general', keywords: 'updates version firmware beta check', anchor: 'card-updates' });
+registerSettingsSearch({ labelKey: 'settings.dataManagement', tab: 'general', keywords: 'data reset clear logs notifications preferences', anchor: 'card-data' });
+registerSettingsSearch({ labelKey: 'settings.smartPlugs', tab: 'plugs', keywords: 'smart plug energy power automation tapo kasa tplink shelly', anchor: 'card-plugs' });
+registerSettingsSearch({ labelKey: 'settings.providers', tab: 'notifications', keywords: 'telegram discord email notification providers webhook', anchor: 'card-providers' });
+registerSettingsSearch({ labelKey: 'settings.messageTemplates', tab: 'notifications', keywords: 'message templates notification text edit', anchor: 'card-templates' });
+registerSettingsSearch({ labelKey: 'settings.defaultPrintOptions', labelFallback: 'Default Print Options', tab: 'queue', keywords: 'print bed leveling flow calibration vibration first layer timelapse', anchor: 'card-print-options' });
+registerSettingsSearch({ labelKey: 'settings.staggeredStart', labelFallback: 'Staggered Start', tab: 'queue', keywords: 'staggered batch delay start queue group', anchor: 'card-staggered' });
+registerSettingsSearch({ labelKey: 'settings.plateClear', labelFallback: 'Plate-Clear Confirmation', tab: 'queue', keywords: 'plate clear confirm auto queue', anchor: 'card-plate' });
+registerSettingsSearch({ labelKey: 'settings.gcodeInjection', labelFallback: 'G-code Injection', tab: 'queue', keywords: 'gcode injection start end autoprint farmloop swapmod autoclear printflow', anchor: 'card-gcode' });
+registerSettingsSearch({ labelKey: 'settings.queueDrying', tab: 'queue', keywords: 'drying presets temperature time humidity ams', anchor: 'card-drying' });
+registerSettingsSearch({ labelKey: 'settings.filamentChecks', tab: 'filament', keywords: 'filament check warning runout remaining', anchor: 'card-filamentchecks' });
+registerSettingsSearch({ labelKey: 'settings.printModal', tab: 'filament', keywords: 'print modal custom mapping', anchor: 'card-printmodal' });
+registerSettingsSearch({ labelKey: 'settings.amsDisplayThresholds', tab: 'filament', keywords: 'ams humidity temperature threshold history retention', anchor: 'card-amsthresholds' });
+registerSettingsSearch({ labelKey: 'settings.externalUrl', tab: 'network', keywords: 'external url reverse proxy public notification link', anchor: 'card-externalurl' });
+registerSettingsSearch({ labelKey: 'settings.ftpRetry', tab: 'network', keywords: 'ftp retry upload retries backoff', anchor: 'card-ftpretry' });
+registerSettingsSearch({ labelKey: 'settings.homeAssistant', tab: 'network', keywords: 'home assistant ha hass mqtt integration', anchor: 'card-ha' });
+registerSettingsSearch({ labelKey: 'settings.mqttPublishing', tab: 'network', keywords: 'mqtt publish broker topic', anchor: 'card-mqtt' });
+registerSettingsSearch({ labelKey: 'settings.prometheusMetrics', tab: 'network', keywords: 'prometheus metrics grafana monitoring bearer token', anchor: 'card-prometheus' });
+registerSettingsSearch({ labelKey: 'settings.createNewApiKey', tab: 'apikeys', keywords: 'api key create permission scope', anchor: 'card-createapi' });
+registerSettingsSearch({ labelKey: 'settings.webhookEndpoints', tab: 'apikeys', keywords: 'webhook endpoint post http', anchor: 'card-webhooks' });
+registerSettingsSearch({ labelKey: 'settings.apiBrowser', tab: 'apikeys', keywords: 'api browser endpoint documentation test', anchor: 'card-apibrowser' });
+registerSettingsSearch({ labelKey: 'settings.tabs.virtualPrinter', tab: 'virtual-printer', keywords: 'virtual printer proxy archive slicer bambustudio orcaslicer ip bind', anchor: 'card-vp' });
+registerSettingsSearch({ labelKey: 'settings.tabs.spoolbuddy', tab: 'spoolbuddy', keywords: 'spoolbuddy device scale nfc rfid kiosk unregister', anchor: 'card-spoolbuddy' });
+registerSettingsSearch({ labelKey: 'settings.currentUser', tab: 'users', subTab: 'users', keywords: 'current user profile password change', anchor: 'card-currentuser' });
+registerSettingsSearch({ labelKey: 'settings.users', tab: 'users', subTab: 'users', keywords: 'users accounts list', anchor: 'card-users' });
+registerSettingsSearch({ labelKey: 'settings.groups', tab: 'users', subTab: 'users', keywords: 'groups roles permissions administrators operators viewers', anchor: 'card-groups' });
+registerSettingsSearch({ labelKey: 'settings.email.smtpSettings', labelFallback: 'SMTP Configuration', tab: 'users', subTab: 'email', keywords: 'smtp email send server port password auth starttls ssl', anchor: 'card-smtp' });
+registerSettingsSearch({ labelKey: 'settings.ldap.title', labelFallback: 'LDAP Authentication', tab: 'users', subTab: 'ldap', keywords: 'ldap active directory ad authentication bind dn search base group mapping', anchor: 'card-ldap' });
+registerSettingsSearch({ labelKey: 'settings.tabs.backup', tab: 'backup', keywords: 'backup github restore download cloud sync profiles archives', anchor: 'card-backup' });
+// Sidebar Links (external links settings is rendered in the General tab)
+registerSettingsSearch({ labelKey: 'externalLinks.title', labelFallback: 'Sidebar Links', tab: 'general', keywords: 'sidebar links external custom navigation url add', anchor: 'card-sidebar-links' });
+// Filament tab — integrations
+registerSettingsSearch({ labelKey: 'settings.filamentTracking', tab: 'filament', keywords: 'spoolman filament tracking inventory sync remote integration', anchor: 'card-spoolman' });
+registerSettingsSearch({ labelKey: 'settings.catalog.spoolCatalog', labelFallback: 'Spool Catalog', tab: 'filament', keywords: 'spool catalog entries brand material reset import export', anchor: 'card-spool-catalog' });
+registerSettingsSearch({ labelKey: 'settings.colorCatalog.title', labelFallback: 'Color Catalog', tab: 'filament', keywords: 'color catalog hex swatch palette sync reset', anchor: 'card-color-catalog' });
+// Failure detection sub-cards
+registerSettingsSearch({ labelKey: 'settings.tabs.failureDetection', labelFallback: 'Failure Detection', tab: 'failure-detection', keywords: 'failure detection ai ml obico spaghetti detect monitoring', anchor: 'card-fd-ml' });
+registerSettingsSearch({ labelKey: 'failureDetection.perPrinterTitle', labelFallback: 'Per-Printer Settings', tab: 'failure-detection', keywords: 'failure detection per printer enable per-printer sensitivity', anchor: 'card-fd-perprinter' });
+registerSettingsSearch({ labelKey: 'failureDetection.statusTitle', labelFallback: 'Detection Status', tab: 'failure-detection', keywords: 'failure detection status running connection', anchor: 'card-fd-status' });
+registerSettingsSearch({ labelKey: 'failureDetection.historyTitle', labelFallback: 'Detection History', tab: 'failure-detection', keywords: 'failure detection history log events', anchor: 'card-fd-history' });
+// Email auth sub-cards (subTab=email)
+registerSettingsSearch({ labelKey: 'settings.email.advancedAuth', labelFallback: 'Advanced Email Authentication', tab: 'users', subTab: 'email', keywords: 'email authentication advanced password reset self-service forgot', anchor: 'card-email-advanced-auth' });
+registerSettingsSearch({ labelKey: 'settings.email.testConnection', labelFallback: 'Test SMTP Connection', tab: 'users', subTab: 'email', keywords: 'email smtp test connection send check', anchor: 'card-email-test' });
+// Two-Factor sub-cards (subTab=twofa)
+registerSettingsSearch({ labelKey: 'settings.twoFa.totpTitle', labelFallback: 'Authenticator App (TOTP)', tab: 'users', subTab: 'twofa', keywords: 'two factor 2fa totp authenticator app google authy otp', anchor: 'card-2fa-totp' });
+registerSettingsSearch({ labelKey: 'settings.twoFa.emailOtpTitle', labelFallback: 'Email One-Time Codes', tab: 'users', subTab: 'twofa', keywords: 'two factor 2fa email otp one time code', anchor: 'card-2fa-emailotp' });
+registerSettingsSearch({ labelKey: 'settings.twoFa.linkedAccounts', labelFallback: 'Linked Accounts', tab: 'users', subTab: 'twofa', keywords: 'two factor 2fa linked accounts sso oidc provider google github', anchor: 'card-2fa-linked' });
+// OIDC / SSO (subTab=oidc)
+registerSettingsSearch({ labelKey: 'settings.oidc.title', labelFallback: 'Single Sign-On (OIDC)', tab: 'users', subTab: 'oidc', keywords: 'sso oidc openid single sign-on pocketid authentik keycloak google okta azure provider', anchor: 'card-oidc' });
+// LDAP server config card (complements existing card-ldap)
+registerSettingsSearch({ labelKey: 'settings.ldap.serverConfig', labelFallback: 'LDAP Server Configuration', tab: 'users', subTab: 'ldap', keywords: 'ldap server url bind dn user search base group filter tls', anchor: 'card-ldap-server' });
+// Backup sub-cards
+registerSettingsSearch({ labelKey: 'backup.githubBackup', labelFallback: 'GitHub Backup', tab: 'backup', keywords: 'github backup cloud remote sync profiles token', anchor: 'card-backup-github' });
+registerSettingsSearch({ labelKey: 'backup.history', labelFallback: 'Backup History', tab: 'backup', keywords: 'backup history log runs github commits', anchor: 'card-backup-history' });
+registerSettingsSearch({ labelKey: 'backup.localBackup', labelFallback: 'Local Backup', tab: 'backup', keywords: 'local backup download zip manual export', anchor: 'card-backup-local' });
+registerSettingsSearch({ labelKey: 'backup.scheduledBackup', labelFallback: 'Scheduled Backups', tab: 'backup', keywords: 'scheduled backup automatic hourly daily weekly retention local path', anchor: 'card-backup-scheduled' });
+
 const STORAGE_CATEGORY_COLORS: Record<string, string> = {
   database: 'bg-blue-600',
   library_files: 'bg-green-500',
@@ -999,62 +1066,12 @@ export function SettingsPage() {
     );
   }
 
-  // Cross-tab settings search index. Keep labels short — matched against user's query.
-  // anchor is the DOM id attached to the target card (also wired to scrollIntoView).
-  const searchIndex: Array<{
-    label: string;
-    tab: TabType;
-    subTab?: UsersSubTab;
-    keywords: string;
-    anchor: string;
-  }> = [
-    // General
-    { label: t('settings.general'), tab: 'general', keywords: 'language date time format printer model printers cards', anchor: 'card-general' },
-    { label: t('settings.appearance'), tab: 'general', keywords: 'theme dark light mode colors', anchor: 'card-appearance' },
-    { label: t('settings.archiveSettings'), tab: 'general', keywords: 'archive auto save thumbnails captures', anchor: 'card-archive' },
-    { label: t('settings.camera'), tab: 'general', keywords: 'camera external video stream', anchor: 'card-camera' },
-    { label: t('settings.costTracking'), tab: 'general', keywords: 'currency filament cost energy kwh price', anchor: 'card-cost' },
-    { label: t('settings.fileManager'), tab: 'general', keywords: 'file manager archive mode disk warning storage', anchor: 'card-filemanager' },
-    { label: t('settings.updates'), tab: 'general', keywords: 'updates version firmware beta check', anchor: 'card-updates' },
-    { label: t('settings.dataManagement'), tab: 'general', keywords: 'data reset clear logs notifications preferences', anchor: 'card-data' },
-    // Smart Plugs
-    { label: t('settings.smartPlugs'), tab: 'plugs', keywords: 'smart plug energy power automation tapo kasa tplink shelly', anchor: 'card-plugs' },
-    // Notifications
-    { label: t('settings.providers'), tab: 'notifications', keywords: 'telegram discord email notification providers webhook', anchor: 'card-providers' },
-    { label: t('settings.messageTemplates'), tab: 'notifications', keywords: 'message templates notification text edit', anchor: 'card-templates' },
-    // Queue / Workflow
-    { label: t('settings.defaultPrintOptions', 'Default Print Options'), tab: 'queue', keywords: 'print bed leveling flow calibration vibration first layer timelapse', anchor: 'card-print-options' },
-    { label: t('settings.staggeredStart', 'Staggered Start'), tab: 'queue', keywords: 'staggered batch delay start queue group', anchor: 'card-staggered' },
-    { label: t('settings.plateClear', 'Plate-Clear Confirmation'), tab: 'queue', keywords: 'plate clear confirm auto queue', anchor: 'card-plate' },
-    { label: t('settings.gcodeInjection', 'G-code Injection'), tab: 'queue', keywords: 'gcode injection start end autoprint farmloop swapmod autoclear printflow', anchor: 'card-gcode' },
-    { label: t('settings.queueDrying'), tab: 'queue', keywords: 'drying presets temperature time humidity ams', anchor: 'card-drying' },
-    // Filament
-    { label: t('settings.filamentChecks'), tab: 'filament', keywords: 'filament check warning runout remaining', anchor: 'card-filamentchecks' },
-    { label: t('settings.printModal'), tab: 'filament', keywords: 'print modal custom mapping', anchor: 'card-printmodal' },
-    { label: t('settings.amsDisplayThresholds'), tab: 'filament', keywords: 'ams humidity temperature threshold history retention', anchor: 'card-amsthresholds' },
-    // Network
-    { label: t('settings.externalUrl'), tab: 'network', keywords: 'external url reverse proxy public notification link', anchor: 'card-externalurl' },
-    { label: t('settings.ftpRetry'), tab: 'network', keywords: 'ftp retry upload retries backoff', anchor: 'card-ftpretry' },
-    { label: t('settings.homeAssistant'), tab: 'network', keywords: 'home assistant ha hass mqtt integration', anchor: 'card-ha' },
-    { label: t('settings.mqttPublishing'), tab: 'network', keywords: 'mqtt publish broker topic', anchor: 'card-mqtt' },
-    { label: t('settings.prometheusMetrics'), tab: 'network', keywords: 'prometheus metrics grafana monitoring bearer token', anchor: 'card-prometheus' },
-    // API Keys
-    { label: t('settings.createNewApiKey'), tab: 'apikeys', keywords: 'api key create permission scope', anchor: 'card-createapi' },
-    { label: t('settings.webhookEndpoints'), tab: 'apikeys', keywords: 'webhook endpoint post http', anchor: 'card-webhooks' },
-    { label: t('settings.apiBrowser'), tab: 'apikeys', keywords: 'api browser endpoint documentation test', anchor: 'card-apibrowser' },
-    // Virtual Printer
-    { label: t('settings.tabs.virtualPrinter'), tab: 'virtual-printer', keywords: 'virtual printer proxy archive slicer bambustudio orcaslicer ip bind', anchor: 'card-vp' },
-    // SpoolBuddy
-    { label: t('settings.tabs.spoolbuddy'), tab: 'spoolbuddy', keywords: 'spoolbuddy device scale nfc rfid kiosk unregister', anchor: 'card-spoolbuddy' },
-    // Users / Auth
-    { label: t('settings.currentUser'), tab: 'users', subTab: 'users', keywords: 'current user profile password change', anchor: 'card-currentuser' },
-    { label: t('settings.users'), tab: 'users', subTab: 'users', keywords: 'users accounts list', anchor: 'card-users' },
-    { label: t('settings.groups'), tab: 'users', subTab: 'users', keywords: 'groups roles permissions administrators operators viewers', anchor: 'card-groups' },
-    { label: t('settings.email.smtpSettings', 'SMTP Configuration'), tab: 'users', subTab: 'email', keywords: 'smtp email send server port password auth starttls ssl', anchor: 'card-smtp' },
-    { label: t('settings.ldap.title', 'LDAP Authentication'), tab: 'users', subTab: 'ldap', keywords: 'ldap active directory ad authentication bind dn search base group mapping', anchor: 'card-ldap' },
-    // Backup
-    { label: t('settings.tabs.backup'), tab: 'backup', keywords: 'backup github restore download cloud sync profiles archives', anchor: 'card-backup' },
-  ];
+  // Cross-tab search is powered by the module-level registry in lib/settingsSearch.
+  // Resolve i18n labels here so language changes take effect without re-registering.
+  const searchIndex = getSettingsSearchEntries().map(e => ({
+    ...e,
+    label: t(e.labelKey, e.labelFallback ?? e.labelKey),
+  }));
 
   const searchQuery = settingsSearch.trim().toLowerCase();
   const searchResults = searchQuery
@@ -1066,9 +1083,9 @@ export function SettingsPage() {
     : [];
 
   const jumpToSetting = (entry: typeof searchIndex[number]) => {
-    handleTabChange(entry.tab);
+    handleTabChange(entry.tab as TabType);
     if (entry.subTab) {
-      setUsersSubTab(entry.subTab);
+      setUsersSubTab(entry.subTab as UsersSubTab);
     }
     setSettingsSearch('');
     // Scroll to the card after the tab has rendered

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BQYDsJHk.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BhUKKvXf.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BopeIWC_.js"></script>
+    <script type="module" crossorigin src="/assets/index-BQYDsJHk.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-B7mnhxng.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff