// config.js — extracted from app.js (3872-4865) import { api, AuthManager } from './auth.js'; import { state } from './state.js'; import { el, icon, openFile } from './viewer.js'; import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadVaultSettings, loadTags, TagFilterService, refreshSidebarTreePreservingState } from './sidebar.js'; import { escapeHtml, safeCreateIcons } from './utils.js'; import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js'; import { t, setLocale, getLocale } from './i18n.js'; let _recentTimestampTimer = null; let _recentFilesCache = []; let _recentRefreshTimer = null; export async function loadRecentFiles(vaultFilter) { const listEl = document.getElementById("recent-list"); const emptyEl = document.getElementById("recent-empty"); if (!listEl) return; let url = "/api/recent?mode=modified"; if (vaultFilter) url += `&vault=${encodeURIComponent(vaultFilter)}`; try { const data = await api(url); _recentFilesCache = data.files || []; renderRecentList(_recentFilesCache); } catch (err) { console.error("Failed to load recent files:", err); listEl.innerHTML = ""; if (emptyEl) { emptyEl.classList.remove("hidden"); } } } function renderRecentList(files) { const listEl = document.getElementById("recent-list"); const emptyEl = document.getElementById("recent-empty"); if (!listEl) return; listEl.innerHTML = ""; if (!files || files.length === 0) { if (emptyEl) { emptyEl.classList.remove("hidden"); safeCreateIcons(); } return; } if (emptyEl) emptyEl.classList.add("hidden"); files.forEach((f) => { const item = el("div", { class: "recent-item", "data-vault": f.vault, "data-path": f.path }); // Header row: time + vault badge const header = el("div", { class: "recent-item-header" }); const timeSpan = el("span", { class: "recent-time" }, [icon("clock", 11), document.createTextNode(f.mtime_human)]); const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]); header.appendChild(timeSpan); header.appendChild(badge); item.appendChild(header); // Title const titleEl = el("div", { class: "recent-item-title" }, [document.createTextNode(f.title || f.path.split("/").pop())]); item.appendChild(titleEl); // Path breadcrumb const pathParts = f.path.split("/"); if (pathParts.length > 1) { const pathEl = el("div", { class: "recent-item-path" }, [document.createTextNode(pathParts.slice(0, -1).join(" / "))]); item.appendChild(pathEl); } // Preview if (f.preview) { const previewEl = el("div", { class: "recent-item-preview" }, [document.createTextNode(f.preview)]); item.appendChild(previewEl); } // Tags if (f.tags && f.tags.length > 0) { const tagsEl = el("div", { class: "recent-item-tags" }); f.tags.forEach((t) => { tagsEl.appendChild(el("span", { class: "tag-pill" }, [document.createTextNode(t)])); }); item.appendChild(tagsEl); } // Click handler item.addEventListener("click", () => { openFile(f.vault, f.path); closeMobileSidebar(); }); listEl.appendChild(item); }); safeCreateIcons(); } function _humanizeDelta(mtime) { const delta = Date.now() / 1000 - mtime; if (delta < 60) return "à l'instant"; if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`; if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`; if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`; return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" }); } function _refreshRecentTimestamps() { if (state.activeSidebarTab !== "recent" || !_recentFilesCache.length) return; const items = document.querySelectorAll(".recent-item"); items.forEach((item, i) => { if (i < _recentFilesCache.length) { const timeSpan = item.querySelector(".recent-time"); if (timeSpan) { // keep the icon, update text const textNode = timeSpan.lastChild; if (textNode && textNode.nodeType === Node.TEXT_NODE) { textNode.textContent = _humanizeDelta(_recentFilesCache[i].mtime); } } } }); } export function _populateRecentVaultFilter() { const select = document.getElementById("recent-vault-filter"); if (!select) return; // keep first option "Tous les vaults" while (select.options.length > 1) select.remove(1); state.allVaults.forEach((v) => { const opt = document.createElement("option"); opt.value = v.name; opt.textContent = v.name; select.appendChild(opt); }); syncVaultSelectors(); } function initRecentTab() { const select = document.getElementById("recent-vault-filter"); if (select) { select.addEventListener("change", async () => { const val = select.value || "all"; await setSelectedVaultContext(val, { focusVault: val !== "all" }); }); } // Periodic timestamp refresh (every 60s) _recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000); } // --------------------------------------------------------------------------- // Sidebar tabs // --------------------------------------------------------------------------- function initSidebarTabs() { document.querySelectorAll(".sidebar-tab").forEach((tab) => { tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab)); }); } function switchSidebarTab(tab) { state.activeSidebarTab = tab; document.querySelectorAll(".sidebar-tab").forEach((btn) => { const isActive = btn.dataset.tab === tab; btn.classList.toggle("active", isActive); btn.setAttribute("aria-selected", isActive ? "true" : "false"); }); document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => { const isActive = panel.id === `sidebar-panel-${tab}`; panel.classList.toggle("active", isActive); }); const filterInput = document.getElementById("sidebar-filter-input"); if (filterInput) { const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" }; filterInput.placeholder = placeholders[tab] || ""; } const query = filterInput ? (state.sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : ""; if (query) { if (tab === "vaults") performTreeSearch(query); else if (tab === "tags") filterTagCloud(query); } // Auto-load recent files when switching to the recent tab if (tab === "recent") { _populateRecentVaultFilter(); const vaultFilter = document.getElementById("recent-vault-filter"); loadRecentFiles(vaultFilter ? vaultFilter.value || null : null); } } function initHelpModal() { const openBtn = document.getElementById("help-open-btn"); const closeBtn = document.getElementById("help-close"); const modal = document.getElementById("help-modal"); if (!openBtn || !closeBtn || !modal) return; let helpNavInitialized = false; openBtn.addEventListener("click", () => { modal.classList.add("active"); closeHeaderMenu(); safeCreateIcons(); // Reset TOC to CSS default (mobile: hidden, desktop: visible) const nav = document.getElementById("help-nav"); if (nav) nav.style.display = ''; if (!helpNavInitialized) { initHelpNavigation(); helpNavInitialized = true; } }); closeBtn.addEventListener("click", closeHelpModal); modal.addEventListener("click", (e) => { if (e.target === modal) { closeHelpModal(); } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && modal.classList.contains("active")) { closeHelpModal(); } }); } function initEditorPocBtn() { var btn = document.getElementById("editor-poc-btn"); if (!btn) return; btn.addEventListener("click", function() { window.open("/editor-poc", "_blank"); }); } function initHelpNavigation() { const helpBody = document.getElementById("help-body"); const navList = document.getElementById("help-nav-list"); const nav = document.getElementById("help-nav"); const searchInput = document.getElementById("help-nav-search"); const helpContent = document.querySelector(".help-content"); if (!helpBody || !navList || !nav) return; const allLinks = [...navList.querySelectorAll(".help-nav-link")]; const allSections = [...document.querySelectorAll(".help-section")]; const sectionMap = {}; allSections.forEach(s => { sectionMap[s.id] = s; }); // Build text index for content search const sectionTextCache = {}; allSections.forEach(s => { sectionTextCache[s.id] = (s.textContent || '').toLowerCase(); }); // ── Mobile: simple display toggle ── function isMobile() { return window.innerWidth <= 768; } function toggleTOC() { if (nav.style.display === 'none' || nav.style.display === '') { nav.style.display = 'flex'; if (searchInput) setTimeout(() => searchInput.focus(), 150); } else { nav.style.display = 'none'; } } // ── Scroll to section ── function scrollToSection(targetId) { const section = sectionMap[targetId]; if (!section) return; section.scrollIntoView({ behavior: "smooth", block: "start" }); section.setAttribute("tabindex", "-1"); if (isMobile()) nav.style.display = 'none'; } // ── Focus the content pane ── function focusContent() { const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)"); if (activeLink) { const targetId = activeLink.getAttribute("href").substring(1); const section = sectionMap[targetId]; if (section) { const h2 = section.querySelector("h2, h3"); if (h2) { h2.setAttribute("tabindex", "-1"); h2.focus({ preventScroll: true }); return; } } } if (helpContent) { helpContent.setAttribute("tabindex", "-1"); helpContent.focus({ preventScroll: true }); } } // ── Focus TOC active link ── function focusTOC() { if (isMobile() && (nav.style.display === 'none' || nav.style.display === '')) { nav.style.display = 'flex'; } const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)"); if (activeLink) { activeLink.focus(); return; } const firstLink = navList.querySelector(".help-nav-link:not(.hidden)"); if (firstLink) firstLink.focus(); } // ── Nav link click handler ── allLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const targetId = link.getAttribute("href").substring(1); scrollToSection(targetId); }); }); // ── Keyboard navigation inside nav list ── navList.addEventListener("keydown", (e) => { const items = [...navList.querySelectorAll(".help-nav-link:not(.hidden)")]; if (!items.length) return; const currentIdx = items.indexOf(document.activeElement); if (e.key === "ArrowDown") { e.preventDefault(); const next = (currentIdx + 1) % items.length; items[next].focus(); } else if (e.key === "ArrowUp") { e.preventDefault(); const prev = (currentIdx - 1 + items.length) % items.length; items[prev].focus(); } else if (e.key === "ArrowRight") { e.preventDefault(); focusContent(); } else if (e.key === "ArrowLeft") { e.preventDefault(); const prev = (currentIdx - 1 + items.length) % items.length; items[prev].focus(); } else if (e.key === "Home") { e.preventDefault(); items[0].focus(); } else if (e.key === "End") { e.preventDefault(); items[items.length - 1].focus(); } else if (e.key === "Enter" && currentIdx >= 0) { e.preventDefault(); const targetId = items[currentIdx].getAttribute("href").substring(1); scrollToSection(targetId); setTimeout(() => focusContent(), 400); } }); navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none")); // ── Keyboard navigation in content pane ── if (helpContent) { helpContent.setAttribute("tabindex", "-1"); helpContent.addEventListener("keydown", (e) => { if (e.key === "ArrowLeft") { e.preventDefault(); focusTOC(); } else if (e.key === "ArrowUp" && document.activeElement === helpContent) { e.preventDefault(); focusTOC(); } }); } // ── Keyboard navigation in search input ── if (searchInput) { searchInput.addEventListener("keydown", (e) => { if (e.key === "ArrowDown") { e.preventDefault(); const firstLink = navList.querySelector(".help-nav-link:not(.hidden)"); if (firstLink) firstLink.focus(); } }); } // ── Search: filter TOC + hide non-matching sections + highlight text ── let currentQuery = ""; if (searchInput) { searchInput.addEventListener("input", () => { currentQuery = searchInput.value.toLowerCase().trim(); applySearchFilter(); }); searchInput.addEventListener("keydown", (e) => { if (e.key === "Escape" && currentQuery) { searchInput.value = ""; currentQuery = ""; applySearchFilter(); } }); } function clearHighlights() { allSections.forEach(section => { section.querySelectorAll(".help-search-highlight").forEach(el => { const parent = el.parentNode; parent.replaceChild(document.createTextNode(el.textContent), el); parent.normalize(); }); }); } function highlightText(root, query) { if (!query || query.length < 2) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false); const textNodes = []; while (walker.nextNode()) { const node = walker.currentNode; // Skip nodes inside script, style, or already-highlighted if (node.parentElement && !node.parentElement.closest("script, style, .help-search-highlight, .help-nav")) { textNodes.push(node); } } const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); textNodes.forEach(node => { const text = node.textContent; if (!regex.test(text)) return; regex.lastIndex = 0; const frag = document.createDocumentFragment(); let lastIdx = 0; let match; while ((match = regex.exec(text)) !== null) { if (match.index > lastIdx) { frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index))); } const mark = document.createElement("mark"); mark.className = "help-search-highlight"; mark.textContent = match[0]; frag.appendChild(mark); lastIdx = regex.lastIndex; if (match[0].length === 0) regex.lastIndex++; } if (lastIdx < text.length) { frag.appendChild(document.createTextNode(text.slice(lastIdx))); } node.parentNode.replaceChild(frag, node); }); } function applySearchFilter() { const q = currentQuery; // Clear previous highlights clearHighlights(); // Filter TOC links allLinks.forEach((link) => { const text = link.textContent.toLowerCase(); if (!q || text.includes(q)) { link.classList.remove("hidden"); } else { link.classList.add("hidden"); } }); // Filter content sections + highlight allSections.forEach((section) => { if (!q) { section.classList.remove("help-hidden"); return; } const sectionText = sectionTextCache[section.id] || ''; const sectionTitle = (section.querySelector("h2, h3")?.textContent || '').toLowerCase(); if (sectionTitle.includes(q) || sectionText.includes(q)) { section.classList.remove("help-hidden"); // Highlight matching text in this section highlightText(section, q); } else { section.classList.add("help-hidden"); } }); updateActiveLink(); ensureNoResultsMessage(q); } function ensureNoResultsMessage(q) { const existing = document.getElementById("help-no-results"); const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden")); if (q && visibleSections.length === 0) { if (!existing) { const msg = document.createElement("div"); msg.id = "help-no-results"; msg.style.cssText = "padding:40px;text-align:center;color:var(--text-muted);font-size:0.9rem"; msg.innerHTML = `
Aucun résultat pour "${q}"
Essayez un autre terme
`; if (helpContent) helpContent.appendChild(msg); } } else if (existing) { existing.remove(); } } // ── Scroll spy ── function updateActiveLink() { const visibleLinks = allLinks.filter(l => !l.classList.contains("hidden")); const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden")); let activeId = null; for (const section of visibleSections) { const rect = section.getBoundingClientRect(); const contentRect = helpContent ? helpContent.getBoundingClientRect() : null; if (rect.top < (contentRect ? contentRect.top + 120 : 200)) { activeId = section.id; } } visibleLinks.forEach((link) => { const isMatch = link.getAttribute("href") === `#${activeId}`; link.classList.toggle("active", isMatch); }); } const observer = new IntersectionObserver( (entries) => { let bestEntry = null; for (const entry of entries) { if (entry.isIntersecting) { if (!bestEntry || entry.boundingClientRect.top < bestEntry.boundingClientRect.top) { bestEntry = entry; } } } if (bestEntry) { const id = bestEntry.target.getAttribute("id"); allLinks.forEach((link) => { const isMatch = link.getAttribute("href") === `#${id}`; link.classList.toggle("active", isMatch); }); } }, { root: helpBody, rootMargin: "-10% 0px -75% 0px", threshold: [0, 0.25, 0.5], } ); allSections.forEach((section) => observer.observe(section)); // ── Keyboard: Ctrl+↑/↓ to jump between sections ── helpBody.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowDown" || e.key === "ArrowUp")) { e.preventDefault(); const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden")); const currentSection = visibleSections.find((s) => { const rect = s.getBoundingClientRect(); const contentRect = helpContent ? helpContent.getBoundingClientRect() : null; return rect.top >= (contentRect ? contentRect.top : 0) && rect.top < (contentRect ? contentRect.top + 120 : 120); }); if (!currentSection) return; const currentIdx = visibleSections.indexOf(currentSection); if (e.key === "ArrowDown" && currentIdx < visibleSections.length - 1) { visibleSections[currentIdx + 1].scrollIntoView({ behavior: "smooth", block: "start" }); } else if (e.key === "ArrowUp" && currentIdx > 0) { visibleSections[currentIdx - 1].scrollIntoView({ behavior: "smooth", block: "start" }); } } }); } function closeHelpModal() { const modal = document.getElementById("help-modal"); if (modal) modal.classList.remove("active"); } function initConfigModal() { const openBtn = document.getElementById("config-open-btn"); const closeBtn = document.getElementById("config-close"); const modal = document.getElementById("config-modal"); const addBtn = document.getElementById("config-add-btn"); const patternInput = document.getElementById("config-pattern-input"); if (!openBtn || !closeBtn || !modal) return; openBtn.addEventListener("click", async () => { modal.classList.add("active"); closeHeaderMenu(); renderConfigFilters(); loadConfigFields(); loadDiagnostics(); loadAbout(); await loadHiddenFilesSettings(); loadWebhooksUI(); loadSharesUI(); safeCreateIcons(); }); closeBtn.addEventListener("click", closeConfigModal); modal.addEventListener("click", (e) => { if (e.target === modal) { closeConfigModal(); } }); addBtn.addEventListener("click", addConfigFilter); patternInput.addEventListener("keypress", (e) => { if (e.key === "Enter") { addConfigFilter(); } }); patternInput.addEventListener("input", updateRegexPreview); // Frontend config fields — save to localStorage on change ["cfg-debounce", "cfg-results-per-page", "cfg-min-query", "cfg-timeout"].forEach((id) => { const input = document.getElementById(id); if (input) input.addEventListener("change", saveFrontendConfig); }); // Backend save button const saveBtn = document.getElementById("cfg-save-backend"); if (saveBtn) saveBtn.addEventListener("click", saveBackendConfig); // Force reindex const reindexBtn = document.getElementById("cfg-reindex"); if (reindexBtn) reindexBtn.addEventListener("click", forceReindex); // Reset defaults const resetBtn = document.getElementById("cfg-reset-defaults"); if (resetBtn) resetBtn.addEventListener("click", resetConfigDefaults); // Refresh diagnostics const diagBtn = document.getElementById("cfg-refresh-diag"); if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics); // Hidden files configuration const saveHiddenBtn = document.getElementById("cfg-save-hidden-files"); if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings); // AI Keys const saveAIKeysBtn = document.getElementById("cfg-save-ai-keys"); if (saveAIKeysBtn) saveAIKeysBtn.addEventListener("click", saveAIKeys); const testAIKeysBtn = document.getElementById("cfg-test-ai-keys"); if (testAIKeysBtn) testAIKeysBtn.addEventListener("click", testAIKeys); // Load existing keys on modal open document.getElementById("config-open-btn").addEventListener("click", loadAIKeys); // Profile menu button: open config modal and scroll to profile section var profileBtn = document.getElementById("profile-open-btn"); if (profileBtn) { profileBtn.addEventListener("click", function() { var modal = document.getElementById("config-modal"); if (modal) modal.classList.add("active"); setTimeout(function() { var el = document.getElementById("cfg-profile"); if (el) el.scrollIntoView({ behavior: 'smooth' }); }, 300); }); } // Logout button (always visible) var logoutBtn = document.getElementById("logout-btn"); if (logoutBtn) { logoutBtn.addEventListener("click", function() { if (confirm("Voulez-vous vraiment vous deconnecter ?")) { fetch("/api/auth/logout", { method: "POST" }) .then(function() { window.location.href = "/"; }) .catch(function() { window.location.href = "/"; }); } }); } // Config nav search var cfgSearch = document.getElementById("config-nav-search"); if (cfgSearch) { cfgSearch.addEventListener("input", function() { var q = cfgSearch.value.toLowerCase(); document.querySelectorAll("#config-scroll .config-section").forEach(function(s) { var text = s.textContent.toLowerCase(); s.style.display = (q && !text.includes(q)) ? "none" : ""; }); document.querySelectorAll("#config-nav .help-nav-link").forEach(function(a) { var li = a.closest("li"); if (li) li.style.display = (q && !a.textContent.toLowerCase().includes(q)) ? "none" : ""; }); }); } document.addEventListener("keydown", (e) => { if (e.key === "Escape" && modal.classList.contains("active")) { closeConfigModal(); } }); // Load saved frontend config on startup applyFrontendConfig(); // Init theme picker initThemePicker(); // Init profile initProfile(); } function closeConfigModal() { const modal = document.getElementById("config-modal"); if (modal) modal.classList.remove("active"); } // --- Config field helpers --- const _FRONTEND_CONFIG_KEY = "obsigate-perf-config"; function _getFrontendConfig() { try { return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}"); } catch { return {}; } } function applyFrontendConfig() { const cfg = _getFrontendConfig(); if (cfg.debounce_ms) { /* applied dynamically in debounce setTimeout */ } if (cfg.results_per_page) { /* used as ADVANCED_SEARCH_LIMIT override */ } if (cfg.min_query_length) { /* used as MIN_SEARCH_LENGTH override */ } if (cfg.search_timeout_ms) { /* used as SEARCH_TIMEOUT_MS override */ } } export function _getEffective(key, fallback) { const cfg = _getFrontendConfig(); return cfg[key] !== undefined ? cfg[key] : fallback; } async function loadConfigFields() { // Frontend fields from localStorage const cfg = _getFrontendConfig(); _setField("cfg-debounce", cfg.debounce_ms || 300); _setField("cfg-results-per-page", cfg.results_per_page || 50); _setField("cfg-min-query", cfg.min_query_length || 2); _setField("cfg-timeout", cfg.search_timeout_ms || 30000); // Backend fields from API try { const data = await api("/api/config"); _setField("cfg-workers", data.search_workers); _setField("cfg-max-content", data.max_content_size); _setField("cfg-title-boost", data.title_boost); _setField("cfg-tag-boost", data.tag_boost); _setField("cfg-prefix-exp", data.prefix_max_expansions); _setField("cfg-recent-limit", data.recent_files_limit || 20); _setField("cfg-max-backups", data.max_backups_per_file || 10); // Watcher config _setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false); _setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true); _setField("cfg-watcher-interval", data.watcher_polling_interval || 5); _setField("cfg-watcher-debounce", data.watcher_debounce || 2); } catch (err) { console.error("Failed to load backend config:", err); } } function _setField(id, value) { const el = document.getElementById(id); if (el && value !== undefined) el.value = value; } function _setCheckbox(id, checked) { const el = document.getElementById(id); if (el) el.checked = !!checked; } function _getCheckbox(id) { const el = document.getElementById(id); return el ? el.checked : false; } function _getFieldNum(id, fallback) { const el = document.getElementById(id); if (!el) return fallback; const v = parseFloat(el.value); return isNaN(v) ? fallback : v; } function saveFrontendConfig() { const cfg = { debounce_ms: _getFieldNum("cfg-debounce", 300), results_per_page: _getFieldNum("cfg-results-per-page", 50), min_query_length: _getFieldNum("cfg-min-query", 2), search_timeout_ms: _getFieldNum("cfg-timeout", 30000), }; localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg)); showToast(t("toast.config_client_saved"), "success"); } async function saveBackendConfig() { const body = { search_workers: _getFieldNum("cfg-workers", 2), max_content_size: _getFieldNum("cfg-max-content", 100000), title_boost: _getFieldNum("cfg-title-boost", 3.0), tag_boost: _getFieldNum("cfg-tag-boost", 2.0), prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50), recent_files_limit: _getFieldNum("cfg-recent-limit", 20), max_backups_per_file: _getFieldNum("cfg-max-backups", 10), watcher_enabled: _getCheckbox("cfg-watcher-enabled"), watcher_use_polling: _getCheckbox("cfg-watcher-polling"), watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0), watcher_debounce: _getFieldNum("cfg-watcher-debounce", 2.0), }; try { const res = await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (res.ok) { showToast(t("toast.config_saved"), "success"); } else { const errorData = await res.json().catch(() => ({})); showToast(errorData.detail || "Erreur de sauvegarde", "error"); } } catch (err) { console.error("Failed to save backend config:", err); showToast("Erreur de sauvegarde", "error"); } } async function forceReindex() { const btn = document.getElementById("cfg-reindex"); if (btn) { btn.disabled = true; btn.textContent = "Réindexation..."; } try { await api("/api/index/reload"); showToast(t("toast.reindex_done"), "success"); loadDiagnostics(); await Promise.all([loadVaults(), loadTags()]); } catch (err) { console.error("Reindex error:", err); showToast("Erreur de réindexation", "error"); } finally { if (btn) { btn.disabled = false; btn.textContent = "Forcer réindexation"; } } } async function resetConfigDefaults() { // Reset frontend localStorage.removeItem(_FRONTEND_CONFIG_KEY); // Reset backend try { await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ search_workers: 2, debounce_ms: 300, results_per_page: 50, min_query_length: 2, search_timeout_ms: 30000, max_content_size: 100000, title_boost: 3.0, path_boost: 1.5, tag_boost: 2.0, prefix_max_expansions: 50, snippet_context_chars: 120, max_snippet_highlights: 5, }), }); } catch (err) { console.error("Reset config error:", err); } loadConfigFields(); showToast(t("toast.config_reset"), "success"); } async function loadDiagnostics() { const container = document.getElementById("config-diagnostics"); if (!container) return; container.innerHTML = 'Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}
` : 'Sans expiration
'; div.innerHTML = ` `; div.querySelector(".share-copy-btn").addEventListener("click", async () => { try { await navigator.clipboard.writeText(url); } catch (e) { // Fallback for non-HTTPS contexts const ta = document.createElement("textarea"); ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } showToast("Lien copié !", "success"); div.remove(); }); div.querySelector(".share-revoke-btn").addEventListener("click", async () => { try { await api(`/api/share/${existingShare.id}`, { method: "DELETE" }); showToast("Partage révoqué", "success"); existingShare = null; renderContent(); } catch (err) { showToast("Erreur: " + err.message, "error"); } }); } else { div.innerHTML = ` `; div.querySelector(".share-create-btn").addEventListener("click", async () => { try { const expiry = document.getElementById("share-expiry")?.value; const share = await api(`/api/share/${encodeURIComponent(vault)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }), }); existingShare = share; renderContent(); showToast("Lien créé !", "success"); } catch (err) { showToast("Erreur: " + err.message, "error"); } }); } div.querySelector(".share-close-btn").addEventListener("click", () => div.remove()); div.addEventListener("click", (e) => { if (e.target === div) div.remove(); }); }; renderContent(); document.body.appendChild(div); } function renderConfigFilters() { const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; const container = document.getElementById("config-filters-list"); container.innerHTML = ""; filters.forEach((filter, index) => { const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [ el("span", {}, [document.createTextNode(filter.pattern)]), el( "button", { class: "config-filter-toggle", title: filter.enabled ? "Désactiver" : "Activer", type: "button", }, [document.createTextNode(filter.enabled ? "✓" : "○")], ), el( "button", { class: "config-filter-remove", title: "Supprimer", type: "button", }, [document.createTextNode("×")], ), ]); const toggleBtn = badge.querySelector(".config-filter-toggle"); const removeBtn = badge.querySelector(".config-filter-remove"); toggleBtn.addEventListener("click", (e) => { e.stopPropagation(); toggleConfigFilter(index); }); removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeConfigFilter(index); }); container.appendChild(badge); }); } function toggleConfigFilter(index) { const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; if (filters[index]) { filters[index].enabled = !filters[index].enabled; config.tagFilters = filters; TagFilterService.saveConfig(config); renderConfigFilters(); refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); } } function removeConfigFilter(index) { const config = TagFilterService.getConfig(); let filters = config.tagFilters || TagFilterService.defaultFilters; filters = filters.filter((_, i) => i !== index); config.tagFilters = filters; TagFilterService.saveConfig(config); renderConfigFilters(); refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); } function addConfigFilter() { const input = document.getElementById("config-pattern-input"); const pattern = input.value.trim(); if (!pattern) return; const regex = TagFilterService.patternToRegex(pattern); const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; const newFilter = { pattern, regex, enabled: true }; filters.push(newFilter); config.tagFilters = filters; TagFilterService.saveConfig(config); input.value = ""; renderConfigFilters(); refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); updateRegexPreview(); } function updateRegexPreview() { const input = document.getElementById("config-pattern-input"); const preview = document.getElementById("config-regex-preview"); const code = document.getElementById("config-regex-code"); const pattern = input.value.trim(); if (pattern) { const regex = TagFilterService.patternToRegex(pattern); code.textContent = `^${regex}$`; preview.style.display = "block"; } else { preview.style.display = "none"; } } // ── AI Keys management ── async function loadAIKeys() { try { const data = await api("/api/config/ai-keys"); ["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY"].forEach(k => { const id = "cfg-" + k.toLowerCase().replace(/_api_key/g, "") + "-key"; const input = document.getElementById(id); if (input && data[k]) input.placeholder = data[k]; }); } catch(e) {} } async function saveAIKeys() { const keys = {}; const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY" }; for (const [id, name] of Object.entries(map)) { const input = document.getElementById(id); if (input && input.value.trim()) keys[name] = input.value.trim(); } if (!Object.keys(keys).length) { showToast("Aucune cle a sauvegarder", "warning"); return; } try { await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) }); showToast("Cles API sauvegardees", "success"); // Clear inputs Object.keys(map).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; }); loadAIKeys(); } catch(e) { showToast("Erreur: " + e.message, "error"); } } async function testAIKeys() { const status = document.getElementById("cfg-ai-status"); if (status) status.textContent = "Test en cours..."; try { // Test connectivity const results = await api("/api/config/ai-keys/test", { method: "POST" }); var msg = Object.entries(results).map(([k,v]) => k + ": " + v).join(" | "); if (status) status.textContent = msg; // Fetch models for configured providers for (const p of ["deepseek","openrouter","gemini"]) { if (results[p] === "ok") { try { const m = await api("/api/config/ai-models?provider=" + p); var sel = document.getElementById("cfg-" + p + "-model"); if (sel && m.models && m.models.length) { sel.innerHTML = ''; m.models.forEach(function(md) { sel.innerHTML += ''; }); } } catch(e) { console.warn("Failed to fetch models for", p, e); } } } } catch(e) { if (status) status.textContent = "Erreur: " + e.message; } } export { initSidebarTabs, initConfigModal, initConfigModal as initConfigPanel, initHelpModal, closeHelpModal, initRecentTab, initEditorPocBtn, loadAbout as initAboutSection, loadHiddenFilesSettings as initHiddenFilesConfig, initThemePicker, initAboutModal, initProfile, switchSidebarTab, }; // ── Theme Picker ───────────────────────────────────────────────────── function initThemePicker() { import('./themes.js').then(function(mod) { var THEMES = mod.THEMES; var applyTheme = mod.applyTheme; var getCurrentTheme = mod.getCurrentTheme; var getCurrentMode = mod.getCurrentMode; var grid = document.getElementById('theme-grid'); if (!grid) return; var themeKeys = Object.keys(THEMES); function renderCards() { var current = getCurrentTheme(); var mode = getCurrentMode(); grid.innerHTML = ''; themeKeys.forEach(function(key) { var t = THEMES[key]; var v = t.modes[mode]; if (!v) return; var card = document.createElement('div'); card.className = 'theme-card' + (key === current ? ' active' : ''); card.innerHTML = '