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.
1013 lines
41 KiB
JavaScript
1013 lines
41 KiB
JavaScript
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| |