frontend/js/ structure: state.js (55 lines) — Shared mutable state, constants utils.js (510 lines) — EXT_ICONS, getFileIcon, escapeHtml, safeCreateIcons auth.js (547 lines) — api(), AuthManager, initLoginForm, AdminPanel search.js (1106 lines)— SearchHistory, QueryParser, Autocomplete, performSearch sidebar.js (1091 lines)— Vault tree, sidebar filter, TagFilterService, loadTags viewer.js (1554 lines)— openFile, Outline, ScrollSpy, Frontmatter, Editor ui.js (2250 lines)— Theme, Toast, Sidebar, Dropdowns, Tabs, ContextMenu dashboard.js (461 lines) — Dashboard widgets (Recent, Stats, Bookmarks) config.js (999 lines) — Config panel, Hidden files, About, Sidebar tabs sync.js (436 lines) — SSE/IndexUpdateManager, PWA registration graph.js (401 lines) — GraphViewManager (force-directed canvas graph) legacy.js (550 lines) — Remaining bridge functions (goHome, showWelcome, initSearch) app.js (80 lines) — Thin orchestrator: imports all modules, calls init() index.html: switched from <script src="app.js"> to <script type="module" src="js/app.js"> Original app.js preserved for backward compatibility. All 14 modules pass node --check syntax validation.
1009 lines
34 KiB
JavaScript
1009 lines
34 KiB
JavaScript
// config.js — extracted from app.js (3872-4865)
|
||
import { selectedContextVault, allVaults, activeSidebarTab, sidebarFilterCaseSensitive } from './state.js';
|
||
|
||
async function loadRecentFiles(vaultFilter) {
|
||
const listEl = document.getElementById("recent-list");
|
||
const emptyEl = document.getElementById("recent-empty");
|
||
if (!listEl) return;
|
||
|
||
let url = "/api/recent?mode=modified";
|
||
if (vaultFilter) url += `&vault=${encodeURIComponent(vaultFilter)}`;
|
||
try {
|
||
const data = await api(url);
|
||
_recentFilesCache = data.files || [];
|
||
renderRecentList(_recentFilesCache);
|
||
} catch (err) {
|
||
console.error("Failed to load recent files:", err);
|
||
listEl.innerHTML = "";
|
||
if (emptyEl) {
|
||
emptyEl.classList.remove("hidden");
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderRecentList(files) {
|
||
const listEl = document.getElementById("recent-list");
|
||
const emptyEl = document.getElementById("recent-empty");
|
||
if (!listEl) return;
|
||
listEl.innerHTML = "";
|
||
|
||
if (!files || files.length === 0) {
|
||
if (emptyEl) {
|
||
emptyEl.classList.remove("hidden");
|
||
safeCreateIcons();
|
||
}
|
||
return;
|
||
}
|
||
if (emptyEl) emptyEl.classList.add("hidden");
|
||
|
||
files.forEach((f) => {
|
||
const item = el("div", { class: "recent-item", "data-vault": f.vault, "data-path": f.path });
|
||
|
||
// Header row: time + vault badge
|
||
const header = el("div", { class: "recent-item-header" });
|
||
const timeSpan = el("span", { class: "recent-time" }, [icon("clock", 11), document.createTextNode(f.mtime_human)]);
|
||
const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]);
|
||
header.appendChild(timeSpan);
|
||
header.appendChild(badge);
|
||
item.appendChild(header);
|
||
|
||
// Title
|
||
const titleEl = el("div", { class: "recent-item-title" }, [document.createTextNode(f.title || f.path.split("/").pop())]);
|
||
item.appendChild(titleEl);
|
||
|
||
// Path breadcrumb
|
||
const pathParts = f.path.split("/");
|
||
if (pathParts.length > 1) {
|
||
const pathEl = el("div", { class: "recent-item-path" }, [document.createTextNode(pathParts.slice(0, -1).join(" / "))]);
|
||
item.appendChild(pathEl);
|
||
}
|
||
|
||
// Preview
|
||
if (f.preview) {
|
||
const previewEl = el("div", { class: "recent-item-preview" }, [document.createTextNode(f.preview)]);
|
||
item.appendChild(previewEl);
|
||
}
|
||
|
||
// Tags
|
||
if (f.tags && f.tags.length > 0) {
|
||
const tagsEl = el("div", { class: "recent-item-tags" });
|
||
f.tags.forEach((t) => {
|
||
tagsEl.appendChild(el("span", { class: "tag-pill" }, [document.createTextNode(t)]));
|
||
});
|
||
item.appendChild(tagsEl);
|
||
}
|
||
|
||
// Click handler
|
||
item.addEventListener("click", () => {
|
||
openFile(f.vault, f.path);
|
||
closeMobileSidebar();
|
||
});
|
||
|
||
listEl.appendChild(item);
|
||
});
|
||
safeCreateIcons();
|
||
}
|
||
|
||
function _humanizeDelta(mtime) {
|
||
const delta = Date.now() / 1000 - mtime;
|
||
if (delta < 60) return "à l'instant";
|
||
if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
|
||
if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
|
||
if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
|
||
return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
|
||
}
|
||
|
||
function _refreshRecentTimestamps() {
|
||
if (activeSidebarTab !== "recent" || !_recentFilesCache.length) return;
|
||
const items = document.querySelectorAll(".recent-item");
|
||
items.forEach((item, i) => {
|
||
if (i < _recentFilesCache.length) {
|
||
const timeSpan = item.querySelector(".recent-time");
|
||
if (timeSpan) {
|
||
// keep the icon, update text
|
||
const textNode = timeSpan.lastChild;
|
||
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
||
textNode.textContent = _humanizeDelta(_recentFilesCache[i].mtime);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function _populateRecentVaultFilter() {
|
||
const select = document.getElementById("recent-vault-filter");
|
||
if (!select) return;
|
||
// keep first option "Tous les vaults"
|
||
while (select.options.length > 1) select.remove(1);
|
||
allVaults.forEach((v) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = v.name;
|
||
opt.textContent = v.name;
|
||
select.appendChild(opt);
|
||
});
|
||
syncVaultSelectors();
|
||
}
|
||
|
||
function initRecentTab() {
|
||
const select = document.getElementById("recent-vault-filter");
|
||
if (select) {
|
||
select.addEventListener("change", async () => {
|
||
const val = select.value || "all";
|
||
await setSelectedVaultContext(val, { focusVault: val !== "all" });
|
||
});
|
||
}
|
||
// Periodic timestamp refresh (every 60s)
|
||
_recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Sidebar tabs
|
||
// ---------------------------------------------------------------------------
|
||
function initSidebarTabs() {
|
||
document.querySelectorAll(".sidebar-tab").forEach((tab) => {
|
||
tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab));
|
||
});
|
||
}
|
||
|
||
function switchSidebarTab(tab) {
|
||
activeSidebarTab = tab;
|
||
document.querySelectorAll(".sidebar-tab").forEach((btn) => {
|
||
const isActive = btn.dataset.tab === tab;
|
||
btn.classList.toggle("active", isActive);
|
||
btn.setAttribute("aria-selected", isActive ? "true" : "false");
|
||
});
|
||
document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => {
|
||
const isActive = panel.id === `sidebar-panel-${tab}`;
|
||
panel.classList.toggle("active", isActive);
|
||
});
|
||
const filterInput = document.getElementById("sidebar-filter-input");
|
||
if (filterInput) {
|
||
const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" };
|
||
filterInput.placeholder = placeholders[tab] || "";
|
||
}
|
||
const query = filterInput ? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : "";
|
||
if (query) {
|
||
if (tab === "vaults") performTreeSearch(query);
|
||
else if (tab === "tags") filterTagCloud(query);
|
||
}
|
||
// Auto-load recent files when switching to the recent tab
|
||
if (tab === "recent") {
|
||
_populateRecentVaultFilter();
|
||
const vaultFilter = document.getElementById("recent-vault-filter");
|
||
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
|
||
}
|
||
}
|
||
|
||
function initHelpModal() {
|
||
const openBtn = document.getElementById("help-open-btn");
|
||
const closeBtn = document.getElementById("help-close");
|
||
const modal = document.getElementById("help-modal");
|
||
if (!openBtn || !closeBtn || !modal) return;
|
||
|
||
openBtn.addEventListener("click", () => {
|
||
modal.classList.add("active");
|
||
closeHeaderMenu();
|
||
safeCreateIcons();
|
||
initHelpNavigation();
|
||
});
|
||
|
||
closeBtn.addEventListener("click", closeHelpModal);
|
||
modal.addEventListener("click", (e) => {
|
||
if (e.target === modal) {
|
||
closeHelpModal();
|
||
}
|
||
});
|
||
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||
closeHelpModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
function initHelpNavigation() {
|
||
const helpContent = document.querySelector(".help-content");
|
||
const helpBody = document.getElementById("help-body");
|
||
const navLinks = document.querySelectorAll(".help-nav-link");
|
||
|
||
if (!helpContent || !helpBody || !navLinks.length) return;
|
||
|
||
// Handle nav link clicks
|
||
navLinks.forEach((link) => {
|
||
link.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
const targetId = link.getAttribute("href").substring(1);
|
||
const targetSection = document.getElementById(targetId);
|
||
if (targetSection) {
|
||
targetSection.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
});
|
||
});
|
||
|
||
// Scroll spy - update active nav link based on scroll position
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
const id = entry.target.getAttribute("id");
|
||
navLinks.forEach((link) => {
|
||
if (link.getAttribute("href") === `#${id}`) {
|
||
navLinks.forEach((l) => l.classList.remove("active"));
|
||
link.classList.add("active");
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
{
|
||
root: helpBody,
|
||
rootMargin: "-20% 0px -70% 0px",
|
||
threshold: 0,
|
||
},
|
||
);
|
||
|
||
// Observe all sections
|
||
document.querySelectorAll(".help-section").forEach((section) => {
|
||
observer.observe(section);
|
||
});
|
||
}
|
||
|
||
function closeHelpModal() {
|
||
const modal = document.getElementById("help-modal");
|
||
if (modal) modal.classList.remove("active");
|
||
}
|
||
|
||
function initConfigModal() {
|
||
const openBtn = document.getElementById("config-open-btn");
|
||
const closeBtn = document.getElementById("config-close");
|
||
const modal = document.getElementById("config-modal");
|
||
const addBtn = document.getElementById("config-add-btn");
|
||
const patternInput = document.getElementById("config-pattern-input");
|
||
|
||
if (!openBtn || !closeBtn || !modal) return;
|
||
|
||
openBtn.addEventListener("click", async () => {
|
||
modal.classList.add("active");
|
||
closeHeaderMenu();
|
||
renderConfigFilters();
|
||
loadConfigFields();
|
||
loadDiagnostics();
|
||
loadAbout();
|
||
await loadHiddenFilesSettings();
|
||
loadWebhooksUI();
|
||
loadSharesUI();
|
||
safeCreateIcons();
|
||
});
|
||
|
||
closeBtn.addEventListener("click", closeConfigModal);
|
||
modal.addEventListener("click", (e) => {
|
||
if (e.target === modal) {
|
||
closeConfigModal();
|
||
}
|
||
});
|
||
|
||
addBtn.addEventListener("click", addConfigFilter);
|
||
patternInput.addEventListener("keypress", (e) => {
|
||
if (e.key === "Enter") {
|
||
addConfigFilter();
|
||
}
|
||
});
|
||
|
||
patternInput.addEventListener("input", updateRegexPreview);
|
||
|
||
// Frontend config fields — save to localStorage on change
|
||
["cfg-debounce", "cfg-results-per-page", "cfg-min-query", "cfg-timeout"].forEach((id) => {
|
||
const input = document.getElementById(id);
|
||
if (input) input.addEventListener("change", saveFrontendConfig);
|
||
});
|
||
|
||
// Backend save button
|
||
const saveBtn = document.getElementById("cfg-save-backend");
|
||
if (saveBtn) saveBtn.addEventListener("click", saveBackendConfig);
|
||
|
||
// Force reindex
|
||
const reindexBtn = document.getElementById("cfg-reindex");
|
||
if (reindexBtn) reindexBtn.addEventListener("click", forceReindex);
|
||
|
||
// Reset defaults
|
||
const resetBtn = document.getElementById("cfg-reset-defaults");
|
||
if (resetBtn) resetBtn.addEventListener("click", resetConfigDefaults);
|
||
|
||
// Refresh diagnostics
|
||
const diagBtn = document.getElementById("cfg-refresh-diag");
|
||
if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics);
|
||
|
||
// Hidden files configuration
|
||
const saveHiddenBtn = document.getElementById("cfg-save-hidden-files");
|
||
if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings);
|
||
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||
closeConfigModal();
|
||
}
|
||
});
|
||
|
||
// Load saved frontend config on startup
|
||
applyFrontendConfig();
|
||
}
|
||
|
||
function closeConfigModal() {
|
||
const modal = document.getElementById("config-modal");
|
||
if (modal) modal.classList.remove("active");
|
||
}
|
||
|
||
// --- Config field helpers ---
|
||
const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
|
||
|
||
function _getFrontendConfig() {
|
||
try {
|
||
return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function applyFrontendConfig() {
|
||
const cfg = _getFrontendConfig();
|
||
if (cfg.debounce_ms) {
|
||
/* applied dynamically in debounce setTimeout */
|
||
}
|
||
if (cfg.results_per_page) {
|
||
/* used as ADVANCED_SEARCH_LIMIT override */
|
||
}
|
||
if (cfg.min_query_length) {
|
||
/* used as MIN_SEARCH_LENGTH override */
|
||
}
|
||
if (cfg.search_timeout_ms) {
|
||
/* used as SEARCH_TIMEOUT_MS override */
|
||
}
|
||
}
|
||
|
||
function _getEffective(key, fallback) {
|
||
const cfg = _getFrontendConfig();
|
||
return cfg[key] !== undefined ? cfg[key] : fallback;
|
||
}
|
||
|
||
async function loadConfigFields() {
|
||
// Frontend fields from localStorage
|
||
const cfg = _getFrontendConfig();
|
||
_setField("cfg-debounce", cfg.debounce_ms || 300);
|
||
_setField("cfg-results-per-page", cfg.results_per_page || 50);
|
||
_setField("cfg-min-query", cfg.min_query_length || 2);
|
||
_setField("cfg-timeout", cfg.search_timeout_ms || 30000);
|
||
|
||
// Backend fields from API
|
||
try {
|
||
const data = await api("/api/config");
|
||
_setField("cfg-workers", data.search_workers);
|
||
_setField("cfg-max-content", data.max_content_size);
|
||
_setField("cfg-title-boost", data.title_boost);
|
||
_setField("cfg-tag-boost", data.tag_boost);
|
||
_setField("cfg-prefix-exp", data.prefix_max_expansions);
|
||
_setField("cfg-recent-limit", data.recent_files_limit || 20);
|
||
// Watcher config
|
||
_setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false);
|
||
_setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true);
|
||
_setField("cfg-watcher-interval", data.watcher_polling_interval || 5);
|
||
_setField("cfg-watcher-debounce", data.watcher_debounce || 2);
|
||
} catch (err) {
|
||
console.error("Failed to load backend config:", err);
|
||
}
|
||
}
|
||
|
||
function _setField(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el && value !== undefined) el.value = value;
|
||
}
|
||
|
||
function _setCheckbox(id, checked) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.checked = !!checked;
|
||
}
|
||
|
||
function _getCheckbox(id) {
|
||
const el = document.getElementById(id);
|
||
return el ? el.checked : false;
|
||
}
|
||
|
||
function _getFieldNum(id, fallback) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return fallback;
|
||
const v = parseFloat(el.value);
|
||
return isNaN(v) ? fallback : v;
|
||
}
|
||
|
||
function saveFrontendConfig() {
|
||
const cfg = {
|
||
debounce_ms: _getFieldNum("cfg-debounce", 300),
|
||
results_per_page: _getFieldNum("cfg-results-per-page", 50),
|
||
min_query_length: _getFieldNum("cfg-min-query", 2),
|
||
search_timeout_ms: _getFieldNum("cfg-timeout", 30000),
|
||
};
|
||
localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg));
|
||
showToast("Paramètres client sauvegardés", "success");
|
||
}
|
||
|
||
async function saveBackendConfig() {
|
||
const body = {
|
||
search_workers: _getFieldNum("cfg-workers", 2),
|
||
max_content_size: _getFieldNum("cfg-max-content", 100000),
|
||
title_boost: _getFieldNum("cfg-title-boost", 3.0),
|
||
tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
|
||
prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
|
||
recent_files_limit: _getFieldNum("cfg-recent-limit", 20),
|
||
watcher_enabled: _getCheckbox("cfg-watcher-enabled"),
|
||
watcher_use_polling: _getCheckbox("cfg-watcher-polling"),
|
||
watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0),
|
||
watcher_debounce: _getFieldNum("cfg-watcher-debounce", 2.0),
|
||
};
|
||
try {
|
||
const res = await fetch("/api/config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
if (res.ok) {
|
||
showToast("Configuration backend sauvegardée", "success");
|
||
} else {
|
||
const errorData = await res.json().catch(() => ({}));
|
||
showToast(errorData.detail || "Erreur de sauvegarde", "error");
|
||
}
|
||
} catch (err) {
|
||
console.error("Failed to save backend config:", err);
|
||
showToast("Erreur de sauvegarde", "error");
|
||
}
|
||
}
|
||
|
||
async function forceReindex() {
|
||
const btn = document.getElementById("cfg-reindex");
|
||
if (btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = "Réindexation...";
|
||
}
|
||
try {
|
||
await api("/api/index/reload");
|
||
showToast("Réindexation terminée", "success");
|
||
loadDiagnostics();
|
||
await Promise.all([loadVaults(), loadTags()]);
|
||
} catch (err) {
|
||
console.error("Reindex error:", err);
|
||
showToast("Erreur de réindexation", "error");
|
||
} finally {
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.textContent = "Forcer réindexation";
|
||
}
|
||
}
|
||
}
|
||
|
||
async function resetConfigDefaults() {
|
||
// Reset frontend
|
||
localStorage.removeItem(_FRONTEND_CONFIG_KEY);
|
||
// Reset backend
|
||
try {
|
||
await fetch("/api/config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
search_workers: 2,
|
||
debounce_ms: 300,
|
||
results_per_page: 50,
|
||
min_query_length: 2,
|
||
search_timeout_ms: 30000,
|
||
max_content_size: 100000,
|
||
title_boost: 3.0,
|
||
path_boost: 1.5,
|
||
tag_boost: 2.0,
|
||
prefix_max_expansions: 50,
|
||
snippet_context_chars: 120,
|
||
max_snippet_highlights: 5,
|
||
}),
|
||
});
|
||
} catch (err) {
|
||
console.error("Reset config error:", err);
|
||
}
|
||
loadConfigFields();
|
||
showToast("Configuration réinitialisée", "success");
|
||
}
|
||
|
||
async function loadDiagnostics() {
|
||
const container = document.getElementById("config-diagnostics");
|
||
if (!container) return;
|
||
container.innerHTML = '<div class="config-diag-loading">Chargement...</div>';
|
||
try {
|
||
const data = await api("/api/diagnostics");
|
||
renderDiagnostics(container, data);
|
||
} catch (err) {
|
||
container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
|
||
}
|
||
}
|
||
|
||
function renderDiagnostics(container, data) {
|
||
container.innerHTML = "";
|
||
const sections = [
|
||
{
|
||
title: "Index",
|
||
rows: [
|
||
["Fichiers indexés", data.index.total_files],
|
||
["Tags uniques", data.index.total_tags],
|
||
["Vaults", Object.keys(data.index.vaults).join(", ")],
|
||
],
|
||
},
|
||
{
|
||
title: "Index inversé",
|
||
rows: [
|
||
["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()],
|
||
["Postings total", data.inverted_index.total_postings.toLocaleString()],
|
||
["Documents", data.inverted_index.documents],
|
||
["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"],
|
||
["Stale", data.inverted_index.is_stale ? "Oui" : "Non"],
|
||
],
|
||
},
|
||
{
|
||
title: "Moteur de recherche",
|
||
rows: [
|
||
["Executor actif", data.search_executor.active ? "Oui" : "Non"],
|
||
["Workers max", data.search_executor.max_workers],
|
||
],
|
||
},
|
||
];
|
||
sections.forEach((section) => {
|
||
const div = document.createElement("div");
|
||
div.className = "config-diag-section";
|
||
const title = document.createElement("div");
|
||
title.className = "config-diag-section-title";
|
||
title.textContent = section.title;
|
||
div.appendChild(title);
|
||
section.rows.forEach(([label, value]) => {
|
||
const row = document.createElement("div");
|
||
row.className = "config-diag-row";
|
||
row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
|
||
div.appendChild(row);
|
||
});
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
// --- About Section ---
|
||
|
||
function loadAbout() {
|
||
const container = document.getElementById("config-about");
|
||
if (!container) return;
|
||
|
||
// Fetch health info for version
|
||
api("/api/health").then((health) => {
|
||
container.innerHTML = "";
|
||
|
||
const sections = [
|
||
{
|
||
title: "Application",
|
||
rows: [
|
||
["Nom", "ObsiGate"],
|
||
["Version", APP_VERSION],
|
||
["Version API", health.version || "—"],
|
||
["Statut", health.status || "—"],
|
||
],
|
||
},
|
||
{
|
||
title: "Environnement",
|
||
rows: [
|
||
["Vaults configurés", health.vaults || "—"],
|
||
["Fichiers indexés", health.total_files || "—"],
|
||
["Navigateur", navigator.userAgent.split(" ").pop()],
|
||
["Plateforme", navigator.platform || "—"],
|
||
["Langue", navigator.language || "—"],
|
||
],
|
||
},
|
||
{
|
||
title: "Composants",
|
||
rows: [
|
||
["Backend", "FastAPI (Python)"],
|
||
["Rendu Markdown", "mistune"],
|
||
["Surveillance fichiers", "watchdog"],
|
||
["Frontend", "Vanilla JavaScript"],
|
||
["Icônes", "Lucide Icons"],
|
||
["Coloration syntaxe", "highlight.js"],
|
||
["Éditeur", "CodeMirror 6"],
|
||
],
|
||
},
|
||
];
|
||
|
||
sections.forEach((section) => {
|
||
const div = document.createElement("div");
|
||
div.className = "config-diag-section";
|
||
const title = document.createElement("div");
|
||
title.className = "config-diag-section-title";
|
||
title.textContent = section.title;
|
||
div.appendChild(title);
|
||
section.rows.forEach(([label, value]) => {
|
||
const row = document.createElement("div");
|
||
row.className = "config-diag-row";
|
||
row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
|
||
div.appendChild(row);
|
||
});
|
||
container.appendChild(div);
|
||
});
|
||
}).catch(() => {
|
||
container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
|
||
});
|
||
}
|
||
|
||
// --- Hidden Files Configuration ---
|
||
|
||
async function loadHiddenFilesSettings() {
|
||
const container = document.getElementById("hidden-files-vault-list");
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Chargement...</div>';
|
||
|
||
try {
|
||
const settings = await api("/api/vaults/settings/all");
|
||
renderHiddenFilesSettings(container, settings);
|
||
} catch (err) {
|
||
console.error("Failed to load hidden files settings:", err);
|
||
container.innerHTML = '<div style="padding:12px;color:var(--error)">Erreur de chargement</div>';
|
||
}
|
||
}
|
||
|
||
function renderHiddenFilesSettings(container, allSettings) {
|
||
container.innerHTML = "";
|
||
|
||
if (!allVaults || allVaults.length === 0) {
|
||
container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Aucun vault configuré</div>';
|
||
return;
|
||
}
|
||
|
||
allVaults.forEach((vault) => {
|
||
const settings = allSettings[vault.name] || { hideHiddenFiles: false };
|
||
|
||
const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name });
|
||
|
||
// Vault header
|
||
const header = el("div", { class: "hidden-files-vault-header" }, [el("h3", {}, [document.createTextNode(vault.name)]), el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")])]);
|
||
|
||
// Hide hidden files toggle
|
||
const toggleRow = el("div", { class: "config-row" }, [
|
||
el("label", { class: "config-label", for: `hide-hidden-${vault.name}` }, [document.createTextNode("Masquer les fichiers/dossiers cachés")]),
|
||
el("label", { class: "config-toggle" }, [
|
||
el("input", {
|
||
type: "checkbox",
|
||
id: `hide-hidden-${vault.name}`,
|
||
"data-vault": vault.name,
|
||
checked: settings.hideHiddenFiles ? "true" : false,
|
||
}),
|
||
el("span", { class: "config-toggle-slider" }),
|
||
]),
|
||
el("span", { class: "config-hint" }, [document.createTextNode("Masquer les fichiers/dossiers commençant par un point dans l'interface (ils restent indexés et cherchables)")]),
|
||
]);
|
||
|
||
vaultCard.appendChild(header);
|
||
vaultCard.appendChild(toggleRow);
|
||
|
||
container.appendChild(vaultCard);
|
||
});
|
||
}
|
||
|
||
async function saveHiddenFilesSettings() {
|
||
const btn = document.getElementById("cfg-save-hidden-files");
|
||
if (btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = "Sauvegarde...";
|
||
}
|
||
|
||
try {
|
||
const vaultCards = document.querySelectorAll(".hidden-files-vault-card");
|
||
const promises = [];
|
||
|
||
vaultCards.forEach((card) => {
|
||
const vaultName = card.dataset.vault;
|
||
const hideHiddenFiles = document.getElementById(`hide-hidden-${vaultName}`)?.checked || false;
|
||
|
||
const settings = {
|
||
hideHiddenFiles,
|
||
};
|
||
|
||
promises.push(
|
||
api(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(settings),
|
||
}),
|
||
);
|
||
});
|
||
|
||
await Promise.all(promises);
|
||
|
||
// Reload vault settings to update the cache
|
||
await loadVaultSettings();
|
||
|
||
showToast("✓ Paramètres sauvegardés", "success");
|
||
|
||
// Refresh the UI to apply the filter
|
||
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
|
||
} catch (err) {
|
||
console.error("Failed to save hidden files settings:", err);
|
||
const errorMsg = err.message || "Erreur inconnue";
|
||
showToast(`Erreur: ${errorMsg}`, "error");
|
||
} finally {
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.textContent = "💾 Sauvegarder";
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Webhooks UI ──
|
||
async function loadWebhooksUI() {
|
||
const list = document.getElementById("webhooks-list");
|
||
if (!list) return;
|
||
try {
|
||
const webhooks = await api("/api/webhooks");
|
||
renderWebhooksUI(webhooks);
|
||
} catch { list.innerHTML = '<div class="config-description">Admin uniquement</div>'; }
|
||
}
|
||
function renderWebhooksUI(webhooks) {
|
||
const list = document.getElementById("webhooks-list");
|
||
if (!list) return;
|
||
if (!webhooks.length) { list.innerHTML = '<div class="config-description">Aucun webhook configuré.</div>'; return; }
|
||
list.innerHTML = webhooks.map(w => `
|
||
<div class="webhook-item">
|
||
<span class="webhook-name">${escapeHtml(w.name)}</span>
|
||
<span class="webhook-url">${escapeHtml(w.url)}</span>
|
||
<span class="webhook-events">${(w.events||[]).join(", ")}</span>
|
||
<button class="webhook-delete" data-id="${w.id}">✕</button>
|
||
</div>
|
||
`).join("");
|
||
list.querySelectorAll(".webhook-delete").forEach(b => b.addEventListener("click", async () => {
|
||
await api(`/api/webhooks/${b.dataset.id}`, { method: "DELETE" });
|
||
loadWebhooksUI();
|
||
}));
|
||
}
|
||
document.addEventListener("click", function(e) {
|
||
if (e.target.id === "webhook-add-btn") {
|
||
const name = document.getElementById("webhook-name-input").value.trim();
|
||
const url = document.getElementById("webhook-url-input").value.trim();
|
||
if (!url) { showToast("URL requise", "error"); return; }
|
||
api("/api/webhooks", { method: "POST", body: JSON.stringify({ name: name || "Webhook", url, events: ["file_created","file_deleted","file_modified","file_renamed"] }) }).then(() => { loadWebhooksUI(); document.getElementById("webhook-name-input").value = ""; document.getElementById("webhook-url-input").value = ""; }).catch(err => showToast(err.message, "error"));
|
||
}
|
||
});
|
||
|
||
// ── Shares UI ──
|
||
async function loadSharesUI() {
|
||
const list = document.getElementById("shares-list");
|
||
if (!list) return;
|
||
try {
|
||
const shares = await api("/api/shares");
|
||
renderSharesUI(shares);
|
||
} catch { list.innerHTML = '<div class="config-description">Chargement...</div>'; }
|
||
}
|
||
function renderSharesUI(shares) {
|
||
const list = document.getElementById("shares-list");
|
||
if (!list) return;
|
||
if (!shares.length) { list.innerHTML = '<div class="config-description">Aucun partage actif.</div>'; return; }
|
||
list.innerHTML = shares.map(s => `
|
||
<div class="share-item">
|
||
<span class="share-path">${escapeHtml(s.vault)}/${escapeHtml(s.path)}</span>
|
||
<span class="share-url"><a href="${s.url}" target="_blank">${s.url}</a></span>
|
||
<span class="share-meta">${s.access_count} vue(s)${s.expires_at ? ' · Expire' : ''}</span>
|
||
<button class="share-revoke" data-id="${s.id}">Révoquer</button>
|
||
</div>
|
||
`).join("");
|
||
list.querySelectorAll(".share-revoke").forEach(b => b.addEventListener("click", async () => {
|
||
await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
|
||
loadSharesUI();
|
||
}));
|
||
}
|
||
|
||
// ── Share Dialog (professional) ──
|
||
async function openShareDialog(vault, path) {
|
||
// First check if already shared
|
||
let existingShare = null;
|
||
try {
|
||
const shares = await api("/api/shares");
|
||
existingShare = shares.find(s => s.vault === vault && s.path === path);
|
||
} catch (e) { /* ignore */ }
|
||
|
||
const div = document.createElement("div");
|
||
div.className = "share-dialog-overlay";
|
||
|
||
const renderContent = () => {
|
||
if (existingShare) {
|
||
const url = window.location.origin + existingShare.url;
|
||
const expiresInfo = existingShare.expires_at
|
||
? `<p style="font-size:0.75rem;color:var(--text-muted)">Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}</p>`
|
||
: '<p style="font-size:0.75rem;color:var(--text-muted)">Sans expiration</p>';
|
||
div.innerHTML = `
|
||
<div class="share-dialog">
|
||
<h3>📤 Document partagé</h3>
|
||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:4px">${escapeHtml(vault)}/${escapeHtml(path)}</p>
|
||
${expiresInfo}
|
||
<p style="font-size:0.75rem;color:var(--text-muted);margin-bottom:8px">${existingShare.access_count} vue(s)</p>
|
||
<input type="text" class="share-url-input" value="${url}" readonly onclick="this.select()">
|
||
<div class="share-dialog-actions">
|
||
<button class="share-copy-btn">📋 Copier le lien</button>
|
||
<button class="share-revoke-btn">🗑 Révoquer</button>
|
||
<button class="share-close-btn">Fermer</button>
|
||
</div>
|
||
</div>`;
|
||
div.querySelector(".share-copy-btn").addEventListener("click", async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(url);
|
||
} catch (e) {
|
||
// Fallback for non-HTTPS contexts
|
||
const ta = document.createElement("textarea");
|
||
ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px";
|
||
document.body.appendChild(ta); ta.select(); document.execCommand("copy");
|
||
document.body.removeChild(ta);
|
||
}
|
||
showToast("Lien copié !", "success");
|
||
div.remove();
|
||
});
|
||
div.querySelector(".share-revoke-btn").addEventListener("click", async () => {
|
||
try {
|
||
await api(`/api/share/${existingShare.id}`, { method: "DELETE" });
|
||
showToast("Partage révoqué", "success");
|
||
existingShare = null;
|
||
renderContent();
|
||
} catch (err) { showToast("Erreur: " + err.message, "error"); }
|
||
});
|
||
} else {
|
||
div.innerHTML = `
|
||
<div class="share-dialog">
|
||
<h3>📤 Partager ce document</h3>
|
||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:12px">${escapeHtml(vault)}/${escapeHtml(path)}</p>
|
||
<p style="font-size:0.8rem;margin-bottom:8px">Ce lien sera accessible <strong>publiquement, sans authentification</strong>.</p>
|
||
<label style="font-size:0.8rem;display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
Expiration :
|
||
<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">
|
||
<option value="">Jamais</option>
|
||
<option value="1">1 heure</option>
|
||
<option value="24">24 heures</option>
|
||
<option value="168">7 jours</option>
|
||
<option value="720">30 jours</option>
|
||
</select>
|
||
</label>
|
||
<div class="share-dialog-actions">
|
||
<button class="share-create-btn">🔗 Créer le lien</button>
|
||
<button class="share-close-btn">Fermer</button>
|
||
</div>
|
||
</div>`;
|
||
div.querySelector(".share-create-btn").addEventListener("click", async () => {
|
||
try {
|
||
const expiry = document.getElementById("share-expiry")?.value;
|
||
const share = await api(`/api/share/${encodeURIComponent(vault)}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }),
|
||
});
|
||
existingShare = share;
|
||
renderContent();
|
||
showToast("Lien créé !", "success");
|
||
} catch (err) { showToast("Erreur: " + err.message, "error"); }
|
||
});
|
||
}
|
||
div.querySelector(".share-close-btn").addEventListener("click", () => div.remove());
|
||
div.addEventListener("click", (e) => { if (e.target === div) div.remove(); });
|
||
};
|
||
|
||
renderContent();
|
||
document.body.appendChild(div);
|
||
}
|
||
|
||
function renderConfigFilters() {
|
||
const config = TagFilterService.getConfig();
|
||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||
const container = document.getElementById("config-filters-list");
|
||
|
||
container.innerHTML = "";
|
||
|
||
filters.forEach((filter, index) => {
|
||
const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
|
||
el("span", {}, [document.createTextNode(filter.pattern)]),
|
||
el(
|
||
"button",
|
||
{
|
||
class: "config-filter-toggle",
|
||
title: filter.enabled ? "Désactiver" : "Activer",
|
||
type: "button",
|
||
},
|
||
[document.createTextNode(filter.enabled ? "✓" : "○")],
|
||
),
|
||
el(
|
||
"button",
|
||
{
|
||
class: "config-filter-remove",
|
||
title: "Supprimer",
|
||
type: "button",
|
||
},
|
||
[document.createTextNode("×")],
|
||
),
|
||
]);
|
||
|
||
const toggleBtn = badge.querySelector(".config-filter-toggle");
|
||
const removeBtn = badge.querySelector(".config-filter-remove");
|
||
|
||
toggleBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
toggleConfigFilter(index);
|
||
});
|
||
|
||
removeBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
removeConfigFilter(index);
|
||
});
|
||
|
||
container.appendChild(badge);
|
||
});
|
||
}
|
||
|
||
function toggleConfigFilter(index) {
|
||
const config = TagFilterService.getConfig();
|
||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||
if (filters[index]) {
|
||
filters[index].enabled = !filters[index].enabled;
|
||
config.tagFilters = filters;
|
||
TagFilterService.saveConfig(config);
|
||
renderConfigFilters();
|
||
refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
|
||
}
|
||
}
|
||
|
||
function removeConfigFilter(index) {
|
||
const config = TagFilterService.getConfig();
|
||
let filters = config.tagFilters || TagFilterService.defaultFilters;
|
||
filters = filters.filter((_, i) => i !== index);
|
||
config.tagFilters = filters;
|
||
TagFilterService.saveConfig(config);
|
||
renderConfigFilters();
|
||
refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
|
||
}
|
||
|
||
function addConfigFilter() {
|
||
const input = document.getElementById("config-pattern-input");
|
||
const pattern = input.value.trim();
|
||
|
||
if (!pattern) return;
|
||
|
||
const regex = TagFilterService.patternToRegex(pattern);
|
||
const config = TagFilterService.getConfig();
|
||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||
|
||
const newFilter = { pattern, regex, enabled: true };
|
||
filters.push(newFilter);
|
||
config.tagFilters = filters;
|
||
TagFilterService.saveConfig(config);
|
||
|
||
input.value = "";
|
||
renderConfigFilters();
|
||
refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err));
|
||
updateRegexPreview();
|
||
}
|
||
|
||
function updateRegexPreview() {
|
||
const input = document.getElementById("config-pattern-input");
|
||
const preview = document.getElementById("config-regex-preview");
|
||
const code = document.getElementById("config-regex-code");
|
||
const pattern = input.value.trim();
|
||
|
||
if (pattern) {
|
||
const regex = TagFilterService.patternToRegex(pattern);
|
||
code.textContent = `^${regex}$`;
|
||
preview.style.display = "block";
|
||
} else {
|
||
preview.style.display = "none";
|
||
}
|
||
}
|
||
|
||
|
||
export {
|
||
initSidebarTabs,
|
||
initConfigModal,
|
||
initConfigModal as initConfigPanel,
|
||
initHelpModal,
|
||
closeHelpModal,
|
||
initRecentTab,
|
||
loadAbout as initAboutSection,
|
||
loadHiddenFilesSettings as initHiddenFilesConfig,
|
||
};
|