CI / lint (push) Successful in 25s
CI / security (push) Successful in 19s
CI / test (push) Successful in 29s
CI / build (push) Successful in 8s
CI / e2e (push) Failing after 21s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
1571 lines
56 KiB
JavaScript
1571 lines
56 KiB
JavaScript
// config.js — extracted from app.js (3872-4865)
|
||
import { api, AuthManager } from './auth.js';
|
||
import { state } from './state.js';
|
||
import { el, icon, openFile } from './viewer.js';
|
||
import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadVaultSettings, loadTags, TagFilterService, refreshSidebarTreePreservingState } from './sidebar.js';
|
||
import { escapeHtml, safeCreateIcons } from './utils.js';
|
||
import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js';
|
||
import { t, setLocale, getLocale } from './i18n.js';
|
||
|
||
let _recentTimestampTimer = null;
|
||
let _recentFilesCache = [];
|
||
let _recentRefreshTimer = null;
|
||
|
||
export 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 t("config.just_now");
|
||
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;
|
||
|
||
let helpNavInitialized = false;
|
||
|
||
openBtn.addEventListener("click", () => {
|
||
modal.classList.add("active");
|
||
closeHeaderMenu();
|
||
safeCreateIcons();
|
||
// Reset TOC to CSS default (mobile: hidden, desktop: visible)
|
||
const nav = document.getElementById("help-nav");
|
||
if (nav) nav.style.display = '';
|
||
if (!helpNavInitialized) {
|
||
initHelpNavigation();
|
||
helpNavInitialized = true;
|
||
}
|
||
});
|
||
|
||
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 initEditorPocBtn() {
|
||
var btn = document.getElementById("editor-poc-btn");
|
||
if (!btn) return;
|
||
btn.addEventListener("click", function() {
|
||
window.open("/editor-poc", "_blank");
|
||
});
|
||
}
|
||
|
||
function initHelpNavigation() {
|
||
const helpBody = document.getElementById("help-body");
|
||
const navList = document.getElementById("help-nav-list");
|
||
const nav = document.getElementById("help-nav");
|
||
const searchInput = document.getElementById("help-nav-search");
|
||
const helpContent = document.querySelector(".help-content");
|
||
|
||
if (!helpBody || !navList || !nav) return;
|
||
|
||
const allLinks = [...navList.querySelectorAll(".help-nav-link")];
|
||
const allSections = [...document.querySelectorAll(".help-section")];
|
||
const sectionMap = {};
|
||
allSections.forEach(s => { sectionMap[s.id] = s; });
|
||
|
||
// Build text index for content search
|
||
const sectionTextCache = {};
|
||
allSections.forEach(s => {
|
||
sectionTextCache[s.id] = (s.textContent || '').toLowerCase();
|
||
});
|
||
|
||
// ── Mobile: simple display toggle ──
|
||
function isMobile() { return window.innerWidth <= 768; }
|
||
function toggleTOC() {
|
||
if (nav.style.display === 'none' || nav.style.display === '') {
|
||
nav.style.display = 'flex';
|
||
if (searchInput) setTimeout(() => searchInput.focus(), 150);
|
||
} else {
|
||
nav.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// ── Scroll to section ──
|
||
function scrollToSection(targetId) {
|
||
const section = sectionMap[targetId];
|
||
if (!section) return;
|
||
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
section.setAttribute("tabindex", "-1");
|
||
if (isMobile()) nav.style.display = 'none';
|
||
}
|
||
|
||
// ── Focus the content pane ──
|
||
function focusContent() {
|
||
const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
|
||
if (activeLink) {
|
||
const targetId = activeLink.getAttribute("href").substring(1);
|
||
const section = sectionMap[targetId];
|
||
if (section) {
|
||
const h2 = section.querySelector("h2, h3");
|
||
if (h2) { h2.setAttribute("tabindex", "-1"); h2.focus({ preventScroll: true }); return; }
|
||
}
|
||
}
|
||
if (helpContent) { helpContent.setAttribute("tabindex", "-1"); helpContent.focus({ preventScroll: true }); }
|
||
}
|
||
|
||
// ── Focus TOC active link ──
|
||
function focusTOC() {
|
||
if (isMobile() && (nav.style.display === 'none' || nav.style.display === '')) {
|
||
nav.style.display = 'flex';
|
||
}
|
||
const activeLink = navList.querySelector(".help-nav-link.active:not(.hidden)");
|
||
if (activeLink) { activeLink.focus(); return; }
|
||
const firstLink = navList.querySelector(".help-nav-link:not(.hidden)");
|
||
if (firstLink) firstLink.focus();
|
||
}
|
||
|
||
// ── Nav link click handler ──
|
||
allLinks.forEach((link) => {
|
||
link.addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
const targetId = link.getAttribute("href").substring(1);
|
||
scrollToSection(targetId);
|
||
});
|
||
});
|
||
|
||
// ── Keyboard navigation inside nav list ──
|
||
navList.addEventListener("keydown", (e) => {
|
||
const items = [...navList.querySelectorAll(".help-nav-link:not(.hidden)")];
|
||
if (!items.length) return;
|
||
const currentIdx = items.indexOf(document.activeElement);
|
||
|
||
if (e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
const next = (currentIdx + 1) % items.length;
|
||
items[next].focus();
|
||
} else if (e.key === "ArrowUp") {
|
||
e.preventDefault();
|
||
const prev = (currentIdx - 1 + items.length) % items.length;
|
||
items[prev].focus();
|
||
} else if (e.key === "ArrowRight") {
|
||
e.preventDefault();
|
||
focusContent();
|
||
} else if (e.key === "ArrowLeft") {
|
||
e.preventDefault();
|
||
const prev = (currentIdx - 1 + items.length) % items.length;
|
||
items[prev].focus();
|
||
} else if (e.key === "Home") {
|
||
e.preventDefault();
|
||
items[0].focus();
|
||
} else if (e.key === "End") {
|
||
e.preventDefault();
|
||
items[items.length - 1].focus();
|
||
} else if (e.key === "Enter" && currentIdx >= 0) {
|
||
e.preventDefault();
|
||
const targetId = items[currentIdx].getAttribute("href").substring(1);
|
||
scrollToSection(targetId);
|
||
setTimeout(() => focusContent(), 400);
|
||
}
|
||
});
|
||
|
||
navList.querySelectorAll("li").forEach((li) => li.setAttribute("role", "none"));
|
||
|
||
// ── Keyboard navigation in content pane ──
|
||
if (helpContent) {
|
||
helpContent.setAttribute("tabindex", "-1");
|
||
helpContent.addEventListener("keydown", (e) => {
|
||
if (e.key === "ArrowLeft") {
|
||
e.preventDefault();
|
||
focusTOC();
|
||
} else if (e.key === "ArrowUp" && document.activeElement === helpContent) {
|
||
e.preventDefault();
|
||
focusTOC();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Keyboard navigation in search input ──
|
||
if (searchInput) {
|
||
searchInput.addEventListener("keydown", (e) => {
|
||
if (e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
const firstLink = navList.querySelector(".help-nav-link:not(.hidden)");
|
||
if (firstLink) firstLink.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Search: filter TOC + hide non-matching sections + highlight text ──
|
||
let currentQuery = "";
|
||
if (searchInput) {
|
||
searchInput.addEventListener("input", () => {
|
||
currentQuery = searchInput.value.toLowerCase().trim();
|
||
applySearchFilter();
|
||
});
|
||
searchInput.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && currentQuery) {
|
||
searchInput.value = "";
|
||
currentQuery = "";
|
||
applySearchFilter();
|
||
}
|
||
});
|
||
}
|
||
|
||
function clearHighlights() {
|
||
allSections.forEach(section => {
|
||
section.querySelectorAll(".help-search-highlight").forEach(el => {
|
||
const parent = el.parentNode;
|
||
parent.replaceChild(document.createTextNode(el.textContent), el);
|
||
parent.normalize();
|
||
});
|
||
});
|
||
}
|
||
|
||
function highlightText(root, query) {
|
||
if (!query || query.length < 2) return;
|
||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
|
||
const textNodes = [];
|
||
while (walker.nextNode()) {
|
||
const node = walker.currentNode;
|
||
// Skip nodes inside script, style, or already-highlighted
|
||
if (node.parentElement && !node.parentElement.closest("script, style, .help-search-highlight, .help-nav")) {
|
||
textNodes.push(node);
|
||
}
|
||
}
|
||
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||
textNodes.forEach(node => {
|
||
const text = node.textContent;
|
||
if (!regex.test(text)) return;
|
||
regex.lastIndex = 0;
|
||
const frag = document.createDocumentFragment();
|
||
let lastIdx = 0;
|
||
let match;
|
||
while ((match = regex.exec(text)) !== null) {
|
||
if (match.index > lastIdx) {
|
||
frag.appendChild(document.createTextNode(text.slice(lastIdx, match.index)));
|
||
}
|
||
const mark = document.createElement("mark");
|
||
mark.className = "help-search-highlight";
|
||
mark.textContent = match[0];
|
||
frag.appendChild(mark);
|
||
lastIdx = regex.lastIndex;
|
||
if (match[0].length === 0) regex.lastIndex++;
|
||
}
|
||
if (lastIdx < text.length) {
|
||
frag.appendChild(document.createTextNode(text.slice(lastIdx)));
|
||
}
|
||
node.parentNode.replaceChild(frag, node);
|
||
});
|
||
}
|
||
|
||
function applySearchFilter() {
|
||
const q = currentQuery;
|
||
// Clear previous highlights
|
||
clearHighlights();
|
||
// Filter TOC links
|
||
allLinks.forEach((link) => {
|
||
const text = link.textContent.toLowerCase();
|
||
if (!q || text.includes(q)) {
|
||
link.classList.remove("hidden");
|
||
} else {
|
||
link.classList.add("hidden");
|
||
}
|
||
});
|
||
// Filter content sections + highlight
|
||
allSections.forEach((section) => {
|
||
if (!q) {
|
||
section.classList.remove("help-hidden");
|
||
return;
|
||
}
|
||
const sectionText = sectionTextCache[section.id] || '';
|
||
const sectionTitle = (section.querySelector("h2, h3")?.textContent || '').toLowerCase();
|
||
if (sectionTitle.includes(q) || sectionText.includes(q)) {
|
||
section.classList.remove("help-hidden");
|
||
// Highlight matching text in this section
|
||
highlightText(section, q);
|
||
} else {
|
||
section.classList.add("help-hidden");
|
||
}
|
||
});
|
||
updateActiveLink();
|
||
ensureNoResultsMessage(q);
|
||
}
|
||
|
||
function ensureNoResultsMessage(q) {
|
||
const existing = document.getElementById("help-no-results");
|
||
const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
|
||
if (q && visibleSections.length === 0) {
|
||
if (!existing) {
|
||
const msg = document.createElement("div");
|
||
msg.id = "help-no-results";
|
||
msg.style.cssText = "padding:40px;text-align:center;color:var(--text-muted);font-size:0.9rem";
|
||
msg.innerHTML = `<div style="font-size:2rem;margin-bottom:8px">🔍</div><p>Aucun résultat pour <strong>"${q}"</strong></p><p style="font-size:0.75rem">Essayez un autre terme</p>`;
|
||
if (helpContent) helpContent.appendChild(msg);
|
||
}
|
||
} else if (existing) {
|
||
existing.remove();
|
||
}
|
||
}
|
||
|
||
// ── Scroll spy ──
|
||
function updateActiveLink() {
|
||
const visibleLinks = allLinks.filter(l => !l.classList.contains("hidden"));
|
||
const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
|
||
let activeId = null;
|
||
for (const section of visibleSections) {
|
||
const rect = section.getBoundingClientRect();
|
||
const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
|
||
if (rect.top < (contentRect ? contentRect.top + 120 : 200)) {
|
||
activeId = section.id;
|
||
}
|
||
}
|
||
visibleLinks.forEach((link) => {
|
||
const isMatch = link.getAttribute("href") === `#${activeId}`;
|
||
link.classList.toggle("active", isMatch);
|
||
});
|
||
}
|
||
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
let bestEntry = null;
|
||
for (const entry of entries) {
|
||
if (entry.isIntersecting) {
|
||
if (!bestEntry || entry.boundingClientRect.top < bestEntry.boundingClientRect.top) {
|
||
bestEntry = entry;
|
||
}
|
||
}
|
||
}
|
||
if (bestEntry) {
|
||
const id = bestEntry.target.getAttribute("id");
|
||
allLinks.forEach((link) => {
|
||
const isMatch = link.getAttribute("href") === `#${id}`;
|
||
link.classList.toggle("active", isMatch);
|
||
});
|
||
}
|
||
},
|
||
{
|
||
root: helpBody,
|
||
rootMargin: "-10% 0px -75% 0px",
|
||
threshold: [0, 0.25, 0.5],
|
||
}
|
||
);
|
||
|
||
allSections.forEach((section) => observer.observe(section));
|
||
|
||
// ── Keyboard: Ctrl+↑/↓ to jump between sections ──
|
||
helpBody.addEventListener("keydown", (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
||
e.preventDefault();
|
||
const visibleSections = allSections.filter(s => !s.classList.contains("help-hidden"));
|
||
const currentSection = visibleSections.find((s) => {
|
||
const rect = s.getBoundingClientRect();
|
||
const contentRect = helpContent ? helpContent.getBoundingClientRect() : null;
|
||
return rect.top >= (contentRect ? contentRect.top : 0) && rect.top < (contentRect ? contentRect.top + 120 : 120);
|
||
});
|
||
if (!currentSection) return;
|
||
const currentIdx = visibleSections.indexOf(currentSection);
|
||
if (e.key === "ArrowDown" && currentIdx < visibleSections.length - 1) {
|
||
visibleSections[currentIdx + 1].scrollIntoView({ behavior: "smooth", block: "start" });
|
||
} else if (e.key === "ArrowUp" && currentIdx > 0) {
|
||
visibleSections[currentIdx - 1].scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
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);
|
||
|
||
// AI Keys
|
||
const saveAIKeysBtn = document.getElementById("cfg-save-ai-keys");
|
||
if (saveAIKeysBtn) saveAIKeysBtn.addEventListener("click", saveAIKeys);
|
||
const testAIKeysBtn = document.getElementById("cfg-test-ai-keys");
|
||
if (testAIKeysBtn) testAIKeysBtn.addEventListener("click", testAIKeys);
|
||
// Load existing keys on modal open
|
||
document.getElementById("config-open-btn").addEventListener("click", loadAIKeys);
|
||
|
||
// Profile menu button: open config modal and scroll to profile section
|
||
var profileBtn = document.getElementById("profile-open-btn");
|
||
if (profileBtn) {
|
||
profileBtn.addEventListener("click", function() {
|
||
var modal = document.getElementById("config-modal");
|
||
if (modal) modal.classList.add("active");
|
||
setTimeout(function() {
|
||
var el = document.getElementById("cfg-profile");
|
||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
||
}, 300);
|
||
});
|
||
}
|
||
|
||
// Logout button — handled inline in index.html (onclick)
|
||
|
||
// Config nav search
|
||
var cfgSearch = document.getElementById("config-nav-search");
|
||
if (cfgSearch) {
|
||
cfgSearch.addEventListener("input", function() {
|
||
var q = cfgSearch.value.toLowerCase();
|
||
document.querySelectorAll("#config-scroll .config-section").forEach(function(s) {
|
||
var text = s.textContent.toLowerCase();
|
||
s.style.display = (q && !text.includes(q)) ? "none" : "";
|
||
});
|
||
document.querySelectorAll("#config-nav .help-nav-link").forEach(function(a) {
|
||
var li = a.closest("li");
|
||
if (li) li.style.display = (q && !a.textContent.toLowerCase().includes(q)) ? "none" : "";
|
||
});
|
||
});
|
||
}
|
||
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||
closeConfigModal();
|
||
}
|
||
});
|
||
|
||
// Load saved frontend config on startup
|
||
applyFrontendConfig();
|
||
|
||
// Init theme picker
|
||
initThemePicker();
|
||
|
||
// Init profile
|
||
initProfile();
|
||
}
|
||
|
||
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 */
|
||
}
|
||
}
|
||
|
||
export 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);
|
||
_setField("cfg-max-backups", data.max_backups_per_file || 10);
|
||
// 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(t("toast.config_client_saved"), "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),
|
||
max_backups_per_file: _getFieldNum("cfg-max-backups", 10),
|
||
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(t("toast.config_saved"), "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(t("config.save_error"), "error");
|
||
}
|
||
}
|
||
|
||
async function forceReindex() {
|
||
const btn = document.getElementById("cfg-reindex");
|
||
if (btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = t('config.reindexing');
|
||
}
|
||
try {
|
||
await api("/api/index/reload");
|
||
showToast(t("toast.reindex_done"), "success");
|
||
loadDiagnostics();
|
||
await Promise.all([loadVaults(), loadTags()]);
|
||
} catch (err) {
|
||
console.error("Reindex error:", err);
|
||
showToast(t("config.reindex_error"), "error");
|
||
} finally {
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.textContent = t('config.force_reindex');
|
||
}
|
||
}
|
||
}
|
||
|
||
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(t("toast.config_reset"), "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: [
|
||
[t("about.files_indexed"), data.index.total_files],
|
||
[t("dashboard.stats_tags"), data.index.total_tags],
|
||
[t("dashboard.stats_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: t("about.section_app"),
|
||
rows: [
|
||
[t("about.name"), "ObsiGate"],
|
||
[t("about.version"), state.APP_VERSION],
|
||
[t("about.api_version"), health.version || "—"],
|
||
[t("about.status"), health.status || "—"],
|
||
],
|
||
},
|
||
{
|
||
title: t("about.section_env"),
|
||
rows: [
|
||
[t("about.vaults_configured"), health.vaults || "—"],
|
||
[t("about.files_indexed"), health.total_files || "—"],
|
||
[t("about.browser"), navigator.userAgent.split(" ").pop()],
|
||
[t("about.platform"), navigator.platform || "—"],
|
||
[t("about.language"), navigator.language || "—"],
|
||
],
|
||
},
|
||
{
|
||
title: t("about.section_components"),
|
||
rows: [
|
||
[t("about.backend"), "FastAPI (Python)"],
|
||
[t("about.markdown_renderer"), "mistune"],
|
||
[t("about.file_watcher"), "watchdog"],
|
||
[t("about.frontend"), "Vanilla JavaScript"],
|
||
[t("about.icons"), "Lucide Icons"],
|
||
[t("about.syntax_highlight"), "highlight.js"],
|
||
[t("about.editor"), "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 = t("config.saving") + "...";
|
||
}
|
||
|
||
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(t("config.settings_saved"), "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 || t("config.error_unknown");
|
||
showToast(t("config.error_prefix") + ": " + errorMsg, "error");
|
||
} finally {
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.textContent = "💾 " + t("config.save");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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(t("config.url_required"), "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) ──
|
||
export 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(t("config.link_copied"), "success");
|
||
div.remove();
|
||
});
|
||
div.querySelector(".share-revoke-btn").addEventListener("click", async () => {
|
||
try {
|
||
await api(`/api/share/${existingShare.id}`, { method: "DELETE" });
|
||
showToast(t("config.share_revoked"), "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(t("config.link_created"), "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";
|
||
}
|
||
}
|
||
|
||
|
||
// ── AI Keys management ──
|
||
async function loadAIKeys() {
|
||
try {
|
||
const data = await api("/api/config/ai-keys");
|
||
["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY"].forEach(k => {
|
||
const id = "cfg-" + k.toLowerCase().replace(/_api_key/g, "") + "-key";
|
||
const input = document.getElementById(id);
|
||
if (input && data[k]) input.placeholder = data[k];
|
||
});
|
||
} catch(e) {}
|
||
}
|
||
|
||
async function saveAIKeys() {
|
||
const keys = {};
|
||
const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY" };
|
||
for (const [id, name] of Object.entries(map)) {
|
||
const input = document.getElementById(id);
|
||
if (input && input.value.trim()) keys[name] = input.value.trim();
|
||
}
|
||
if (!Object.keys(keys).length) { showToast(t("config.no_keys"), "warning"); return; }
|
||
try {
|
||
await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) });
|
||
showToast(t("config.api_keys_saved"), "success");
|
||
// Clear inputs
|
||
Object.keys(map).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
|
||
loadAIKeys();
|
||
} catch(e) { showToast("Erreur: " + e.message, "error"); }
|
||
}
|
||
|
||
async function testAIKeys() {
|
||
const status = document.getElementById("cfg-ai-status");
|
||
if (status) status.textContent = "Test en cours...";
|
||
try {
|
||
// Test connectivity
|
||
const results = await api("/api/config/ai-keys/test", { method: "POST" });
|
||
var msg = Object.entries(results).map(([k,v]) => k + ": " + v).join(" | ");
|
||
if (status) status.textContent = msg;
|
||
// Fetch models for configured providers
|
||
for (const p of ["deepseek","openrouter","gemini"]) {
|
||
if (results[p] === "ok") {
|
||
try {
|
||
const m = await api("/api/config/ai-models?provider=" + p);
|
||
var sel = document.getElementById("cfg-" + p + "-model");
|
||
if (sel && m.models && m.models.length) {
|
||
sel.innerHTML = '<option value="">-- Modele (' + m.models.length + ') --</option>';
|
||
m.models.forEach(function(md) {
|
||
sel.innerHTML += '<option value="' + md + '">' + md + '</option>';
|
||
});
|
||
}
|
||
} catch(e) { console.warn("Failed to fetch models for", p, e); }
|
||
}
|
||
}
|
||
} catch(e) {
|
||
if (status) status.textContent = "Erreur: " + e.message;
|
||
}
|
||
}
|
||
|
||
|
||
export {
|
||
initSidebarTabs,
|
||
initConfigModal,
|
||
initConfigModal as initConfigPanel,
|
||
initHelpModal,
|
||
closeHelpModal,
|
||
initRecentTab,
|
||
initEditorPocBtn,
|
||
loadAbout as initAboutSection,
|
||
loadHiddenFilesSettings as initHiddenFilesConfig,
|
||
initThemePicker,
|
||
initAboutModal,
|
||
initProfile,
|
||
switchSidebarTab,
|
||
};
|
||
|
||
// ── Theme Picker ─────────────────────────────────────────────────────
|
||
function initThemePicker() {
|
||
import('./themes.js').then(function(mod) {
|
||
var THEMES = mod.THEMES;
|
||
var applyTheme = mod.applyTheme;
|
||
var getCurrentTheme = mod.getCurrentTheme;
|
||
var getCurrentMode = mod.getCurrentMode;
|
||
|
||
var grid = document.getElementById('theme-grid');
|
||
if (!grid) return;
|
||
|
||
var themeKeys = Object.keys(THEMES);
|
||
|
||
function renderCards() {
|
||
var current = getCurrentTheme();
|
||
var mode = getCurrentMode();
|
||
grid.innerHTML = '';
|
||
themeKeys.forEach(function(key) {
|
||
var t = THEMES[key];
|
||
var v = t.modes[mode];
|
||
if (!v) return;
|
||
var card = document.createElement('div');
|
||
card.className = 'theme-card' + (key === current ? ' active' : '');
|
||
card.innerHTML =
|
||
'<div class="theme-card-preview">' +
|
||
'<div class="theme-card-preview-bar" style="background:' + (v['--bg-sidebar'] || v['--bg-primary']) + '"></div>' +
|
||
'<div class="theme-card-preview-body" style="background:' + (v['--bg-primary'] || '#000') + '">' +
|
||
'<div class="theme-card-preview-btn" style="background:' + (v['--accent'] || '#58a6ff') + '"></div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="theme-card-name">' + t.name + '</div>' +
|
||
'<div class="theme-card-desc">' + t.desc + '</div>';
|
||
card.addEventListener('click', function() {
|
||
applyTheme(key, mode);
|
||
renderCards();
|
||
});
|
||
grid.appendChild(card);
|
||
});
|
||
// Mode toggle
|
||
var toggle = document.createElement('div');
|
||
toggle.className = 'theme-mode-toggle';
|
||
var dBtn = elBtn(t('theme.dark'), 'dark', mode);
|
||
var lBtn = elBtn(t('theme.light'), 'light', mode);
|
||
toggle.appendChild(dBtn);
|
||
toggle.appendChild(lBtn);
|
||
grid.appendChild(toggle);
|
||
}
|
||
|
||
function elBtn(label, m, currentMode) {
|
||
var btn = document.createElement('button');
|
||
btn.className = 'theme-mode-btn' + (m === currentMode ? ' active' : '');
|
||
btn.textContent = label;
|
||
btn.addEventListener('click', function() {
|
||
applyTheme(getCurrentTheme(), m);
|
||
renderCards();
|
||
});
|
||
return btn;
|
||
}
|
||
|
||
renderCards();
|
||
});
|
||
}
|
||
|
||
// ── About Modal ──────────────────────────────────────────────────────
|
||
function initAboutModal() {
|
||
var overlay = document.getElementById('about-overlay');
|
||
var closeBtn = document.getElementById('about-close');
|
||
if (!overlay) return;
|
||
|
||
// Open button
|
||
var openBtn = document.getElementById('about-open-btn');
|
||
if (openBtn) {
|
||
openBtn.addEventListener('click', function() {
|
||
overlay.classList.add('active');
|
||
populateAbout();
|
||
});
|
||
}
|
||
|
||
// Close
|
||
closeBtn.addEventListener('click', function() { overlay.classList.remove('active'); });
|
||
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.classList.remove('active'); });
|
||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && overlay.classList.contains('active')) overlay.classList.remove('active'); });
|
||
|
||
function populateAbout() {
|
||
// Live stats from health endpoint
|
||
fetch('/api/health')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
var fEl = document.getElementById('about-stat-files');
|
||
var vEl = document.getElementById('about-stat-vaults');
|
||
if (fEl) fEl.textContent = d.total_files || '—';
|
||
if (vEl) vEl.textContent = d.vaults || '—';
|
||
})
|
||
.catch(function() {});
|
||
|
||
// Build date (today)
|
||
var now = new Date();
|
||
var months = [t('month.january'),t('month.february'),t('month.march'),t('month.april'),t('month.may'),t('month.june'),t('month.july'),t('month.august'),t('month.september'),t('month.october'),t('month.november'),t('month.december')];
|
||
var dateEl = document.getElementById('about-build-date');
|
||
if (dateEl) dateEl.textContent = months[now.getMonth()] + ' ' + now.getFullYear();
|
||
|
||
// Random joke
|
||
var jokes = [
|
||
t('about.joke_0'),
|
||
t('about.joke_1'),
|
||
t('about.joke_2'),
|
||
t('about.joke_3'),
|
||
t('about.joke_4'),
|
||
t('about.joke_5'),
|
||
t('about.joke_6'),
|
||
t('about.joke_7'),
|
||
t('about.joke_8'),
|
||
t('about.joke_9')
|
||
];
|
||
var jokeEl = document.getElementById('about-joke');
|
||
if (jokeEl) jokeEl.textContent = jokes[Math.floor(Math.random() * jokes.length)];
|
||
}
|
||
}
|
||
|
||
// ── Profile ──────────────────────────────────────────────────────────
|
||
function initProfile() {
|
||
var langSel = document.getElementById('profile-lang');
|
||
var nameInp = document.getElementById('profile-name');
|
||
var saveBtn = document.getElementById('profile-save');
|
||
var logoutBtn = document.getElementById('profile-logout');
|
||
var savedEl = document.getElementById('profile-saved');
|
||
if (!langSel || !saveBtn) return;
|
||
|
||
// Load saved profile
|
||
try {
|
||
var lang = getLocale();
|
||
var name = localStorage.getItem('obsigate-name') || '';
|
||
if (langSel) langSel.value = lang;
|
||
if (nameInp) nameInp.value = name;
|
||
} catch(e) {}
|
||
|
||
saveBtn.addEventListener('click', async function() {
|
||
try {
|
||
var newLang = langSel.value;
|
||
await setLocale(newLang); // Live-reload all translations
|
||
if (nameInp) localStorage.setItem('obsigate-name', nameInp.value.trim());
|
||
|
||
// Persist language to server (survives clear site data)
|
||
try {
|
||
await fetch('/api/auth/me', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ language: newLang }),
|
||
});
|
||
} catch (e) { /* non-bloquant */ }
|
||
|
||
if (savedEl) { savedEl.style.display = 'inline'; setTimeout(function() { savedEl.style.display = 'none'; }, 2000); }
|
||
} catch(e) {}
|
||
});
|
||
|
||
// Logout button — handled inline in index.html (onclick)
|
||
}
|