ObsiGate/frontend/js/config.js
Bruno Charest c7378e4f12
All checks were successful
CI / lint (push) Successful in 16s
CI / security (push) Successful in 10s
CI / test (push) Successful in 22s
CI / build (push) Successful in 3s
fix: export icon from viewer.js, import syncVaultSelectors in config.js, cleanup dupes
2026-05-28 20:26:04 -04:00

1016 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// config.js — extracted from app.js (3872-4865)
import { api } from './auth.js';
import { state } from './state.js';
import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadTags } from './sidebar.js';
import { escapeHtml, safeCreateIcons } from './utils.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 (state.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);
}
}
}
});
}
export 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 ? (state.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 (!state.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,
};