1013 lines
34 KiB
JavaScript
1013 lines
34 KiB
JavaScript
1|// config.js — extracted from app.js (3872-4865)
|
||
import { state } from './state.js';
|
||
|
||
let _recentTimestampTimer = null;
|
||
let _recentFilesCache = [];
|
||
let _recentRefreshTimer = null;
|
||
|
||
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);
|
||
state.allVaults.forEach((v) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = v.name;
|
||
opt.textContent = v.name;
|
||
select.appendChild(opt);
|
||
});
|
||
syncVaultSelectors();
|
||
}
|
||
|
||
function initRecentTab() {
|
||
const select = document.getElementById("recent-vault-filter");
|
||
if (select) {
|
||
select.addEventListener("change", async () => {
|
||
const val = select.value || "all";
|
||
await setSelectedVaultContext(val, { focusVault: val !== "all" });
|
||
});
|
||
}
|
||
// Periodic timestamp refresh (every 60s)
|
||
_recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Sidebar tabs
|
||
// ---------------------------------------------------------------------------
|
||
function initSidebarTabs() {
|
||
document.querySelectorAll(".sidebar-tab").forEach((tab) => {
|
||
tab.addEventListener("click", () => switchSidebarTab(tab.dataset.tab));
|
||
});
|
||
}
|
||
|
||
function switchSidebarTab(tab) {
|
||
state.activeSidebarTab = tab;
|
||
document.querySelectorAll(".sidebar-tab").forEach((btn) => {
|
||
const isActive = btn.dataset.tab === tab;
|
||
btn.classList.toggle("active", isActive);
|
||
btn.setAttribute("aria-selected", isActive ? "true" : "false");
|
||
});
|
||
document.querySelectorAll(".sidebar-tab-panel").forEach((panel) => {
|
||
const isActive = panel.id === `sidebar-panel-${tab}`;
|
||
panel.classList.toggle("active", isActive);
|
||
});
|
||
const filterInput = document.getElementById("sidebar-filter-input");
|
||
if (filterInput) {
|
||
const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" };
|
||
filterInput.placeholder = placeholders[tab] || "";
|
||
}
|
||
const query = filterInput ? (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", state.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 || state.allVaults.length === 0) {
|
||
container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Aucun vault configuré</div>';
|
||
return;
|
||
}
|
||
|
||
state.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,
|
||
};
|