ObsiGate/frontend/js/config.js
Bruno Charest 7866f93778
All checks were successful
CI / lint (push) Successful in 13s
CI / security (push) Successful in 8s
CI / test (push) Successful in 16s
CI / build (push) Successful in 6s
refactor: state.js → mutable object to fix 'assignment to constant' errors
ES module imports are read-only live bindings — can't reassign
imported let/const variables. Replace individual 'export let' with
single 'export const state = {...}' mutable object.

All modules updated: import { state } from './state.js'
All state access changed to state.xxx pattern.

Fixes cascade of 'Assignment to constant variable' errors.
2026-05-28 16:34:39 -04:00

1013 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

1|// config.js — extracted from app.js (3872-4865)
import { state } from './state.js';
3|
4|let _recentTimestampTimer = null;
5|let _recentFilesCache = [];
6|let _recentRefreshTimer = null;
7|
8|async function loadRecentFiles(vaultFilter) {
9| const listEl = document.getElementById("recent-list");
10| const emptyEl = document.getElementById("recent-empty");
11| if (!listEl) return;
12|
13| let url = "/api/recent?mode=modified";
14| if (vaultFilter) url += `&vault=${encodeURIComponent(vaultFilter)}`;
15| try {
16| const data = await api(url);
17| _recentFilesCache = data.files || [];
18| renderRecentList(_recentFilesCache);
19| } catch (err) {
20| console.error("Failed to load recent files:", err);
21| listEl.innerHTML = "";
22| if (emptyEl) {
23| emptyEl.classList.remove("hidden");
24| }
25| }
26|}
27|
28|function renderRecentList(files) {
29| const listEl = document.getElementById("recent-list");
30| const emptyEl = document.getElementById("recent-empty");
31| if (!listEl) return;
32| listEl.innerHTML = "";
33|
34| if (!files || files.length === 0) {
35| if (emptyEl) {
36| emptyEl.classList.remove("hidden");
37| safeCreateIcons();
38| }
39| return;
40| }
41| if (emptyEl) emptyEl.classList.add("hidden");
42|
43| files.forEach((f) => {
44| const item = el("div", { class: "recent-item", "data-vault": f.vault, "data-path": f.path });
45|
46| // Header row: time + vault badge
47| const header = el("div", { class: "recent-item-header" });
48| const timeSpan = el("span", { class: "recent-time" }, [icon("clock", 11), document.createTextNode(f.mtime_human)]);
49| const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]);
50| header.appendChild(timeSpan);
51| header.appendChild(badge);
52| item.appendChild(header);
53|
54| // Title
55| const titleEl = el("div", { class: "recent-item-title" }, [document.createTextNode(f.title || f.path.split("/").pop())]);
56| item.appendChild(titleEl);
57|
58| // Path breadcrumb
59| const pathParts = f.path.split("/");
60| if (pathParts.length > 1) {
61| const pathEl = el("div", { class: "recent-item-path" }, [document.createTextNode(pathParts.slice(0, -1).join(" / "))]);
62| item.appendChild(pathEl);
63| }
64|
65| // Preview
66| if (f.preview) {
67| const previewEl = el("div", { class: "recent-item-preview" }, [document.createTextNode(f.preview)]);
68| item.appendChild(previewEl);
69| }
70|
71| // Tags
72| if (f.tags && f.tags.length > 0) {
73| const tagsEl = el("div", { class: "recent-item-tags" });
74| f.tags.forEach((t) => {
75| tagsEl.appendChild(el("span", { class: "tag-pill" }, [document.createTextNode(t)]));
76| });
77| item.appendChild(tagsEl);
78| }
79|
80| // Click handler
81| item.addEventListener("click", () => {
82| openFile(f.vault, f.path);
83| closeMobileSidebar();
84| });
85|
86| listEl.appendChild(item);
87| });
88| safeCreateIcons();
89|}
90|
91|function _humanizeDelta(mtime) {
92| const delta = Date.now() / 1000 - mtime;
93| if (delta < 60) return "à l'instant";
94| if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
95| if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
96| if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
97| return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
98|}
99|
100|function _refreshRecentTimestamps() {
101| if (activeSidebarTab !== "recent" || !_recentFilesCache.length) return;
102| const items = document.querySelectorAll(".recent-item");
103| items.forEach((item, i) => {
104| if (i < _recentFilesCache.length) {
105| const timeSpan = item.querySelector(".recent-time");
106| if (timeSpan) {
107| // keep the icon, update text
108| const textNode = timeSpan.lastChild;
109| if (textNode && textNode.nodeType === Node.TEXT_NODE) {
110| textNode.textContent = _humanizeDelta(_recentFilesCache[i].mtime);
111| }
112| }
113| }
114| });
115|}
116|
117|function _populateRecentVaultFilter() {
118| const select = document.getElementById("recent-vault-filter");
119| if (!select) return;
120| // keep first option "Tous les vaults"
121| while (select.options.length > 1) select.remove(1);
122| state.allVaults.forEach((v) => {
123| const opt = document.createElement("option");
124| opt.value = v.name;
125| opt.textContent = v.name;
126| select.appendChild(opt);
127| });
128| syncVaultSelectors();
129|}
130|
131|function initRecentTab() {
132| const select = document.getElementById("recent-vault-filter");
133| if (select) {
134| select.addEventListener("change", async () => {
135| const val = select.value || "all";
136| await setSelectedVaultContext(val, { focusVault: val !== "all" });
137| });
138| }
139| // Periodic timestamp refresh (every 60s)
140| _recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000);
141|}
142|
143|// ---------------------------------------------------------------------------
144|// Sidebar tabs
145|// ---------------------------------------------------------------------------
146|function initSidebarTabs() {
147| document.querySelectorAll(".sidebar-tab").forEach((tab) => {
148| tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab));
149| });
150|}
151|
152|function switchSidebarTab(tab) {
153| state.activeSidebarTab = tab;
154| document.querySelectorAll(".sidebar-tab").forEach((btn) => {
155| const isActive = btn.dataset.tab === tab;
156| btn.classList.toggle("active", isActive);
157| btn.setAttribute("aria-selected", isActive ? "true" : "false");
158| });
159| document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => {
160| const isActive = panel.id === `sidebar-panel-${tab}`;
161| panel.classList.toggle("active", isActive);
162| });
163| const filterInput = document.getElementById("sidebar-filter-input");
164| if (filterInput) {
165| const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" };
166| filterInput.placeholder = placeholders[tab] || "";
167| }
168| const query = filterInput ? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : "";
169| if (query) {
170| if (tab === "vaults") performTreeSearch(query);
171| else if (tab === "tags") filterTagCloud(query);
172| }
173| // Auto-load recent files when switching to the recent tab
174| if (tab === "recent") {
175| _populateRecentVaultFilter();
176| const vaultFilter = document.getElementById("recent-vault-filter");
177| loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
178| }
179|}
180|
181|function initHelpModal() {
182| const openBtn = document.getElementById("help-open-btn");
183| const closeBtn = document.getElementById("help-close");
184| const modal = document.getElementById("help-modal");
185| if (!openBtn || !closeBtn || !modal) return;
186|
187| openBtn.addEventListener("click", () => {
188| modal.classList.add("active");
189| closeHeaderMenu();
190| safeCreateIcons();
191| initHelpNavigation();
192| });
193|
194| closeBtn.addEventListener("click", closeHelpModal);
195| modal.addEventListener("click", (e) => {
196| if (e.target === modal) {
197| closeHelpModal();
198| }
199| });
200|
201| document.addEventListener("keydown", (e) => {
202| if (e.key === "Escape" && modal.classList.contains("active")) {
203| closeHelpModal();
204| }
205| });
206|}
207|
208|function initHelpNavigation() {
209| const helpContent = document.querySelector(".help-content");
210| const helpBody = document.getElementById("help-body");
211| const navLinks = document.querySelectorAll(".help-nav-link");
212|
213| if (!helpContent || !helpBody || !navLinks.length) return;
214|
215| // Handle nav link clicks
216| navLinks.forEach((link) => {
217| link.addEventListener("click", (e) => {
218| e.preventDefault();
219| const targetId = link.getAttribute("href").substring(1);
220| const targetSection = document.getElementById(targetId);
221| if (targetSection) {
222| targetSection.scrollIntoView({ behavior: "smooth", block: "start" });
223| }
224| });
225| });
226|
227| // Scroll spy - update active nav link based on scroll position
228| const observer = new IntersectionObserver(
229| (entries) => {
230| entries.forEach((entry) => {
231| if (entry.isIntersecting) {
232| const id = entry.target.getAttribute("id");
233| navLinks.forEach((link) => {
234| if (link.getAttribute("href") === `#${id}`) {
235| navLinks.forEach((l) => l.classList.remove("active"));
236| link.classList.add("active");
237| }
238| });
239| }
240| });
241| },
242| {
243| root: helpBody,
244| rootMargin: "-20% 0px -70% 0px",
245| threshold: 0,
246| },
247| );
248|
249| // Observe all sections
250| document.querySelectorAll(".help-section").forEach((section) => {
251| observer.observe(section);
252| });
253|}
254|
255|function closeHelpModal() {
256| const modal = document.getElementById("help-modal");
257| if (modal) modal.classList.remove("active");
258|}
259|
260|function initConfigModal() {
261| const openBtn = document.getElementById("config-open-btn");
262| const closeBtn = document.getElementById("config-close");
263| const modal = document.getElementById("config-modal");
264| const addBtn = document.getElementById("config-add-btn");
265| const patternInput = document.getElementById("config-pattern-input");
266|
267| if (!openBtn || !closeBtn || !modal) return;
268|
269| openBtn.addEventListener("click", async () => {
270| modal.classList.add("active");
271| closeHeaderMenu();
272| renderConfigFilters();
273| loadConfigFields();
274| loadDiagnostics();
275| loadAbout();
276| await loadHiddenFilesSettings();
277| loadWebhooksUI();
278| loadSharesUI();
279| safeCreateIcons();
280| });
281|
282| closeBtn.addEventListener("click", closeConfigModal);
283| modal.addEventListener("click", (e) => {
284| if (e.target === modal) {
285| closeConfigModal();
286| }
287| });
288|
289| addBtn.addEventListener("click", addConfigFilter);
290| patternInput.addEventListener("keypress", (e) => {
291| if (e.key === "Enter") {
292| addConfigFilter();
293| }
294| });
295|
296| patternInput.addEventListener("input", updateRegexPreview);
297|
298| // Frontend config fields — save to localStorage on change
299| ["cfg-debounce", "cfg-results-per-page", "cfg-min-query", "cfg-timeout"].forEach((id) => {
300| const input = document.getElementById(id);
301| if (input) input.addEventListener("change", saveFrontendConfig);
302| });
303|
304| // Backend save button
305| const saveBtn = document.getElementById("cfg-save-backend");
306| if (saveBtn) saveBtn.addEventListener("click", saveBackendConfig);
307|
308| // Force reindex
309| const reindexBtn = document.getElementById("cfg-reindex");
310| if (reindexBtn) reindexBtn.addEventListener("click", forceReindex);
311|
312| // Reset defaults
313| const resetBtn = document.getElementById("cfg-reset-defaults");
314| if (resetBtn) resetBtn.addEventListener("click", resetConfigDefaults);
315|
316| // Refresh diagnostics
317| const diagBtn = document.getElementById("cfg-refresh-diag");
318| if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics);
319|
320| // Hidden files configuration
321| const saveHiddenBtn = document.getElementById("cfg-save-hidden-files");
322| if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings);
323|
324| document.addEventListener("keydown", (e) => {
325| if (e.key === "Escape" && modal.classList.contains("active")) {
326| closeConfigModal();
327| }
328| });
329|
330| // Load saved frontend config on startup
331| applyFrontendConfig();
332|}
333|
334|function closeConfigModal() {
335| const modal = document.getElementById("config-modal");
336| if (modal) modal.classList.remove("active");
337|}
338|
339|// --- Config field helpers ---
340|const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
341|
342|function _getFrontendConfig() {
343| try {
344| return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
345| } catch {
346| return {};
347| }
348|}
349|
350|function applyFrontendConfig() {
351| const cfg = _getFrontendConfig();
352| if (cfg.debounce_ms) {
353| /* applied dynamically in debounce setTimeout */
354| }
355| if (cfg.results_per_page) {
356| /* used as ADVANCED_SEARCH_LIMIT override */
357| }
358| if (cfg.min_query_length) {
359| /* used as MIN_SEARCH_LENGTH override */
360| }
361| if (cfg.search_timeout_ms) {
362| /* used as SEARCH_TIMEOUT_MS override */
363| }
364|}
365|
366|function _getEffective(key, fallback) {
367| const cfg = _getFrontendConfig();
368| return cfg[key] !== undefined ? cfg[key] : fallback;
369|}
370|
371|async function loadConfigFields() {
372| // Frontend fields from localStorage
373| const cfg = _getFrontendConfig();
374| _setField("cfg-debounce", cfg.debounce_ms || 300);
375| _setField("cfg-results-per-page", cfg.results_per_page || 50);
376| _setField("cfg-min-query", cfg.min_query_length || 2);
377| _setField("cfg-timeout", cfg.search_timeout_ms || 30000);
378|
379| // Backend fields from API
380| try {
381| const data = await api("/api/config");
382| _setField("cfg-workers", data.search_workers);
383| _setField("cfg-max-content", data.max_content_size);
384| _setField("cfg-title-boost", data.title_boost);
385| _setField("cfg-tag-boost", data.tag_boost);
386| _setField("cfg-prefix-exp", data.prefix_max_expansions);
387| _setField("cfg-recent-limit", data.recent_files_limit || 20);
388| // Watcher config
389| _setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false);
390| _setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true);
391| _setField("cfg-watcher-interval", data.watcher_polling_interval || 5);
392| _setField("cfg-watcher-debounce", data.watcher_debounce || 2);
393| } catch (err) {
394| console.error("Failed to load backend config:", err);
395| }
396|}
397|
398|function _setField(id, value) {
399| const el = document.getElementById(id);
400| if (el && value !== undefined) el.value = value;
401|}
402|
403|function _setCheckbox(id, checked) {
404| const el = document.getElementById(id);
405| if (el) el.checked = !!checked;
406|}
407|
408|function _getCheckbox(id) {
409| const el = document.getElementById(id);
410| return el ? el.checked : false;
411|}
412|
413|function _getFieldNum(id, fallback) {
414| const el = document.getElementById(id);
415| if (!el) return fallback;
416| const v = parseFloat(el.value);
417| return isNaN(v) ? fallback : v;
418|}
419|
420|function saveFrontendConfig() {
421| const cfg = {
422| debounce_ms: _getFieldNum("cfg-debounce", 300),
423| results_per_page: _getFieldNum("cfg-results-per-page", 50),
424| min_query_length: _getFieldNum("cfg-min-query", 2),
425| search_timeout_ms: _getFieldNum("cfg-timeout", 30000),
426| };
427| localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg));
428| showToast("Paramètres client sauvegardés", "success");
429|}
430|
431|async function saveBackendConfig() {
432| const body = {
433| search_workers: _getFieldNum("cfg-workers", 2),
434| max_content_size: _getFieldNum("cfg-max-content", 100000),
435| title_boost: _getFieldNum("cfg-title-boost", 3.0),
436| tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
437| prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
438| recent_files_limit: _getFieldNum("cfg-recent-limit", 20),
439| watcher_enabled: _getCheckbox("cfg-watcher-enabled"),
440| watcher_use_polling: _getCheckbox("cfg-watcher-polling"),
441| watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0),
442| watcher_debounce: _getFieldNum("cfg-watcher-debounce", 2.0),
443| };
444| try {
445| const res = await fetch("/api/config", {
446| method: "POST",
447| headers: { "Content-Type": "application/json" },
448| body: JSON.stringify(body),
449| });
450| if (res.ok) {
451| showToast("Configuration backend sauvegardée", "success");
452| } else {
453| const errorData = await res.json().catch(() => ({}));
454| showToast(errorData.detail || "Erreur de sauvegarde", "error");
455| }
456| } catch (err) {
457| console.error("Failed to save backend config:", err);
458| showToast("Erreur de sauvegarde", "error");
459| }
460|}
461|
462|async function forceReindex() {
463| const btn = document.getElementById("cfg-reindex");
464| if (btn) {
465| btn.disabled = true;
466| btn.textContent = "Réindexation...";
467| }
468| try {
469| await api("/api/index/reload");
470| showToast("Réindexation terminée", "success");
471| loadDiagnostics();
472| await Promise.all([loadVaults(), loadTags()]);
473| } catch (err) {
474| console.error("Reindex error:", err);
475| showToast("Erreur de réindexation", "error");
476| } finally {
477| if (btn) {
478| btn.disabled = false;
479| btn.textContent = "Forcer réindexation";
480| }
481| }
482|}
483|
484|async function resetConfigDefaults() {
485| // Reset frontend
486| localStorage.removeItem(_FRONTEND_CONFIG_KEY);
487| // Reset backend
488| try {
489| await fetch("/api/config", {
490| method: "POST",
491| headers: { "Content-Type": "application/json" },
492| body: JSON.stringify({
493| search_workers: 2,
494| debounce_ms: 300,
495| results_per_page: 50,
496| min_query_length: 2,
497| search_timeout_ms: 30000,
498| max_content_size: 100000,
499| title_boost: 3.0,
500| path_boost: 1.5,
501| tag_boost: 2.0,
502| prefix_max_expansions: 50,
503| snippet_context_chars: 120,
504| max_snippet_highlights: 5,
505| }),
506| });
507| } catch (err) {
508| console.error("Reset config error:", err);
509| }
510| loadConfigFields();
511| showToast("Configuration réinitialisée", "success");
512|}
513|
514|async function loadDiagnostics() {
515| const container = document.getElementById("config-diagnostics");
516| if (!container) return;
517| container.innerHTML = '<div class="config-diag-loading">Chargement...</div>';
518| try {
519| const data = await api("/api/diagnostics");
520| renderDiagnostics(container, data);
521| } catch (err) {
522| container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
523| }
524|}
525|
526|function renderDiagnostics(container, data) {
527| container.innerHTML = "";
528| const sections = [
529| {
530| title: "Index",
531| rows: [
532| ["Fichiers indexés", data.index.total_files],
533| ["Tags uniques", data.index.total_tags],
534| ["Vaults", Object.keys(data.index.vaults).join(", ")],
535| ],
536| },
537| {
538| title: "Index inversé",
539| rows: [
540| ["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()],
541| ["Postings total", data.inverted_index.total_postings.toLocaleString()],
542| ["Documents", data.inverted_index.documents],
543| ["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"],
544| ["Stale", data.inverted_index.is_stale ? "Oui" : "Non"],
545| ],
546| },
547| {
548| title: "Moteur de recherche",
549| rows: [
550| ["Executor actif", data.search_executor.active ? "Oui" : "Non"],
551| ["Workers max", data.search_executor.max_workers],
552| ],
553| },
554| ];
555| sections.forEach((section) => {
556| const div = document.createElement("div");
557| div.className = "config-diag-section";
558| const title = document.createElement("div");
559| title.className = "config-diag-section-title";
560| title.textContent = section.title;
561| div.appendChild(title);
562| section.rows.forEach(([label, value]) => {
563| const row = document.createElement("div");
564| row.className = "config-diag-row";
565| row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
566| div.appendChild(row);
567| });
568| container.appendChild(div);
569| });
570|}
571|
572|// --- About Section ---
573|
574|function loadAbout() {
575| const container = document.getElementById("config-about");
576| if (!container) return;
577|
578| // Fetch health info for version
579| api("/api/health").then((health) => {
580| container.innerHTML = "";
581|
582| const sections = [
583| {
584| title: "Application",
585| rows: [
586| ["Nom", "ObsiGate"],
587| ["Version", state.APP_VERSION],
588| ["Version API", health.version || "—"],
589| ["Statut", health.status || "—"],
590| ],
591| },
592| {
593| title: "Environnement",
594| rows: [
595| ["Vaults configurés", health.vaults || "—"],
596| ["Fichiers indexés", health.total_files || "—"],
597| ["Navigateur", navigator.userAgent.split(" ").pop()],
598| ["Plateforme", navigator.platform || "—"],
599| ["Langue", navigator.language || "—"],
600| ],
601| },
602| {
603| title: "Composants",
604| rows: [
605| ["Backend", "FastAPI (Python)"],
606| ["Rendu Markdown", "mistune"],
607| ["Surveillance fichiers", "watchdog"],
608| ["Frontend", "Vanilla JavaScript"],
609| ["Icônes", "Lucide Icons"],
610| ["Coloration syntaxe", "highlight.js"],
611| ["Éditeur", "CodeMirror 6"],
612| ],
613| },
614| ];
615|
616| sections.forEach((section) => {
617| const div = document.createElement("div");
618| div.className = "config-diag-section";
619| const title = document.createElement("div");
620| title.className = "config-diag-section-title";
621| title.textContent = section.title;
622| div.appendChild(title);
623| section.rows.forEach(([label, value]) => {
624| const row = document.createElement("div");
625| row.className = "config-diag-row";
626| row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
627| div.appendChild(row);
628| });
629| container.appendChild(div);
630| });
631| }).catch(() => {
632| container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
633| });
634|}
635|
636|// --- Hidden Files Configuration ---
637|
638|async function loadHiddenFilesSettings() {
639| const container = document.getElementById("hidden-files-vault-list");
640| if (!container) return;
641|
642| container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Chargement...</div>';
643|
644| try {
645| const settings = await api("/api/vaults/settings/all");
646| renderHiddenFilesSettings(container, settings);
647| } catch (err) {
648| console.error("Failed to load hidden files settings:", err);
649| container.innerHTML = '<div style="padding:12px;color:var(--error)">Erreur de chargement</div>';
650| }
651|}
652|
653|function renderHiddenFilesSettings(container, allSettings) {
654| container.innerHTML = "";
655|
656| if (!allVaults || state.allVaults.length === 0) {
657| container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Aucun vault configuré</div>';
658| return;
659| }
660|
661| state.allVaults.forEach((vault) => {
662| const settings = allSettings[vault.name] || { hideHiddenFiles: false };
663|
664| const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name });
665|
666| // Vault header
667| 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")])]);
668|
669| // Hide hidden files toggle
670| const toggleRow = el("div", { class: "config-row" }, [
671| el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [document.createTextNode("Masquer les fichiers/dossiers cachés")]),
672| el("label", { class: "config-toggle" }, [
673| el("input", {
674| type: "checkbox",
675| id: `hide-hidden-${vault.name}`,
676| "data-vault": vault.name,
677| checked: settings.hideHiddenFiles ? "true" : false,
678| }),
679| el("span", { class: "config-toggle-slider" }),
680| ]),
681| 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)")]),
682| ]);
683|
684| vaultCard.appendChild(header);
685| vaultCard.appendChild(toggleRow);
686|
687| container.appendChild(vaultCard);
688| });
689|}
690|
691|async function saveHiddenFilesSettings() {
692| const btn = document.getElementById("cfg-save-hidden-files");
693| if (btn) {
694| btn.disabled = true;
695| btn.textContent = "Sauvegarde...";
696| }
697|
698| try {
699| const vaultCards = document.querySelectorAll(".hidden-files-vault-card");
700| const promises = [];
701|
702| vaultCards.forEach((card) => {
703| const vaultName = card.dataset.vault;
704| const hideHiddenFiles = document.getElementById(`hide-hidden-${vaultName}`)?.checked || false;
705|
706| const settings = {
707| hideHiddenFiles,
708| };
709|
710| promises.push(
711| api(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, {
712| method: "POST",
713| headers: { "Content-Type": "application/json" },
714| body: JSON.stringify(settings),
715| }),
716| );
717| });
718|
719| await Promise.all(promises);
720|
721| // Reload vault settings to update the cache
722| await loadVaultSettings();
723|
724| showToast("✓ Paramètres sauvegardés", "success");
725|
726| // Refresh the UI to apply the filter
727| await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
728| } catch (err) {
729| console.error("Failed to save hidden files settings:", err);
730| const errorMsg = err.message || "Erreur inconnue";
731| showToast(`Erreur: ${errorMsg}`, "error");
732| } finally {
733| if (btn) {
734| btn.disabled = false;
735| btn.textContent = "💾 Sauvegarder";
736| }
737| }
738|}
739|
740|// ── Webhooks UI ──
741|async function loadWebhooksUI() {
742| const list = document.getElementById("webhooks-list");
743| if (!list) return;
744| try {
745| const webhooks = await api("/api/webhooks");
746| renderWebhooksUI(webhooks);
747| } catch { list.innerHTML = '<div class="config-description">Admin uniquement</div>'; }
748|}
749|function renderWebhooksUI(webhooks) {
750| const list = document.getElementById("webhooks-list");
751| if (!list) return;
752| if (!webhooks.length) { list.innerHTML = '<div class="config-description">Aucun webhook configuré.</div>'; return; }
753| list.innerHTML = webhooks.map(w => `
754| <div class="webhook-item">
755| <span class="webhook-name">${escapeHtml(w.name)}</span>
756| <span class="webhook-url">${escapeHtml(w.url)}</span>
757| <span class="webhook-events">${(w.events||[]).join(", ")}</span>
758| <button class="webhook-delete" data-id="${w.id}">✕</button>
759| </div>
760| `).join("");
761| list.querySelectorAll(".webhook-delete").forEach(b => b.addEventListener("click", async () => {
762| await api(`/api/webhooks/${b.dataset.id}`, { method: "DELETE" });
763| loadWebhooksUI();
764| }));
765|}
766|document.addEventListener("click", function(e) {
767| if (e.target.id === "webhook-add-btn") {
768| const name = document.getElementById("webhook-name-input").value.trim();
769| const url = document.getElementById("webhook-url-input").value.trim();
770| if (!url) { showToast("URL requise", "error"); return; }
771| 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"));
772| }
773|});
774|
775|// ── Shares UI ──
776|async function loadSharesUI() {
777| const list = document.getElementById("shares-list");
778| if (!list) return;
779| try {
780| const shares = await api("/api/shares");
781| renderSharesUI(shares);
782| } catch { list.innerHTML = '<div class="config-description">Chargement...</div>'; }
783|}
784|function renderSharesUI(shares) {
785| const list = document.getElementById("shares-list");
786| if (!list) return;
787| if (!shares.length) { list.innerHTML = '<div class="config-description">Aucun partage actif.</div>'; return; }
788| list.innerHTML = shares.map(s => `
789| <div class="share-item">
790| <span class="share-path">${escapeHtml(s.vault)}/${escapeHtml(s.path)}</span>
791| <span class="share-url"><a href="${s.url}" target="_blank">${s.url}</a></span>
792| <span class="share-meta">${s.access_count} vue(s)${s.expires_at ? ' · Expire' : ''}</span>
793| <button class="share-revoke" data-id="${s.id}">Révoquer</button>
794| </div>
795| `).join("");
796| list.querySelectorAll(".share-revoke").forEach(b => b.addEventListener("click", async () => {
797| await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
798| loadSharesUI();
799| }));
800|}
801|
802|// ── Share Dialog (professional) ──
803|async function openShareDialog(vault, path) {
804| // First check if already shared
805| let existingShare = null;
806| try {
807| const shares = await api("/api/shares");
808| existingShare = shares.find(s => s.vault === vault && s.path === path);
809| } catch (e) { /* ignore */ }
810|
811| const div = document.createElement("div");
812| div.className = "share-dialog-overlay";
813|
814| const renderContent = () => {
815| if (existingShare) {
816| const url = window.location.origin + existingShare.url;
817| const expiresInfo = existingShare.expires_at
818| ? `<p style="font-size:0.75rem;color:var(--text-muted)">Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}</p>`
819| : '<p style="font-size:0.75rem;color:var(--text-muted)">Sans expiration</p>';
820| div.innerHTML = `
821| <div class="share-dialog">
822| <h3>📤 Document partagé</h3>
823| <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:4px">${escapeHtml(vault)}/${escapeHtml(path)}</p>
824| ${expiresInfo}
825| <p style="font-size:0.75rem;color:var(--text-muted);margin-bottom:8px">${existingShare.access_count} vue(s)</p>
826| <input type="text" class="share-url-input" value="${url}" readonly onclick="this.select()">
827| <div class="share-dialog-actions">
828| <button class="share-copy-btn">📋 Copier le lien</button>
829| <button class="share-revoke-btn">🗑 Révoquer</button>
830| <button class="share-close-btn">Fermer</button>
831| </div>
832| </div>`;
833| div.querySelector(".share-copy-btn").addEventListener("click", async () => {
834| try {
835| await navigator.clipboard.writeText(url);
836| } catch (e) {
837| // Fallback for non-HTTPS contexts
838| const ta = document.createElement("textarea");
839| ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px";
840| document.body.appendChild(ta); ta.select(); document.execCommand("copy");
841| document.body.removeChild(ta);
842| }
843| showToast("Lien copié !", "success");
844| div.remove();
845| });
846| div.querySelector(".share-revoke-btn").addEventListener("click", async () => {
847| try {
848| await api(`/api/share/${existingShare.id}`, { method: "DELETE" });
849| showToast("Partage révoqué", "success");
850| existingShare = null;
851| renderContent();
852| } catch (err) { showToast("Erreur: " + err.message, "error"); }
853| });
854| } else {
855| div.innerHTML = `
856| <div class="share-dialog">
857| <h3>📤 Partager ce document</h3>
858| <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:12px">${escapeHtml(vault)}/${escapeHtml(path)}</p>
859| <p style="font-size:0.8rem;margin-bottom:8px">Ce lien sera accessible <strong>publiquement, sans authentification</strong>.</p>
860| <label style="font-size:0.8rem;display:flex;align-items:center;gap:8px;margin-bottom:12px">
861| Expiration :
862| <select id="share-expiry" style="padding:4px 8px;border:1px solid var(--border);border-radius:4px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem">
863| <option value="">Jamais</option>
864| <option value="1">1 heure</option>
865| <option value="24">24 heures</option>
866| <option value="168">7 jours</option>
867| <option value="720">30 jours</option>
868| </select>
869| </label>
870| <div class="share-dialog-actions">
871| <button class="share-create-btn">🔗 Créer le lien</button>
872| <button class="share-close-btn">Fermer</button>
873| </div>
874| </div>`;
875| div.querySelector(".share-create-btn").addEventListener("click", async () => {
876| try {
877| const expiry = document.getElementById("share-expiry")?.value;
878| const share = await api(`/api/share/${encodeURIComponent(vault)}`, {
879| method: "POST",
880| headers: { "Content-Type": "application/json" },
881| body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }),
882| });
883| existingShare = share;
884| renderContent();
885| showToast("Lien créé !", "success");
886| } catch (err) { showToast("Erreur: " + err.message, "error"); }
887| });
888| }
889| div.querySelector(".share-close-btn").addEventListener("click", () => div.remove());
890| div.addEventListener("click", (e) => { if (e.target === div) div.remove(); });
891| };
892|
893| renderContent();
894| document.body.appendChild(div);
895|}
896|
897|function renderConfigFilters() {
898| const config = TagFilterService.getConfig();
899| const filters = config.tagFilters || TagFilterService.defaultFilters;
900| const container = document.getElementById("config-filters-list");
901|
902| container.innerHTML = "";
903|
904| filters.forEach((filter, index) => {
905| const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
906| el("span", {}, [document.createTextNode(filter.pattern)]),
907| el(
908| "button",
909| {
910| class: "config-filter-toggle",
911| title: filter.enabled ? "Désactiver" : "Activer",
912| type: "button",
913| },
914| [document.createTextNode(filter.enabled ? "✓" : "○")],
915| ),
916| el(
917| "button",
918| {
919| class: "config-filter-remove",
920| title: "Supprimer",
921| type: "button",
922| },
923| [document.createTextNode("×")],
924| ),
925| ]);
926|
927| const toggleBtn = badge.querySelector(".config-filter-toggle");
928| const removeBtn = badge.querySelector(".config-filter-remove");
929|
930| toggleBtn.addEventListener("click", (e) => {
931| e.stopPropagation();
932| toggleConfigFilter(index);
933| });
934|
935| removeBtn.addEventListener("click", (e) => {
936| e.stopPropagation();
937| removeConfigFilter(index);
938| });
939|
940| container.appendChild(badge);
941| });
942|}
943|
944|function toggleConfigFilter(index) {
945| const config = TagFilterService.getConfig();
946| const filters = config.tagFilters || TagFilterService.defaultFilters;
947| if (filters[index]) {
948| filters[index].enabled = !filters[index].enabled;
949| config.tagFilters = filters;
950| TagFilterService.saveConfig(config);
951| renderConfigFilters();
952| refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
953| }
954|}
955|
956|function removeConfigFilter(index) {
957| const config = TagFilterService.getConfig();
958| let filters = config.tagFilters || TagFilterService.defaultFilters;
959| filters = filters.filter((_, i) => i !== index);
960| config.tagFilters = filters;
961| TagFilterService.saveConfig(config);
962| renderConfigFilters();
963| refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
964|}
965|
966|function addConfigFilter() {
967| const input = document.getElementById("config-pattern-input");
968| const pattern = input.value.trim();
969|
970| if (!pattern) return;
971|
972| const regex = TagFilterService.patternToRegex(pattern);
973| const config = TagFilterService.getConfig();
974| const filters = config.tagFilters || TagFilterService.defaultFilters;
975|
976| const newFilter = { pattern, regex, enabled: true };
977| filters.push(newFilter);
978| config.tagFilters = filters;
979| TagFilterService.saveConfig(config);
980|
981| input.value = "";
982| renderConfigFilters();
983| refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
984| updateRegexPreview();
985|}
986|
987|function updateRegexPreview() {
988| const input = document.getElementById("config-pattern-input");
989| const preview = document.getElementById("config-regex-preview");
990| const code = document.getElementById("config-regex-code");
991| const pattern = input.value.trim();
992|
993| if (pattern) {
994| const regex = TagFilterService.patternToRegex(pattern);
995| code.textContent = `^${regex}$`;
996| preview.style.display = "block";
997| } else {
998| preview.style.display = "none";
999| }
1000|}
1001|
1002|
1003|export {
1004| initSidebarTabs,
1005| initConfigModal,
1006| initConfigModal as initConfigPanel,
1007| initHelpModal,
1008| closeHelpModal,
1009| initRecentTab,
1010| loadAbout as initAboutSection,
1011| loadHiddenFilesSettings as initHiddenFilesConfig,
1012|};
1013|