// config.js — extracted from app.js (3872-4865) import { api } from './auth.js'; import { state } from './state.js'; import { el, icon, openFile } from './viewer.js'; import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadTags, TagFilterService, refreshSidebarTreePreservingState } from './sidebar.js'; import { escapeHtml, safeCreateIcons } from './utils.js'; import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.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; openBtn.addEventListener("click", () => { modal.classList.add("active"); closeHeaderMenu(); safeCreateIcons(); initHelpNavigation(); }); 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 initHelpNavigation() { const helpContent = document.querySelector(".help-content"); const helpBody = document.getElementById("help-body"); const navLinks = document.querySelectorAll(".help-nav-link"); if (!helpContent || !helpBody || !navLinks.length) return; // Handle nav link clicks navLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const targetId = link.getAttribute("href").substring(1); const targetSection = document.getElementById(targetId); if (targetSection) { targetSection.scrollIntoView({ behavior: "smooth", block: "start" }); } }); }); // Scroll spy - update active nav link based on scroll position const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const id = entry.target.getAttribute("id"); navLinks.forEach((link) => { if (link.getAttribute("href") === `#${id}`) { navLinks.forEach((l) => l.classList.remove("active")); link.classList.add("active"); } }); } }); }, { root: helpBody, rootMargin: "-20% 0px -70% 0px", threshold: 0, }, ); // Observe all sections document.querySelectorAll(".help-section").forEach((section) => { observer.observe(section); }); } 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); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && modal.classList.contains("active")) { closeConfigModal(); } }); // Load saved frontend config on startup applyFrontendConfig(); } 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); // 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("Paramètres client sauvegardés", "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), 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("Configuration backend sauvegardée", "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("Réindexation terminée", "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("Configuration réinitialisée", "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"; } } export { initSidebarTabs, initConfigModal, initConfigModal as initConfigPanel, initHelpModal, closeHelpModal, initRecentTab, loadAbout as initAboutSection, loadHiddenFilesSettings as initHiddenFilesConfig, switchSidebarTab, };