// 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 = '
Chargement...
'; try { const data = await api("/api/diagnostics"); renderDiagnostics(container, data); } catch (err) { container.innerHTML = '
Erreur de chargement
'; } } function renderDiagnostics(container, data) { container.innerHTML = ""; const sections = [ { title: "Index", rows: [ [t("about.files_indexed"), data.index.total_files], ["Tags uniques", data.index.total_tags], ["Vaults", Object.keys(data.index.vaults).join(", ")], ], }, { title: "Index inversé", rows: [ ["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()], ["Postings total", data.inverted_index.total_postings.toLocaleString()], ["Documents", data.inverted_index.documents], ["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"], ["Stale", data.inverted_index.is_stale ? "Oui" : "Non"], ], }, { title: "Moteur de recherche", rows: [ ["Executor actif", data.search_executor.active ? "Oui" : "Non"], ["Workers max", data.search_executor.max_workers], ], }, ]; sections.forEach((section) => { const div = document.createElement("div"); div.className = "config-diag-section"; const title = document.createElement("div"); title.className = "config-diag-section-title"; title.textContent = section.title; div.appendChild(title); section.rows.forEach(([label, value]) => { const row = document.createElement("div"); row.className = "config-diag-row"; row.innerHTML = `${label}${value}`; div.appendChild(row); }); container.appendChild(div); }); } // --- About Section --- function loadAbout() { const container = document.getElementById("config-about"); if (!container) return; // Fetch health info for version api("/api/health").then((health) => { container.innerHTML = ""; const sections = [ { title: t("about.section_app"), rows: [ [t("about.name"), "ObsiGate"], [t("about.version"), state.APP_VERSION], [t("about.api_version"), health.version || "—"], [t("about.status"), health.status || "—"], ], }, { title: t("about.section_env"), rows: [ [t("about.vaults_configured"), health.vaults || "—"], [t("about.files_indexed"), health.total_files || "—"], [t("about.browser"), navigator.userAgent.split(" ").pop()], [t("about.platform"), navigator.platform || "—"], [t("about.language"), navigator.language || "—"], ], }, { title: t("about.section_components"), rows: [ [t("about.backend"), "FastAPI (Python)"], [t("about.markdown_renderer"), "mistune"], [t("about.file_watcher"), "watchdog"], [t("about.frontend"), "Vanilla JavaScript"], [t("about.icons"), "Lucide Icons"], [t("about.syntax_highlight"), "highlight.js"], [t("about.editor"), "CodeMirror 6"], ], }, ]; sections.forEach((section) => { const div = document.createElement("div"); div.className = "config-diag-section"; const title = document.createElement("div"); title.className = "config-diag-section-title"; title.textContent = section.title; div.appendChild(title); section.rows.forEach(([label, value]) => { const row = document.createElement("div"); row.className = "config-diag-row"; row.innerHTML = `${label}${value}`; div.appendChild(row); }); container.appendChild(div); }); }).catch(() => { container.innerHTML = '
Erreur de chargement
'; }); } // --- Hidden Files Configuration --- async function loadHiddenFilesSettings() { const container = document.getElementById("hidden-files-vault-list"); if (!container) return; container.innerHTML = '
Chargement...
'; try { const settings = await api("/api/vaults/settings/all"); renderHiddenFilesSettings(container, settings); } catch (err) { console.error("Failed to load hidden files settings:", err); container.innerHTML = '
Erreur de chargement
'; } } function renderHiddenFilesSettings(container, allSettings) { container.innerHTML = ""; if (!state.allVaults || state.allVaults.length === 0) { container.innerHTML = '
Aucun vault configuré
'; return; } state.allVaults.forEach((vault) => { const settings = allSettings[vault.name] || { hideHiddenFiles: false }; const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name }); // Vault header const header = el("div", { class: "hidden-files-vault-header" }, [el("h3", {}, [document.createTextNode(vault.name)]), el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")])]); // Hide hidden files toggle const toggleRow = el("div", { class: "config-row" }, [ el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [document.createTextNode("Masquer les fichiers/dossiers cachés")]), el("label", { class: "config-toggle" }, [ el("input", { type: "checkbox", id: `hide-hidden-${vault.name}`, "data-vault": vault.name, checked: settings.hideHiddenFiles ? "true" : false, }), el("span", { class: "config-toggle-slider" }), ]), el("span", { class: "config-hint" }, [document.createTextNode("Masquer les fichiers/dossiers commençant par un point dans l'interface (ils restent indexés et cherchables)")]), ]); vaultCard.appendChild(header); vaultCard.appendChild(toggleRow); container.appendChild(vaultCard); }); } async function saveHiddenFilesSettings() { const btn = document.getElementById("cfg-save-hidden-files"); if (btn) { btn.disabled = true; btn.textContent = "Sauvegarde..."; } try { const vaultCards = document.querySelectorAll(".hidden-files-vault-card"); const promises = []; vaultCards.forEach((card) => { const vaultName = card.dataset.vault; const hideHiddenFiles = document.getElementById(`hide-hidden-${vaultName}`)?.checked || false; const settings = { hideHiddenFiles, }; promises.push( api(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings), }), ); }); await Promise.all(promises); // Reload vault settings to update the cache await loadVaultSettings(); showToast("✓ Paramètres sauvegardés", "success"); // Refresh the UI to apply the filter await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]); } catch (err) { console.error("Failed to save hidden files settings:", err); const errorMsg = err.message || "Erreur inconnue"; showToast(`Erreur: ${errorMsg}`, "error"); } finally { if (btn) { btn.disabled = false; btn.textContent = "💾 Sauvegarder"; } } } // ── Webhooks UI ── async function loadWebhooksUI() { const list = document.getElementById("webhooks-list"); if (!list) return; try { const webhooks = await api("/api/webhooks"); renderWebhooksUI(webhooks); } catch { list.innerHTML = '
Admin uniquement
'; } } function renderWebhooksUI(webhooks) { const list = document.getElementById("webhooks-list"); if (!list) return; if (!webhooks.length) { list.innerHTML = '
Aucun webhook configuré.
'; return; } list.innerHTML = webhooks.map(w => `
${escapeHtml(w.name)} ${escapeHtml(w.url)} ${(w.events||[]).join(", ")}
`).join(""); list.querySelectorAll(".webhook-delete").forEach(b => b.addEventListener("click", async () => { await api(`/api/webhooks/${b.dataset.id}`, { method: "DELETE" }); loadWebhooksUI(); })); } document.addEventListener("click", function(e) { if (e.target.id === "webhook-add-btn") { const name = document.getElementById("webhook-name-input").value.trim(); const url = document.getElementById("webhook-url-input").value.trim(); if (!url) { showToast("URL requise", "error"); return; } api("/api/webhooks", { method: "POST", body: JSON.stringify({ name: name || "Webhook", url, events: ["file_created","file_deleted","file_modified","file_renamed"] }) }).then(() => { loadWebhooksUI(); document.getElementById("webhook-name-input").value = ""; document.getElementById("webhook-url-input").value = ""; }).catch(err => showToast(err.message, "error")); } }); // ── Shares UI ── async function loadSharesUI() { const list = document.getElementById("shares-list"); if (!list) return; try { const shares = await api("/api/shares"); renderSharesUI(shares); } catch { list.innerHTML = '
Chargement...
'; } } function renderSharesUI(shares) { const list = document.getElementById("shares-list"); if (!list) return; if (!shares.length) { list.innerHTML = '
Aucun partage actif.
'; return; } list.innerHTML = shares.map(s => `
${escapeHtml(s.vault)}/${escapeHtml(s.path)} ${s.url} ${s.access_count} vue(s)${s.expires_at ? ' · Expire' : ''}
`).join(""); list.querySelectorAll(".share-revoke").forEach(b => b.addEventListener("click", async () => { await api(`/api/share/${b.dataset.id}`, { method: "DELETE" }); loadSharesUI(); })); } // ── Share Dialog (professional) ── export async function openShareDialog(vault, path) { // First check if already shared let existingShare = null; try { const shares = await api("/api/shares"); existingShare = shares.find(s => s.vault === vault && s.path === path); } catch (e) { /* ignore */ } const div = document.createElement("div"); div.className = "share-dialog-overlay"; const renderContent = () => { if (existingShare) { const url = window.location.origin + existingShare.url; const expiresInfo = existingShare.expires_at ? `

Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}

` : '

Sans expiration

'; div.innerHTML = `

📤 Document partagé

${escapeHtml(vault)}/${escapeHtml(path)}

${expiresInfo}

${existingShare.access_count} vue(s)

`; 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 = `

📤 Partager ce document

${escapeHtml(vault)}/${escapeHtml(path)}

Ce lien sera accessible publiquement, sans authentification.

`; 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 = '
' + '
' + '
' + '
' + '
' + '
' + '
' + t.name + '
' + '
' + t.desc + '
'; card.addEventListener('click', function() { applyTheme(key, mode); renderCards(); }); grid.appendChild(card); }); // Mode toggle var toggle = document.createElement('div'); toggle.className = 'theme-mode-toggle'; var dBtn = elBtn('Sombre', 'dark', mode); var lBtn = elBtn('Clair', 'light', mode); toggle.appendChild(dBtn); toggle.appendChild(lBtn); grid.appendChild(toggle); } function elBtn(label, m, currentMode) { var btn = document.createElement('button'); btn.className = 'theme-mode-btn' + (m === currentMode ? ' active' : ''); btn.textContent = label; btn.addEventListener('click', function() { applyTheme(getCurrentTheme(), m); renderCards(); }); return btn; } renderCards(); }); } // ── About Modal ────────────────────────────────────────────────────── function initAboutModal() { var overlay = document.getElementById('about-overlay'); var closeBtn = document.getElementById('about-close'); if (!overlay) return; // Open button var openBtn = document.getElementById('about-open-btn'); if (openBtn) { openBtn.addEventListener('click', function() { overlay.classList.add('active'); populateAbout(); }); } // Close closeBtn.addEventListener('click', function() { overlay.classList.remove('active'); }); overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.classList.remove('active'); }); document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.classList.contains('active')) overlay.classList.remove('active'); }); function populateAbout() { // Live stats from health endpoint fetch('/api/health') .then(function(r) { return r.json(); }) .then(function(d) { var fEl = document.getElementById('about-stat-files'); var vEl = document.getElementById('about-stat-vaults'); if (fEl) fEl.textContent = d.total_files || '—'; if (vEl) vEl.textContent = d.vaults || '—'; }) .catch(function() {}); // Build date (today) var now = new Date(); var months = ['Janvier','Fevrier','Mars','Avril','Mai','Juin','Juillet','Aout','Septembre','Octobre','Novembre','Decembre']; var dateEl = document.getElementById('about-build-date'); if (dateEl) dateEl.textContent = months[now.getMonth()] + ' ' + now.getFullYear(); // Random joke var jokes = [ '0 bug connu. Les autres sont timides.', 'Compile du premier coup. (Sauf le lundi matin.)', 'Ecrit par un humain. Probablement.', 'Aucun LLM n\'a ete blesse durant ce developpement.', 'Quand ca marche, on appelle ca une feature.', 'CSS: 90% du temps de dev, 10% du code.', 'Dark mode active depuis la naissance du projet.', 'Ca marche sur ma machine™', 'Techniquement, tout est un edge case.', 'Le CSS de ce panneau a pris plus de temps que le backend.' ]; var jokeEl = document.getElementById('about-joke'); if (jokeEl) jokeEl.textContent = '« ' + jokes[Math.floor(Math.random() * jokes.length)] + ' »'; } } // ── Profile ────────────────────────────────────────────────────────── function initProfile() { var langSel = document.getElementById('profile-lang'); var nameInp = document.getElementById('profile-name'); var saveBtn = document.getElementById('profile-save'); var logoutBtn = document.getElementById('profile-logout'); var savedEl = document.getElementById('profile-saved'); if (!langSel || !saveBtn) return; // Load saved profile try { var lang = getLocale(); var name = localStorage.getItem('obsigate-name') || ''; if (langSel) langSel.value = lang; if (nameInp) nameInp.value = name; } catch(e) {} saveBtn.addEventListener('click', async function() { try { var newLang = langSel.value; await setLocale(newLang); // Live-reload all translations if (nameInp) localStorage.setItem('obsigate-name', nameInp.value.trim()); if (savedEl) { savedEl.style.display = 'inline'; setTimeout(function() { savedEl.style.display = 'none'; }, 2000); } } catch(e) {} }); // Logout button if (logoutBtn) { logoutBtn.addEventListener('click', function() { if (confirm(t('confirm.logout'))) { fetch('/api/auth/logout', { method: 'POST' }) .then(function() { window.location.href = '/'; }) .catch(function() { window.location.href = '/'; }); } }); } }