From 7510be9481183f22b50823079b2555bfb7c325a7 Mon Sep 17 00:00:00 2001 From: niraj12chaudhary Date: Sun, 8 Mar 2026 09:23:50 +0530 Subject: [PATCH 1/2] Add IndexedDB autosave backup for UI config and fix WebuiManager typing --- src/webui/interface.py | 177 +++++++++++++++++++++++++++++++++++-- src/webui/webui_manager.py | 25 +++--- 2 files changed, 184 insertions(+), 18 deletions(-) diff --git a/src/webui/interface.py b/src/webui/interface.py index 083649e69..0ea3a4c95 100644 --- a/src/webui/interface.py +++ b/src/webui/interface.py @@ -42,15 +42,182 @@ def create_ui(theme_name="Ocean"): } """ - # dark mode in default + # dark mode in default + browser-side autosave backup for UI settings js_func = """ - function refresh() { - const url = new URL(window.location); - + function initializeWebUiClientState() { + const url = new URL(window.location.href); if (url.searchParams.get('__theme') !== 'dark') { url.searchParams.set('__theme', 'dark'); - window.location.href = url.href; + window.location.href = url.toString(); + return; + } + + if (window.__webuiBackupInitDone) { + return; + } + window.__webuiBackupInitDone = true; + + const DB_NAME = "webui-config-backup"; + const STORE_NAME = "settings"; + const RECORD_KEY = "ui-components-v1"; + const FALLBACK_STORAGE_KEY = "webui-config-backup-fallback"; + const SAVE_INTERVAL_MS = 60000; + + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + function isVisible(element) { + return !!(element && (element.offsetParent !== null || element.getClientRects().length)); + } + + function firstVisible(elements) { + return Array.from(elements).find((el) => isVisible(el) && !el.disabled && !el.readOnly); + } + + function getPrimaryControl(componentRoot) { + return ( + firstVisible(componentRoot.querySelectorAll("input[type='checkbox']")) || + firstVisible(componentRoot.querySelectorAll("textarea")) || + firstVisible(componentRoot.querySelectorAll("select")) || + firstVisible(componentRoot.querySelectorAll("input[type='range']")) || + firstVisible(componentRoot.querySelectorAll("input[type='number']")) || + firstVisible( + componentRoot.querySelectorAll( + "input:not([type='file']):not([type='button']):not([type='submit']):not([type='hidden'])" + ) + ) + ); + } + + function readControlValue(componentRoot) { + const control = getPrimaryControl(componentRoot); + if (!control) { + return undefined; + } + if (control.type === "checkbox") { + return !!control.checked; + } + return control.value; + } + + function writeControlValue(componentRoot, value) { + const control = getPrimaryControl(componentRoot); + if (!control) { + return; + } + if (control.type === "checkbox") { + control.checked = !!value; + } else { + control.value = value == null ? "" : String(value); + } + ["input", "change", "blur"].forEach((eventName) => { + control.dispatchEvent(new Event(eventName, { bubbles: true })); + }); + } + + function collectUiState() { + const state = {}; + document.querySelectorAll("[id^='webui-']").forEach((componentRoot) => { + const value = readControlValue(componentRoot); + if (value !== undefined) { + state[componentRoot.id] = value; + } + }); + return state; } + + function openBackupDb() { + return new Promise((resolve, reject) => { + const request = window.indexedDB.open(DB_NAME, 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: "id" }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + async function loadBackupState() { + try { + const db = await openBackupDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readonly"); + const store = tx.objectStore(STORE_NAME); + const request = store.get(RECORD_KEY); + request.onsuccess = () => resolve(request.result ? request.result.payload : null); + request.onerror = () => reject(request.error); + }); + } catch (_) { + const raw = localStorage.getItem(FALLBACK_STORAGE_KEY); + return raw ? JSON.parse(raw) : null; + } + } + + async function persistBackupState(payload) { + try { + const db = await openBackupDb(); + await new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, "readwrite"); + const store = tx.objectStore(STORE_NAME); + store.put({ + id: RECORD_KEY, + updatedAt: Date.now(), + payload + }); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error); + }); + } catch (_) { + localStorage.setItem(FALLBACK_STORAGE_KEY, JSON.stringify(payload)); + } + } + + async function restoreUiState() { + const savedState = await loadBackupState(); + if (!savedState || typeof savedState !== "object") { + return; + } + + const entries = Object.entries(savedState); + for (let pass = 0; pass < 3; pass++) { + entries.forEach(([id, value]) => { + const componentRoot = document.getElementById(id); + if (componentRoot) { + writeControlValue(componentRoot, value); + } + }); + await delay(350); + } + } + + async function saveUiState() { + const state = collectUiState(); + await persistBackupState(state); + } + + async function setupPersistence() { + await delay(1000); + await restoreUiState(); + + setInterval(() => { + void saveUiState(); + }, SAVE_INTERVAL_MS); + + window.addEventListener("beforeunload", () => { + void saveUiState(); + }); + + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + void saveUiState(); + } + }); + } + + void setupPersistence(); } """ diff --git a/src/webui/webui_manager.py b/src/webui/webui_manager.py index 0a9d5e163..a88c1d56b 100644 --- a/src/webui/webui_manager.py +++ b/src/webui/webui_manager.py @@ -1,6 +1,6 @@ import json from collections.abc import Generator -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import os import gradio as gr from datetime import datetime @@ -56,6 +56,8 @@ def add_components(self, tab_name: str, components_dict: dict[str, "Component"]) """ for comp_name, component in components_dict.items(): comp_id = f"{tab_name}.{comp_name}" + if hasattr(component, "elem_id") and not getattr(component, "elem_id", None): + component.elem_id = f"webui-{tab_name}-{comp_name}".replace(".", "-").replace("_", "-") self.id_to_component[comp_id] = component self.component_to_id[component] = comp_id @@ -77,7 +79,7 @@ def get_id_by_component(self, comp: "Component") -> str: """ return self.component_to_id[comp] - def save_config(self, components: Dict["Component", str]) -> None: + def save_config(self, components: Dict["Component", str]) -> str: """ Save config """ @@ -94,29 +96,26 @@ def save_config(self, components: Dict["Component", str]) -> None: return os.path.join(self.settings_save_dir, f"{config_name}.json") - def load_config(self, config_path: str): + def load_config(self, config_path: str) -> Generator[Dict[Component, Any], None, None]: """ Load config """ with open(config_path, "r") as fr: - ui_settings = json.load(fr) + ui_settings: Dict[str, Any] = json.load(fr) - update_components = {} + update_components: Dict[Component, Any] = {} for comp_id, comp_val in ui_settings.items(): if comp_id in self.id_to_component: comp = self.id_to_component[comp_id] - if comp.__class__.__name__ == "Chatbot": - update_components[comp] = comp.__class__(value=comp_val, type="messages") - else: - update_components[comp] = comp.__class__(value=comp_val) - if comp_id == "agent_settings.planner_llm_provider": - yield update_components # yield provider, let callback run - time.sleep(0.1) # wait for Gradio UI callback + update_components[comp] = gr.update(value=comp_val) + if comp_id == "agent_settings.planner_llm_provider": + yield update_components # yield provider, let callback run + time.sleep(0.1) # wait for Gradio UI callback config_status = self.id_to_component["load_save_config.config_status"] update_components.update( { - config_status: config_status.__class__(value=f"Successfully loaded config: {config_path}") + config_status: gr.update(value=f"Successfully loaded config: {config_path}") } ) yield update_components From d4a0d80902f8fea5bfe777a485b754837e1c55ef Mon Sep 17 00:00:00 2001 From: niraj12chaudhary Date: Sun, 8 Mar 2026 09:40:08 +0530 Subject: [PATCH 2/2] Exclude sensitive fields from config autosave and harden storage JSON parsing --- src/webui/interface.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/webui/interface.py b/src/webui/interface.py index 0ea3a4c95..451600860 100644 --- a/src/webui/interface.py +++ b/src/webui/interface.py @@ -99,6 +99,18 @@ def create_ui(theme_name="Ocean"): return control.value; } + function isSensitiveControl(componentRoot, control) { + const id = (componentRoot.id || "").toLowerCase(); + return ( + control.type === "password" || + id.includes("api-key") || + id.includes("apikey") || + id.includes("token") || + id.includes("secret") || + id.includes("password") + ); + } + function writeControlValue(componentRoot, value) { const control = getPrimaryControl(componentRoot); if (!control) { @@ -117,6 +129,10 @@ def create_ui(theme_name="Ocean"): function collectUiState() { const state = {}; document.querySelectorAll("[id^='webui-']").forEach((componentRoot) => { + const control = getPrimaryControl(componentRoot); + if (!control || isSensitiveControl(componentRoot, control)) { + return; + } const value = readControlValue(componentRoot); if (value !== undefined) { state[componentRoot.id] = value; @@ -151,7 +167,15 @@ def create_ui(theme_name="Ocean"): }); } catch (_) { const raw = localStorage.getItem(FALLBACK_STORAGE_KEY); - return raw ? JSON.parse(raw) : null; + if (!raw) { + return null; + } + try { + return JSON.parse(raw); + } catch (_) { + localStorage.removeItem(FALLBACK_STORAGE_KEY); + return null; + } } } @@ -186,6 +210,10 @@ def create_ui(theme_name="Ocean"): entries.forEach(([id, value]) => { const componentRoot = document.getElementById(id); if (componentRoot) { + const control = getPrimaryControl(componentRoot); + if (control && isSensitiveControl(componentRoot, control)) { + return; + } writeControlValue(componentRoot, value); } });