diff --git a/frontend/app.js b/frontend/app.js index a240851..67c852f 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -24,7 +24,7 @@ let _iconDebounceTimer = null; let activeSidebarTab = "vaults"; let filterDebounce = null; - + // Vault settings cache for hideHiddenFiles let vaultSettings = {}; @@ -111,29 +111,39 @@ try { const raw = localStorage.getItem(SEARCH_HISTORY_KEY); return raw ? JSON.parse(raw) : []; - } catch { return []; } + } catch { + return []; + } }, _save(entries) { - try { localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries)); } catch {} + try { + localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries)); + } catch {} + }, + getAll() { + return this._load(); }, - getAll() { return this._load(); }, add(query) { if (!query || !query.trim()) return; const q = query.trim(); - let entries = this._load().filter(e => e !== q); + let entries = this._load().filter((e) => e !== q); entries.unshift(q); if (entries.length > MAX_HISTORY_ENTRIES) entries = entries.slice(0, MAX_HISTORY_ENTRIES); this._save(entries); }, remove(query) { - const entries = this._load().filter(e => e !== query); + const entries = this._load().filter((e) => e !== query); this._save(entries); }, - clear() { this._save([]); }, + clear() { + this._save([]); + }, filter(prefix) { if (!prefix) return this.getAll().slice(0, 8); const lp = prefix.toLowerCase(); - return this._load().filter(e => e.toLowerCase().includes(lp)).slice(0, 8); + return this._load() + .filter((e) => e.toLowerCase().includes(lp)) + .slice(0, 8); }, }; @@ -176,8 +186,11 @@ if (raw[i] !== '"') { let j = i; while (j < n && raw[j] !== " ") { - if (raw[j] === '"') { j++; while (j < n && raw[j] !== '"') j++; if (j < n) j++; } - else j++; + if (raw[j] === '"') { + j++; + while (j < n && raw[j] !== '"') j++; + if (j < n) j++; + } else j++; } tokens.push(raw.slice(i, j).replace(/"/g, "")); i = j; @@ -243,8 +256,7 @@ // Close dropdown on outside click document.addEventListener("click", (e) => { - if (this._dropdown && !this._dropdown.contains(e.target) && - e.target.id !== "search-input") { + if (this._dropdown && !this._dropdown.contains(e.target) && e.target.id !== "search-input") { this.hide(); } }); @@ -267,7 +279,10 @@ /** Populate and show the dropdown with history, title suggestions, and tag suggestions */ async populate(inputValue, cursorPos) { // Cancel previous suggestion request - if (suggestAbortController) { suggestAbortController.abort(); suggestAbortController = null; } + if (suggestAbortController) { + suggestAbortController.abort(); + suggestAbortController = null; + } const ctx = QueryParser.getContext(inputValue, cursorPos); const vault = document.getElementById("vault-filter").value; @@ -304,7 +319,7 @@ try { const [titlesRes, tagsRes] = await Promise.all([ ctx.type !== "tag" ? api(`/api/suggest?q=${encodeURIComponent(ctx.prefix)}&vault=${encodeURIComponent(vault)}&limit=8`, { signal: suggestAbortController.signal }) : Promise.resolve({ suggestions: [] }), - (ctx.type === "tag" || ctx.type === "text") ? api(`/api/tags/suggest?q=${encodeURIComponent(ctx.prefix)}&vault=${encodeURIComponent(vault)}&limit=6`, { signal: suggestAbortController.signal }) : Promise.resolve({ suggestions: [] }), + ctx.type === "tag" || ctx.type === "text" ? api(`/api/tags/suggest?q=${encodeURIComponent(ctx.prefix)}&vault=${encodeURIComponent(vault)}&limit=6`, { signal: suggestAbortController.signal }) : Promise.resolve({ suggestions: [] }), ]); this._renderTitles(titlesRes.suggestions || [], ctx.prefix); @@ -319,7 +334,8 @@ const historyVisible = !this._historySection.hidden; const hasAny = historyVisible || hasTitles || hasTags; this._emptyEl.hidden = hasAny; - if (hasAny) this.show(); else if (!historyVisible) this.hide(); + if (hasAny) this.show(); + else if (!historyVisible) this.hide(); this._collectItems(); } catch (err) { @@ -340,7 +356,7 @@ li.addEventListener("click", () => { const input = document.getElementById("search-input"); input.value = entry; - input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event("input", { bubbles: true })); this.hide(); _triggerAdvancedSearch(entry); }); @@ -402,7 +418,7 @@ } else { input.value = (current ? current + " " : "") + "tag:" + item.tag + " "; } - input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event("input", { bubbles: true })); this.hide(); input.focus(); _triggerAdvancedSearch(input.value); @@ -415,7 +431,10 @@ const lower = text.toLowerCase(); const needle = query.toLowerCase(); const pos = lower.indexOf(needle); - if (pos === -1) { container.textContent = text; return; } + if (pos === -1) { + container.textContent = text; + return; + } container.appendChild(document.createTextNode(text.slice(0, pos))); const markEl = el("mark", {}, [document.createTextNode(text.slice(pos, pos + query.length))]); container.appendChild(markEl); @@ -425,7 +444,7 @@ _collectItems() { dropdownItems = Array.from(this._dropdown.querySelectorAll(".search-dropdown__item")); dropdownActiveIndex = -1; - dropdownItems.forEach(item => item.classList.remove("active")); + dropdownItems.forEach((item) => item.classList.remove("active")); }, navigateDown() { @@ -458,15 +477,29 @@ // --------------------------------------------------------------------------- const SearchChips = { _container: null, - init() { this._container = document.getElementById("search-chips"); }, + init() { + this._container = document.getElementById("search-chips"); + }, update(parsed) { if (!this._container) return; this._container.innerHTML = ""; let hasChips = false; - parsed.tags.forEach(tag => { this._addChip("tag", `tag:${tag}`, tag); hasChips = true; }); - if (parsed.vault) { this._addChip("vault", `vault:${parsed.vault}`, parsed.vault); hasChips = true; } - if (parsed.title) { this._addChip("title", `title:${parsed.title}`, parsed.title); hasChips = true; } - if (parsed.path) { this._addChip("path", `path:${parsed.path}`, parsed.path); hasChips = true; } + parsed.tags.forEach((tag) => { + this._addChip("tag", `tag:${tag}`, tag); + hasChips = true; + }); + if (parsed.vault) { + this._addChip("vault", `vault:${parsed.vault}`, parsed.vault); + hasChips = true; + } + if (parsed.title) { + this._addChip("title", `title:${parsed.title}`, parsed.title); + hasChips = true; + } + if (parsed.path) { + this._addChip("path", `path:${parsed.path}`, parsed.path); + hasChips = true; + } this._container.hidden = !hasChips; }, clear() { @@ -526,7 +559,11 @@ if (_iconDebounceTimer) return; // already scheduled _iconDebounceTimer = requestAnimationFrame(() => { _iconDebounceTimer = null; - try { lucide.createIcons(); } catch (e) { /* CDN not loaded */ } + try { + lucide.createIcons(); + } catch (e) { + /* CDN not loaded */ + } }); } @@ -537,13 +574,21 @@ _iconDebounceTimer = null; } if (typeof lucide !== "undefined" && lucide.createIcons) { - try { lucide.createIcons(); } catch (e) { /* CDN not loaded */ } + try { + lucide.createIcons(); + } catch (e) { + /* CDN not loaded */ + } } } function safeHighlight(block) { if (typeof hljs !== "undefined" && hljs.highlightElement) { - try { hljs.highlightElement(block); } catch (e) { /* CDN not loaded */ } + try { + hljs.highlightElement(block); + } catch (e) { + /* CDN not loaded */ + } } } @@ -556,26 +601,28 @@ * Slugify text to create valid IDs */ slugify(text) { - return text - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim() || 'heading'; + return ( + text + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .trim() || "heading" + ); }, /** * Parse headings from markdown content */ parseHeadings() { - const contentArea = document.querySelector('.md-content'); + const contentArea = document.querySelector(".md-content"); if (!contentArea) return []; const headings = []; - const h2s = contentArea.querySelectorAll('h2'); - const h3s = contentArea.querySelectorAll('h3'); + const h2s = contentArea.querySelectorAll("h2"); + const h3s = contentArea.querySelectorAll("h3"); const allHeadings = [...h2s, ...h3s].sort((a, b) => { return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; }); @@ -609,7 +656,7 @@ id, level, text, - element: heading + element: heading, }); }); @@ -620,12 +667,12 @@ * Render outline list */ renderOutline(headings) { - const outlineList = document.getElementById('outline-list'); - const outlineEmpty = document.getElementById('outline-empty'); + const outlineList = document.getElementById("outline-list"); + const outlineEmpty = document.getElementById("outline-empty"); if (!outlineList) return; - outlineList.innerHTML = ''; + outlineList.innerHTML = ""; if (!headings || headings.length === 0) { outlineList.hidden = true; @@ -640,14 +687,18 @@ if (outlineEmpty) outlineEmpty.hidden = true; headings.forEach((heading) => { - const item = el('a', { - class: `outline-item level-${heading.level}`, - href: `#${heading.id}`, - 'data-heading-id': heading.id, - role: 'link' - }, [document.createTextNode(heading.text)]); + const item = el( + "a", + { + class: `outline-item level-${heading.level}`, + href: `#${heading.id}`, + "data-heading-id": heading.id, + role: "link", + }, + [document.createTextNode(heading.text)], + ); - item.addEventListener('click', (e) => { + item.addEventListener("click", (e) => { e.preventDefault(); this.scrollToHeading(heading.id); }); @@ -665,16 +716,16 @@ const heading = document.getElementById(headingId); if (!heading) return; - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) return; // Calculate offset for fixed header (if any) const headerHeight = 80; const headingTop = heading.offsetTop; - + contentArea.scrollTo({ top: headingTop - headerHeight, - behavior: 'smooth' + behavior: "smooth", }); // Update active state immediately @@ -689,16 +740,16 @@ activeHeadingId = headingId; - const items = document.querySelectorAll('.outline-item'); + const items = document.querySelectorAll(".outline-item"); items.forEach((item) => { - if (item.getAttribute('data-heading-id') === headingId) { - item.classList.add('active'); - item.setAttribute('aria-current', 'location'); + if (item.getAttribute("data-heading-id") === headingId) { + item.classList.add("active"); + item.setAttribute("aria-current", "location"); // Scroll outline item into view - item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + item.scrollIntoView({ block: "nearest", behavior: "smooth" }); } else { - item.classList.remove('active'); - item.removeAttribute('aria-current'); + item.classList.remove("active"); + item.removeAttribute("aria-current"); } }); }, @@ -721,7 +772,7 @@ ReadingProgressManager.destroy(); headingsCache = []; activeHeadingId = null; - } + }, }; // --------------------------------------------------------------------------- @@ -738,13 +789,13 @@ if (!headings || headings.length === 0) return; - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) return; const options = { root: contentArea, - rootMargin: '-20% 0px -70% 0px', - threshold: [0, 0.3, 0.5, 1.0] + rootMargin: "-20% 0px -70% 0px", + threshold: [0, 0.3, 0.5, 1.0], }; this.observer = new IntersectionObserver((entries) => { @@ -778,7 +829,7 @@ this.observer = null; } this.headings = []; - } + }, }; // --------------------------------------------------------------------------- @@ -791,21 +842,21 @@ init() { this.destroy(); - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (!contentArea) return; this.scrollHandler = this.throttle(() => { this.updateProgress(); }, 100); - contentArea.addEventListener('scroll', this.scrollHandler); + contentArea.addEventListener("scroll", this.scrollHandler); this.updateProgress(); }, updateProgress() { - const contentArea = document.getElementById('content-area'); - const progressFill = document.getElementById('reading-progress-fill'); - const progressText = document.getElementById('reading-progress-text'); + const contentArea = document.getElementById("content-area"); + const progressFill = document.getElementById("reading-progress-fill"); + const progressText = document.getElementById("reading-progress-text"); if (!contentArea || !progressFill || !progressText) return; @@ -822,7 +873,7 @@ throttle(func, delay) { let lastCall = 0; - return function(...args) { + return function (...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; @@ -832,18 +883,18 @@ }, destroy() { - const contentArea = document.getElementById('content-area'); + const contentArea = document.getElementById("content-area"); if (contentArea && this.scrollHandler) { - contentArea.removeEventListener('scroll', this.scrollHandler); + contentArea.removeEventListener("scroll", this.scrollHandler); } this.scrollHandler = null; // Reset progress - const progressFill = document.getElementById('reading-progress-fill'); - const progressText = document.getElementById('reading-progress-text'); - if (progressFill) progressFill.style.width = '0%'; - if (progressText) progressText.textContent = '0%'; - } + const progressFill = document.getElementById("reading-progress-fill"); + const progressText = document.getElementById("reading-progress-text"); + if (progressFill) progressFill.style.width = "0%"; + if (progressText) progressText.textContent = "0%"; + }, }; // --------------------------------------------------------------------------- @@ -858,11 +909,11 @@ }, loadState() { - const savedVisible = localStorage.getItem('obsigate-right-sidebar-visible'); - const savedWidth = localStorage.getItem('obsigate-right-sidebar-width'); + const savedVisible = localStorage.getItem("obsigate-right-sidebar-visible"); + const savedWidth = localStorage.getItem("obsigate-right-sidebar-width"); if (savedVisible !== null) { - rightSidebarVisible = savedVisible === 'true'; + rightSidebarVisible = savedVisible === "true"; } if (savedWidth) { @@ -873,58 +924,58 @@ }, applyState() { - const sidebar = document.getElementById('right-sidebar'); - const handle = document.getElementById('right-sidebar-resize-handle'); - const tocBtn = document.getElementById('toc-toggle-btn'); - const headerToggleBtn = document.getElementById('right-sidebar-toggle-btn'); + const sidebar = document.getElementById("right-sidebar"); + const handle = document.getElementById("right-sidebar-resize-handle"); + const tocBtn = document.getElementById("toc-toggle-btn"); + const headerToggleBtn = document.getElementById("right-sidebar-toggle-btn"); if (!sidebar) return; if (rightSidebarVisible) { - sidebar.classList.remove('hidden'); + sidebar.classList.remove("hidden"); sidebar.style.width = `${rightSidebarWidth}px`; - if (handle) handle.classList.remove('hidden'); + if (handle) handle.classList.remove("hidden"); if (tocBtn) { - tocBtn.classList.add('active'); - tocBtn.title = 'Masquer le sommaire'; + tocBtn.classList.add("active"); + tocBtn.title = "Masquer le sommaire"; } if (headerToggleBtn) { - headerToggleBtn.title = 'Masquer le panneau'; - headerToggleBtn.setAttribute('aria-label', 'Masquer le panneau'); + headerToggleBtn.title = "Masquer le panneau"; + headerToggleBtn.setAttribute("aria-label", "Masquer le panneau"); } } else { - sidebar.classList.add('hidden'); - if (handle) handle.classList.add('hidden'); + sidebar.classList.add("hidden"); + if (handle) handle.classList.add("hidden"); if (tocBtn) { - tocBtn.classList.remove('active'); - tocBtn.title = 'Afficher le sommaire'; + tocBtn.classList.remove("active"); + tocBtn.title = "Afficher le sommaire"; } if (headerToggleBtn) { - headerToggleBtn.title = 'Afficher le panneau'; - headerToggleBtn.setAttribute('aria-label', 'Afficher le panneau'); + headerToggleBtn.title = "Afficher le panneau"; + headerToggleBtn.setAttribute("aria-label", "Afficher le panneau"); } } - + // Update icons safeCreateIcons(); }, toggle() { rightSidebarVisible = !rightSidebarVisible; - localStorage.setItem('obsigate-right-sidebar-visible', rightSidebarVisible); + localStorage.setItem("obsigate-right-sidebar-visible", rightSidebarVisible); this.applyState(); }, initToggle() { - const toggleBtn = document.getElementById('right-sidebar-toggle-btn'); + const toggleBtn = document.getElementById("right-sidebar-toggle-btn"); if (toggleBtn) { - toggleBtn.addEventListener('click', () => this.toggle()); + toggleBtn.addEventListener("click", () => this.toggle()); } }, initResize() { - const handle = document.getElementById('right-sidebar-resize-handle'); - const sidebar = document.getElementById('right-sidebar'); + const handle = document.getElementById("right-sidebar-resize-handle"); + const sidebar = document.getElementById("right-sidebar"); if (!handle || !sidebar) return; @@ -936,9 +987,9 @@ isResizing = true; startX = e.clientX; startWidth = sidebar.offsetWidth; - handle.classList.add('active'); - document.body.style.cursor = 'ew-resize'; - document.body.style.userSelect = 'none'; + handle.classList.add("active"); + document.body.style.cursor = "ew-resize"; + document.body.style.userSelect = "none"; }; const onMouseMove = (e) => { @@ -958,17 +1009,17 @@ if (!isResizing) return; isResizing = false; - handle.classList.remove('active'); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; + handle.classList.remove("active"); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; - localStorage.setItem('obsigate-right-sidebar-width', rightSidebarWidth); + localStorage.setItem("obsigate-right-sidebar-width", rightSidebarWidth); }; - handle.addEventListener('mousedown', onMouseDown); - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - } + handle.addEventListener("mousedown", onMouseDown); + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }, }; // --------------------------------------------------------------------------- @@ -1047,30 +1098,30 @@ // Custom Dropdowns // --------------------------------------------------------------------------- function initCustomDropdowns() { - document.querySelectorAll('.custom-dropdown').forEach(dropdown => { - const trigger = dropdown.querySelector('.custom-dropdown-trigger'); - const options = dropdown.querySelectorAll('.custom-dropdown-option'); + document.querySelectorAll(".custom-dropdown").forEach((dropdown) => { + const trigger = dropdown.querySelector(".custom-dropdown-trigger"); + const options = dropdown.querySelectorAll(".custom-dropdown-option"); const hiddenInput = dropdown.querySelector('input[type="hidden"]'); - const selectedText = dropdown.querySelector('.custom-dropdown-selected'); - const menu = dropdown.querySelector('.custom-dropdown-menu'); + const selectedText = dropdown.querySelector(".custom-dropdown-selected"); + const menu = dropdown.querySelector(".custom-dropdown-menu"); if (!trigger) return; // Toggle dropdown - trigger.addEventListener('click', (e) => { + trigger.addEventListener("click", (e) => { e.stopPropagation(); - const isOpen = dropdown.classList.contains('open'); - + const isOpen = dropdown.classList.contains("open"); + // Close all other dropdowns - document.querySelectorAll('.custom-dropdown.open').forEach(d => { - if (d !== dropdown) d.classList.remove('open'); + document.querySelectorAll(".custom-dropdown.open").forEach((d) => { + if (d !== dropdown) d.classList.remove("open"); }); - - dropdown.classList.toggle('open', !isOpen); - trigger.setAttribute('aria-expanded', !isOpen); + + dropdown.classList.toggle("open", !isOpen); + trigger.setAttribute("aria-expanded", !isOpen); // Position fixed menu for sidebar dropdowns - if (!isOpen && dropdown.classList.contains('sidebar-dropdown') && menu) { + if (!isOpen && dropdown.classList.contains("sidebar-dropdown") && menu) { const rect = trigger.getBoundingClientRect(); menu.style.top = `${rect.bottom + 4}px`; menu.style.left = `${rect.left}px`; @@ -1079,17 +1130,17 @@ }); // Handle option selection - options.forEach(option => { - option.addEventListener('click', (e) => { + options.forEach((option) => { + option.addEventListener("click", (e) => { e.stopPropagation(); - const value = option.getAttribute('data-value'); + const value = option.getAttribute("data-value"); const text = option.textContent; // Update hidden input if (hiddenInput) { hiddenInput.value = value; // Trigger change event - hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); } // Update selected text @@ -1098,22 +1149,22 @@ } // Update visual selection - options.forEach(opt => opt.classList.remove('selected')); - option.classList.add('selected'); + options.forEach((opt) => opt.classList.remove("selected")); + option.classList.add("selected"); // Close dropdown - dropdown.classList.remove('open'); - trigger.setAttribute('aria-expanded', 'false'); + dropdown.classList.remove("open"); + trigger.setAttribute("aria-expanded", "false"); }); }); }); // Close dropdowns when clicking outside - document.addEventListener('click', () => { - document.querySelectorAll('.custom-dropdown.open').forEach(dropdown => { - dropdown.classList.remove('open'); - const trigger = dropdown.querySelector('.custom-dropdown-trigger'); - if (trigger) trigger.setAttribute('aria-expanded', 'false'); + document.addEventListener("click", () => { + document.querySelectorAll(".custom-dropdown.open").forEach((dropdown) => { + dropdown.classList.remove("open"); + const trigger = dropdown.querySelector(".custom-dropdown-trigger"); + if (trigger) trigger.setAttribute("aria-expanded", "false"); }); }); } @@ -1123,24 +1174,24 @@ const dropdown = document.getElementById(dropdownId); if (!dropdown) return; - const optionsContainer = dropdown.querySelector('.custom-dropdown-menu'); + const optionsContainer = dropdown.querySelector(".custom-dropdown-menu"); const hiddenInput = dropdown.querySelector('input[type="hidden"]'); - const selectedText = dropdown.querySelector('.custom-dropdown-selected'); + const selectedText = dropdown.querySelector(".custom-dropdown-selected"); if (!optionsContainer) return; // Clear existing options (keep the first one if it's the default) - optionsContainer.innerHTML = ''; + optionsContainer.innerHTML = ""; // Add new options - optionsList.forEach(opt => { - const li = document.createElement('li'); - li.className = 'custom-dropdown-option'; - li.setAttribute('role', 'option'); - li.setAttribute('data-value', opt.value); + optionsList.forEach((opt) => { + const li = document.createElement("li"); + li.className = "custom-dropdown-option"; + li.setAttribute("role", "option"); + li.setAttribute("data-value", opt.value); li.textContent = opt.text; if (opt.value === defaultValue) { - li.classList.add('selected'); + li.classList.add("selected"); if (selectedText) selectedText.textContent = opt.text; if (hiddenInput) hiddenInput.value = opt.value; } @@ -1148,27 +1199,27 @@ }); // Re-initialize click handlers - optionsContainer.querySelectorAll('.custom-dropdown-option').forEach(option => { - option.addEventListener('click', (e) => { + optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((option) => { + option.addEventListener("click", (e) => { e.stopPropagation(); - const value = option.getAttribute('data-value'); + const value = option.getAttribute("data-value"); const text = option.textContent; if (hiddenInput) { hiddenInput.value = value; - hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); + hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); } if (selectedText) { selectedText.textContent = text; } - optionsContainer.querySelectorAll('.custom-dropdown-option').forEach(opt => opt.classList.remove('selected')); - option.classList.add('selected'); + optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((opt) => opt.classList.remove("selected")); + option.classList.add("selected"); - dropdown.classList.remove('open'); - const trigger = dropdown.querySelector('.custom-dropdown-trigger'); - if (trigger) trigger.setAttribute('aria-expanded', 'false'); + dropdown.classList.remove("open"); + const trigger = dropdown.querySelector(".custom-dropdown-trigger"); + if (trigger) trigger.setAttribute("aria-expanded", "false"); }); }); } @@ -1194,10 +1245,14 @@ toast.textContent = message; container.appendChild(toast); // Trigger entrance animation - requestAnimationFrame(function () { toast.classList.add("show"); }); + requestAnimationFrame(function () { + toast.classList.add("show"); + }); setTimeout(function () { toast.classList.remove("show"); - toast.addEventListener("transitionend", function () { toast.remove(); }); + toast.addEventListener("transitionend", function () { + toast.remove(); + }); }, 3500); } @@ -1222,7 +1277,7 @@ if (authHeaders) { mergedOpts.headers = { ...mergedOpts.headers, ...authHeaders }; } - mergedOpts.credentials = 'include'; + mergedOpts.credentials = "include"; res = await fetch(path, mergedOpts); } catch (err) { if (err.name === "AbortError") throw err; // let callers handle abort @@ -1237,17 +1292,22 @@ const retryHeaders = AuthManager.getAuthHeaders(); const retryOpts = opts || {}; retryOpts.headers = { ...retryOpts.headers, ...retryHeaders }; - retryOpts.credentials = 'include'; + retryOpts.credentials = "include"; res = await fetch(path, retryOpts); } catch (refreshErr) { AuthManager.clearSession(); AuthManager.showLoginScreen(); - throw new Error('Session expirée'); + throw new Error("Session expirée"); } } if (!res.ok) { var detail = ""; - try { var body = await res.json(); detail = body.detail || ""; } catch (_) { /* no json body */ } + try { + var body = await res.json(); + detail = body.detail || ""; + } catch (_) { + /* no json body */ + } showToast(detail || "Erreur API : " + res.status, "error"); throw new Error(detail || "API error: " + res.status); } @@ -1259,15 +1319,15 @@ // --------------------------------------------------------------------------- const AuthManager = { - ACCESS_TOKEN_KEY: 'obsigate_access_token', - TOKEN_EXPIRY_KEY: 'obsigate_token_expiry', - USER_KEY: 'obsigate_user', + ACCESS_TOKEN_KEY: "obsigate_access_token", + TOKEN_EXPIRY_KEY: "obsigate_token_expiry", + USER_KEY: "obsigate_user", _authEnabled: false, // ── Token storage (sessionStorage) ───────────────────────────── saveToken(tokenData) { - const expiresAt = Date.now() + (tokenData.expires_in * 1000); + const expiresAt = Date.now() + tokenData.expires_in * 1000; sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokenData.access_token); sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiresAt.toString()); if (tokenData.user) { @@ -1288,7 +1348,7 @@ const expiry = sessionStorage.getItem(this.TOKEN_EXPIRY_KEY); if (!expiry) return true; // Renew 60s before expiration - return Date.now() > (parseInt(expiry) - 60000); + return Date.now() > parseInt(expiry) - 60000; }, clearSession() { @@ -1300,21 +1360,21 @@ getAuthHeaders() { const token = this.getToken(); if (!token || !this._authEnabled) return null; - return { 'Authorization': 'Bearer ' + token }; + return { Authorization: "Bearer " + token }; }, // ── API calls ────────────────────────────────────────────────── async login(username, password, rememberMe) { - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ username, password, remember_me: rememberMe || false }), }); if (!response.ok) { const err = await response.json(); - throw new Error(err.detail || 'Erreur de connexion'); + throw new Error(err.detail || "Erreur de connexion"); } const data = await response.json(); this.saveToken(data); @@ -1324,27 +1384,29 @@ async logout() { try { const token = this.getToken(); - await fetch('/api/auth/logout', { - method: 'POST', - headers: token ? { 'Authorization': 'Bearer ' + token } : {}, - credentials: 'include', + await fetch("/api/auth/logout", { + method: "POST", + headers: token ? { Authorization: "Bearer " + token } : {}, + credentials: "include", }); - } catch (e) { /* continue even if API fails */ } + } catch (e) { + /* continue even if API fails */ + } this.clearSession(); this.showLoginScreen(); }, async refreshAccessToken() { - const response = await fetch('/api/auth/refresh', { - method: 'POST', - credentials: 'include', + const response = await fetch("/api/auth/refresh", { + method: "POST", + credentials: "include", }); if (!response.ok) { this.clearSession(); - throw new Error('Session expirée'); + throw new Error("Session expirée"); } const data = await response.json(); - const expiry = Date.now() + (data.expires_in * 1000); + const expiry = Date.now() + data.expires_in * 1000; sessionStorage.setItem(this.ACCESS_TOKEN_KEY, data.access_token); sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiry.toString()); return data.access_token; @@ -1353,48 +1415,49 @@ // ── UI controls ──────────────────────────────────────────────── showLoginScreen() { - const app = document.getElementById('app'); - const login = document.getElementById('login-screen'); - if (app) app.classList.add('hidden'); + const app = document.getElementById("app"); + const login = document.getElementById("login-screen"); + if (app) app.classList.add("hidden"); if (login) { - login.classList.remove('hidden'); - const usernameInput = document.getElementById('login-username'); + login.classList.remove("hidden"); + const usernameInput = document.getElementById("login-username"); if (usernameInput) usernameInput.focus(); } }, showApp() { - const login = document.getElementById('login-screen'); - const app = document.getElementById('app'); - if (login) login.classList.add('hidden'); - if (app) app.classList.remove('hidden'); + const login = document.getElementById("login-screen"); + const app = document.getElementById("app"); + if (login) login.classList.add("hidden"); + if (app) app.classList.remove("hidden"); this.renderUserMenu(); }, renderUserMenu() { const user = this.getUser(); - const userMenu = document.getElementById('user-menu'); + const userMenu = document.getElementById("user-menu"); if (!userMenu) return; if (!user || !this._authEnabled) { - userMenu.innerHTML = ''; + userMenu.innerHTML = ""; return; } - userMenu.innerHTML = - '' + (user.display_name || user.username) + '' + - ''; + userMenu.innerHTML = '' + (user.display_name || user.username) + "" + ''; safeCreateIcons(); - const logoutBtn = document.getElementById('logout-btn'); - if (logoutBtn) logoutBtn.addEventListener('click', () => AuthManager.logout()); + const logoutBtn = document.getElementById("logout-btn"); + if (logoutBtn) logoutBtn.addEventListener("click", () => AuthManager.logout()); - const adminRow = document.getElementById('admin-menu-row'); + const adminRow = document.getElementById("admin-menu-row"); if (adminRow) { - if (user.role === 'admin') { - adminRow.classList.remove('hidden'); + if (user.role === "admin") { + adminRow.classList.remove("hidden"); // Important: use an inline function to ensure we don't bind multiple identical listeners on rerenders, or clean up before - adminRow.onclick = () => { closeHeaderMenu(); AdminPanel.show(); }; + adminRow.onclick = () => { + closeHeaderMenu(); + AdminPanel.show(); + }; } else { - adminRow.classList.add('hidden'); + adminRow.classList.add("hidden"); } } }, @@ -1403,7 +1466,7 @@ async checkAuthStatus() { try { - const res = await fetch('/api/auth/status'); + const res = await fetch("/api/auth/status"); const data = await res.json(); this._authEnabled = data.auth_enabled; return data; @@ -1432,9 +1495,9 @@ await this.refreshAccessToken(); // Fetch user info const token = this.getToken(); - const res = await fetch('/api/auth/me', { - headers: { 'Authorization': 'Bearer ' + token }, - credentials: 'include', + const res = await fetch("/api/auth/me", { + headers: { Authorization: "Bearer " + token }, + credentials: "include", }); if (res.ok) { const user = await res.json(); @@ -1442,7 +1505,9 @@ this.showApp(); return true; } - } catch (e) { /* silent refresh failed */ } + } catch (e) { + /* silent refresh failed */ + } // No valid session — show login this.showLoginScreen(); @@ -1455,21 +1520,21 @@ // --------------------------------------------------------------------------- function initLoginForm() { - const form = document.getElementById('login-form'); + const form = document.getElementById("login-form"); if (!form) return; - form.addEventListener('submit', async (e) => { + form.addEventListener("submit", async (e) => { e.preventDefault(); - const username = document.getElementById('login-username').value; - const password = document.getElementById('login-password').value; - const rememberMe = document.getElementById('remember-me').checked; - const errorEl = document.getElementById('login-error'); - const btn = document.getElementById('login-btn'); + const username = document.getElementById("login-username").value; + const password = document.getElementById("login-password").value; + const rememberMe = document.getElementById("remember-me").checked; + const errorEl = document.getElementById("login-error"); + const btn = document.getElementById("login-btn"); btn.disabled = true; - btn.querySelector('.btn-spinner').classList.remove('hidden'); - btn.querySelector('.btn-text').textContent = 'Connexion...'; - errorEl.classList.add('hidden'); + btn.querySelector(".btn-spinner").classList.remove("hidden"); + btn.querySelector(".btn-text").textContent = "Connexion..."; + errorEl.classList.add("hidden"); try { await AuthManager.login(username, password, rememberMe); @@ -1478,27 +1543,27 @@ try { await Promise.all([loadVaults(), loadTags()]); } catch (err) { - console.error('Failed to load data after login:', err); + console.error("Failed to load data after login:", err); } safeCreateIcons(); } catch (err) { errorEl.textContent = err.message; - errorEl.classList.remove('hidden'); - document.getElementById('login-password').value = ''; - document.getElementById('login-password').focus(); + errorEl.classList.remove("hidden"); + document.getElementById("login-password").value = ""; + document.getElementById("login-password").focus(); } finally { btn.disabled = false; - btn.querySelector('.btn-spinner').classList.add('hidden'); - btn.querySelector('.btn-text').textContent = 'Se connecter'; + btn.querySelector(".btn-spinner").classList.add("hidden"); + btn.querySelector(".btn-text").textContent = "Se connecter"; } }); // Toggle password visibility - const toggleBtn = document.getElementById('toggle-password'); + const toggleBtn = document.getElementById("toggle-password"); if (toggleBtn) { - toggleBtn.addEventListener('click', () => { - const input = document.getElementById('login-password'); - input.type = input.type === 'password' ? 'text' : 'password'; + toggleBtn.addEventListener("click", () => { + const input = document.getElementById("login-password"); + input.type = input.type === "password" ? "text" : "password"; }); } } @@ -1513,19 +1578,19 @@ show() { this._createModal(); - this._modal.classList.add('active'); + this._modal.classList.add("active"); this._loadUsers(); }, hide() { - if (this._modal) this._modal.classList.remove('active'); + if (this._modal) this._modal.classList.remove("active"); }, _createModal() { if (this._modal) return; - this._modal = document.createElement('div'); - this._modal.className = 'editor-modal'; - this._modal.id = 'admin-modal'; + this._modal = document.createElement("div"); + this._modal.className = "editor-modal"; + this._modal.id = "admin-modal"; this._modal.innerHTML = `
Erreur : ' + err.message + '
'; + document.getElementById("admin-users-list").innerHTML = 'Erreur : ' + err.message + "
"; } }, _renderUsers(users) { - const container = document.getElementById('admin-users-list'); + const container = document.getElementById("admin-users-list"); if (!users.length) { container.innerHTML = 'Aucun utilisateur.
'; return; } - let html = '| Utilisateur | Rôle | Vaults | Statut | Dernière connexion | Actions | ' + - '|||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ' + u.username + '' + (u.display_name && u.display_name !== u.username ? ' ' + u.display_name + '' : '') + ' | ' +
- '' + u.role + ' | ' + - '' + vaults + ' | ' + - '' + status + ' | ' + - '' + lastLogin + ' | ' + + let html = '
| Utilisateur | Rôle | Vaults | Statut | Dernière connexion | Actions | " + "
|---|---|---|---|---|---|
| " +
+ u.username +
+ "" +
+ (u.display_name && u.display_name !== u.username ? " " + u.display_name + "" : "") + + " | " +
+ '' + + u.role + + " | " + + '' + + vaults + + " | " + + "" + + status + + " | " + + "" + + lastLogin + + " | " + '' + - '' + - '' + - ' |