Files
ObsiGate/frontend/js/config.js
T
bruno eef8671912
CI / lint (push) Failing after 33s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 25s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
fix(config): corriger erreur de syntaxe config.js dans la section clés API IA
2026-09-07 18:24:41 -04:00

1730 lines
62 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, AuthManager, initMfaSettings } 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();
// Init MFA settings (security tab)
initMfaSettings();
}
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 ──
const AI_KEY_MAP = {
"cfg-deepseek-key": "DEEPSEEK_API_KEY",
"cfg-openrouter-key": "OPENROUTER_API_KEY",
"cfg-gemini-key": "GEMINI_API_KEY",
"cfg-nvidia-key": "NVIDIA_API_KEY",
"cfg-qwencloud-key": "QWENCLOUD_API_KEY",
"cfg-xiaomi-key": "XIAOMI_API_KEY",
"cfg-mistral-key": "MISTRAL_API_KEY",
};
const AI_PROVIDER_NAMES = ["deepseek","openrouter","gemini","nvidia","qwencloud","xiaomi","mistral"];
function _ensureAIKeyUI() {
for (const [inputId] of Object.entries(AI_KEY_MAP)) {
const input = document.getElementById(inputId);
if (!input) continue;
const row = input.closest(".config-row");
if (!row || row.dataset.enhanced) continue;
row.dataset.enhanced = "1";
row.style.cssText += "display:flex;align-items:center;gap:8px;flex-wrap:wrap;";
const badge = document.createElement("span");
badge.id = inputId.replace("-key", "-badge");
badge.style.cssText = "font-size:11px;padding:2px 8px;border-radius:10px;white-space:nowrap;";
row.appendChild(badge);
const delBtn = document.createElement("button");
delBtn.type = "button";
delBtn.id = inputId.replace("-key", "-delete");
delBtn.className = "config-btn-secondary";
delBtn.style.cssText = "font-size:11px;padding:4px 10px;color:var(--danger,#e74c3c);border-color:var(--danger,#e74c3c);cursor:pointer;display:none;";
delBtn.textContent = "\u00d7 Supprimer";
delBtn.addEventListener("click", () => deleteAIKey(inputId));
row.appendChild(delBtn);
}
}
function _setAIKeyBadge(inputId, hasKey) {
const badge = document.getElementById(inputId.replace("-key", "-badge"));
const delBtn = document.getElementById(inputId.replace("-key", "-delete"));
if (badge) {
if (hasKey) {
badge.textContent = "\u2713 Configur\u00e9";
badge.style.background = "var(--success-bg, #27ae6022)";
badge.style.color = "var(--success, #27ae60)";
badge.style.border = "1px solid var(--success, #27ae60)";
} else {
badge.textContent = "Non configur\u00e9";
badge.style.background = "var(--muted-bg, #ffffff10)";
badge.style.color = "var(--text-muted, #888)";
badge.style.border = "1px solid var(--border, #444)";
}
}
if (delBtn) delBtn.style.display = hasKey ? "inline-block" : "none";
}
async function loadAIKeys() {
_ensureAIKeyUI();
try {
const data = await api("/api/config/ai-keys");
for (const [inputId, envName] of Object.entries(AI_KEY_MAP)) {
const input = document.getElementById(inputId);
const val = data[envName] || "";
if (input) {
input.placeholder = val || (inputId.includes("gemini") ? "AIza..." : inputId.includes("openrouter") ? "sk-or-..." : "sk-...");
}
_setAIKeyBadge(inputId, !!val);
}
} catch(e) {}
}
async function saveAIKeys() {
const keys = {};
for (const [id, name] of Object.entries(AI_KEY_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");
Object.keys(AI_KEY_MAP).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
loadAIKeys();
} catch(e) { showToast("Erreur: " + e.message, "error"); }
}
async function deleteAIKey(inputId) {
const envName = AI_KEY_MAP[inputId];
if (!envName) return;
const provider = envName.replace("_API_KEY", "").replace(/_/g, " ");
if (!confirm("Supprimer la cl\u00e9 API " + provider + " ?\nLe fournisseur ne sera plus disponible.")) return;
try {
await api("/api/config/ai-keys/" + envName, { method: "DELETE" });
showToast("Cl\u00e9 " + provider + " supprim\u00e9e", "success");
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 {
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;
for (const p of AI_PROVIDER_NAMES) {
const inputId = "cfg-" + p + "-key";
if (results[p] === "ok") {
_setAIKeyBadge(inputId, true);
} else if (results[p] && results[p].includes("non")) {
_setAIKeyBadge(inputId, false);
}
}
for (const p of AI_PROVIDER_NAMES) {
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 AVAILABLE_MODES = mod.AVAILABLE_MODES;
var applyTheme = mod.applyTheme;
var getCurrentTheme = mod.getCurrentTheme;
var getCurrentMode = mod.getCurrentMode;
var exportAllThemes = mod.exportAllThemes;
var importTheme = mod.importTheme;
var deleteCustomTheme = mod.deleteCustomTheme;
var loadCustomThemes = mod.loadCustomThemes;
var grid = document.getElementById('theme-grid');
if (!grid) return;
// Mode label map (uses i18n t() when available, fallback to English)
var modeLabels = {
'dark': function() { return t('theme.dark'); },
'light': function() { return t('theme.light'); },
'high-contrast': function() { return t('theme.high_contrast'); },
'sepia': function() { return t('theme.sepia'); }
};
function renderCards() {
var themeKeys = Object.keys(THEMES);
var current = getCurrentTheme();
var mode = getCurrentMode();
grid.innerHTML = '';
themeKeys.forEach(function(key) {
var th = THEMES[key];
// Use dark mode for preview if mode not resolved yet
var v = th.modes[mode] || th.modes.dark || th.modes.light;
if (!v) return;
var card = document.createElement('div');
card.className = 'theme-card' + (key === current ? ' active' : '');
var isCustom = th.desc === 'Custom theme';
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">' + th.name + (isCustom ? ' <span class="theme-custom-badge">✦</span>' : '') + '</div>' +
'<div class="theme-card-desc">' + th.desc + '</div>';
card.addEventListener('click', function() {
applyTheme(key, mode);
renderCards();
});
// Right-click to delete custom themes
if (isCustom) {
card.addEventListener('contextmenu', function(e) {
e.preventDefault();
if (confirm(t('theme.delete_confirm'))) {
deleteCustomTheme(key);
renderCards();
}
});
}
grid.appendChild(card);
});
// Mode toggle — 4 modes
var toggle = document.createElement('div');
toggle.className = 'theme-mode-toggle';
AVAILABLE_MODES.forEach(function(m) {
var btn = document.createElement('button');
btn.className = 'theme-mode-btn' + (m === mode ? ' active' : '');
btn.textContent = (modeLabels[m] || function() { return m; })();
btn.addEventListener('click', function() {
applyTheme(getCurrentTheme(), m);
renderCards();
});
toggle.appendChild(btn);
});
grid.appendChild(toggle);
// Import/Export buttons
var actions = document.createElement('div');
actions.className = 'theme-actions';
var exportBtn = document.createElement('button');
exportBtn.className = 'theme-action-btn';
exportBtn.textContent = t('theme.export_all');
exportBtn.addEventListener('click', function() {
var json = exportAllThemes();
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = 'obsigate-themes.json'; a.click();
URL.revokeObjectURL(url);
});
var importBtn = document.createElement('button');
importBtn.className = 'theme-action-btn';
importBtn.textContent = t('theme.import');
importBtn.addEventListener('click', function() {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(ev) {
var content = ev.target.result;
// Support single theme or all-themes format
try {
var data = JSON.parse(content);
if (data.themes) {
// Multi-theme export: import each
var count = 0;
Object.keys(data.themes).forEach(function(k) {
var res = importTheme(JSON.stringify(data.themes[k]));
if (res.ok) count++;
});
alert(t('theme.import_success', { count: count }));
} else {
var res = importTheme(content);
if (res.ok) {
alert(t('theme.import_success', { count: 1 }));
} else {
alert(t('theme.import_error') + ': ' + res.error);
}
}
} catch(ex) {
alert(t('theme.import_error') + ': ' + ex.message);
}
renderCards();
};
reader.readAsText(file);
});
input.click();
});
actions.appendChild(exportBtn);
actions.appendChild(importBtn);
grid.appendChild(actions);
}
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)
}