Add advanced search engine with inverted index, thread pool execution, configuration API, and comprehensive diagnostics
This commit is contained in:
+241
-7
@@ -33,12 +33,15 @@
|
||||
let suggestAbortController = null;
|
||||
let dropdownActiveIndex = -1;
|
||||
let dropdownItems = [];
|
||||
let currentSearchId = 0;
|
||||
|
||||
// Advanced search constants
|
||||
const SEARCH_HISTORY_KEY = "obsigate_search_history";
|
||||
const MAX_HISTORY_ENTRIES = 50;
|
||||
const SUGGEST_DEBOUNCE_MS = 150;
|
||||
const ADVANCED_SEARCH_LIMIT = 50;
|
||||
const MIN_SEARCH_LENGTH = 2;
|
||||
const SEARCH_TIMEOUT_MS = 30000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File extension → Lucide icon mapping
|
||||
@@ -1825,10 +1828,12 @@
|
||||
|
||||
if (!openBtn || !closeBtn || !modal) return;
|
||||
|
||||
openBtn.addEventListener("click", () => {
|
||||
openBtn.addEventListener("click", async () => {
|
||||
modal.classList.add("active");
|
||||
closeHeaderMenu();
|
||||
renderConfigFilters();
|
||||
loadConfigFields();
|
||||
loadDiagnostics();
|
||||
safeCreateIcons();
|
||||
});
|
||||
|
||||
@@ -1848,11 +1853,36 @@
|
||||
|
||||
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);
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
||||
closeConfigModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Load saved frontend config on startup
|
||||
applyFrontendConfig();
|
||||
}
|
||||
|
||||
function closeConfigModal() {
|
||||
@@ -1860,6 +1890,177 @@
|
||||
if (modal) modal.classList.remove("active");
|
||||
}
|
||||
|
||||
// --- Config field helpers ---
|
||||
const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
|
||||
|
||||
function _getFrontendConfig() {
|
||||
try { return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}"); }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
function applyFrontendConfig() {
|
||||
const cfg = _getFrontendConfig();
|
||||
if (cfg.debounce_ms) { /* applied dynamically in debounce setTimeout */ }
|
||||
if (cfg.results_per_page) { /* used as ADVANCED_SEARCH_LIMIT override */ }
|
||||
if (cfg.min_query_length) { /* used as MIN_SEARCH_LENGTH override */ }
|
||||
if (cfg.search_timeout_ms) { /* used as SEARCH_TIMEOUT_MS override */ }
|
||||
}
|
||||
|
||||
function _getEffective(key, fallback) {
|
||||
const cfg = _getFrontendConfig();
|
||||
return cfg[key] !== undefined ? cfg[key] : fallback;
|
||||
}
|
||||
|
||||
async function loadConfigFields() {
|
||||
// Frontend fields from localStorage
|
||||
const cfg = _getFrontendConfig();
|
||||
_setField("cfg-debounce", cfg.debounce_ms || 300);
|
||||
_setField("cfg-results-per-page", cfg.results_per_page || 50);
|
||||
_setField("cfg-min-query", cfg.min_query_length || 2);
|
||||
_setField("cfg-timeout", cfg.search_timeout_ms || 30000);
|
||||
|
||||
// Backend fields from API
|
||||
try {
|
||||
const data = await api("/api/config");
|
||||
_setField("cfg-workers", data.search_workers);
|
||||
_setField("cfg-max-content", data.max_content_size);
|
||||
_setField("cfg-title-boost", data.title_boost);
|
||||
_setField("cfg-tag-boost", data.tag_boost);
|
||||
_setField("cfg-prefix-exp", data.prefix_max_expansions);
|
||||
} 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 _getFieldNum(id, fallback) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return fallback;
|
||||
const v = parseFloat(el.value);
|
||||
return isNaN(v) ? fallback : v;
|
||||
}
|
||||
|
||||
function saveFrontendConfig() {
|
||||
const cfg = {
|
||||
debounce_ms: _getFieldNum("cfg-debounce", 300),
|
||||
results_per_page: _getFieldNum("cfg-results-per-page", 50),
|
||||
min_query_length: _getFieldNum("cfg-min-query", 2),
|
||||
search_timeout_ms: _getFieldNum("cfg-timeout", 30000),
|
||||
};
|
||||
localStorage.setItem(_FRONTEND_CONFIG_KEY, JSON.stringify(cfg));
|
||||
showToast("Paramètres client sauvegardés");
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
try {
|
||||
await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
showToast("Configuration backend sauvegardée");
|
||||
} catch (err) {
|
||||
console.error("Failed to save backend config:", err);
|
||||
showToast("Erreur de sauvegarde");
|
||||
}
|
||||
}
|
||||
|
||||
async function forceReindex() {
|
||||
const btn = document.getElementById("cfg-reindex");
|
||||
if (btn) { btn.disabled = true; btn.textContent = "Réindexation..."; }
|
||||
try {
|
||||
await api("/api/index/reload");
|
||||
showToast("Réindexation terminée");
|
||||
loadDiagnostics();
|
||||
await Promise.all([loadVaults(), loadTags()]);
|
||||
} catch (err) {
|
||||
console.error("Reindex error:", err);
|
||||
showToast("Erreur de réindexation");
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = "Forcer réindexation"; }
|
||||
}
|
||||
}
|
||||
|
||||
async function resetConfigDefaults() {
|
||||
// Reset frontend
|
||||
localStorage.removeItem(_FRONTEND_CONFIG_KEY);
|
||||
// Reset backend
|
||||
try {
|
||||
await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
search_workers: 2, debounce_ms: 300, results_per_page: 50,
|
||||
min_query_length: 2, search_timeout_ms: 30000, max_content_size: 100000,
|
||||
title_boost: 3.0, path_boost: 1.5, tag_boost: 2.0, prefix_max_expansions: 50,
|
||||
snippet_context_chars: 120, max_snippet_highlights: 5,
|
||||
}),
|
||||
});
|
||||
} catch (err) { console.error("Reset config error:", err); }
|
||||
loadConfigFields();
|
||||
showToast("Configuration réinitialisée");
|
||||
}
|
||||
|
||||
async function loadDiagnostics() {
|
||||
const container = document.getElementById("config-diagnostics");
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="config-diag-loading">Chargement...</div>';
|
||||
try {
|
||||
const data = await api("/api/diagnostics");
|
||||
renderDiagnostics(container, data);
|
||||
} catch (err) {
|
||||
container.innerHTML = '<div class="config-diag-loading">Erreur de chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDiagnostics(container, data) {
|
||||
container.innerHTML = "";
|
||||
const sections = [
|
||||
{ title: "Index", rows: [
|
||||
["Fichiers indexés", data.index.total_files],
|
||||
["Tags uniques", data.index.total_tags],
|
||||
["Vaults", Object.keys(data.index.vaults).join(", ")],
|
||||
]},
|
||||
{ title: "Index inversé", rows: [
|
||||
["Tokens uniques", data.inverted_index.unique_tokens.toLocaleString()],
|
||||
["Postings total", data.inverted_index.total_postings.toLocaleString()],
|
||||
["Documents", data.inverted_index.documents],
|
||||
["Mémoire estimée", data.inverted_index.memory_estimate_mb + " MB"],
|
||||
["Stale", data.inverted_index.is_stale ? "Oui" : "Non"],
|
||||
]},
|
||||
{ title: "Moteur de recherche", rows: [
|
||||
["Executor actif", data.search_executor.active ? "Oui" : "Non"],
|
||||
["Workers max", data.search_executor.max_workers],
|
||||
]},
|
||||
];
|
||||
sections.forEach((section) => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "config-diag-section";
|
||||
const title = document.createElement("div");
|
||||
title.className = "config-diag-section-title";
|
||||
title.textContent = section.title;
|
||||
div.appendChild(title);
|
||||
section.rows.forEach(([label, value]) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "config-diag-row";
|
||||
row.innerHTML = `<span class="diag-label">${label}</span><span class="diag-value">${value}</span>`;
|
||||
div.appendChild(row);
|
||||
});
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function renderConfigFilters() {
|
||||
const config = TagFilterService.getConfig();
|
||||
const filters = config.tagFilters || TagFilterService.defaultFilters;
|
||||
@@ -1987,13 +2188,13 @@
|
||||
const vault = document.getElementById("vault-filter").value;
|
||||
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
|
||||
advancedSearchOffset = 0;
|
||||
if (q.length > 0 || tagFilter) {
|
||||
if ((q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH)) || tagFilter) {
|
||||
performAdvancedSearch(q, vault, tagFilter);
|
||||
} else {
|
||||
} else if (q.length === 0) {
|
||||
SearchChips.clear();
|
||||
showWelcome();
|
||||
}
|
||||
}, 300);
|
||||
}, _getEffective("debounce_ms", 300));
|
||||
});
|
||||
|
||||
// --- Focus handler: show history dropdown ---
|
||||
@@ -2097,17 +2298,21 @@
|
||||
async function performSearch(query, vaultFilter, tagFilter) {
|
||||
if (searchAbortController) searchAbortController.abort();
|
||||
searchAbortController = new AbortController();
|
||||
const searchId = ++currentSearchId;
|
||||
showLoading();
|
||||
let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
|
||||
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
|
||||
try {
|
||||
const data = await api(url, { signal: searchAbortController.signal });
|
||||
if (searchId !== currentSearchId) return;
|
||||
renderSearchResults(data, query, tagFilter);
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") return;
|
||||
if (searchId !== currentSearchId) return;
|
||||
showWelcome();
|
||||
} finally {
|
||||
searchAbortController = null;
|
||||
hideProgressBar();
|
||||
if (searchId === currentSearchId) searchAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2115,6 +2320,7 @@
|
||||
async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) {
|
||||
if (searchAbortController) searchAbortController.abort();
|
||||
searchAbortController = new AbortController();
|
||||
const searchId = ++currentSearchId;
|
||||
showLoading();
|
||||
|
||||
const ofs = offset !== undefined ? offset : advancedSearchOffset;
|
||||
@@ -2125,19 +2331,30 @@
|
||||
const parsed = QueryParser.parse(query);
|
||||
SearchChips.update(parsed);
|
||||
|
||||
let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${ADVANCED_SEARCH_LIMIT}&offset=${ofs}&sort=${sortBy}`;
|
||||
const effectiveLimit = _getEffective("results_per_page", ADVANCED_SEARCH_LIMIT);
|
||||
let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`;
|
||||
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
|
||||
|
||||
// Search timeout — abort if server takes too long
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (searchAbortController) searchAbortController.abort();
|
||||
}, _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS));
|
||||
|
||||
try {
|
||||
const data = await api(url, { signal: searchAbortController.signal });
|
||||
clearTimeout(timeoutId);
|
||||
if (searchId !== currentSearchId) return;
|
||||
advancedSearchTotal = data.total;
|
||||
advancedSearchOffset = ofs;
|
||||
renderAdvancedSearchResults(data, query, tagFilter);
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
if (err.name === "AbortError") return;
|
||||
if (searchId !== currentSearchId) return;
|
||||
showWelcome();
|
||||
} finally {
|
||||
searchAbortController = null;
|
||||
hideProgressBar();
|
||||
if (searchId === currentSearchId) searchAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2209,6 +2426,11 @@
|
||||
} else {
|
||||
summaryText.textContent = `${data.total} résultat(s)`;
|
||||
}
|
||||
if (data.query_time_ms !== undefined && data.query_time_ms > 0) {
|
||||
const timeBadge = el("span", { class: "search-time-badge" });
|
||||
timeBadge.textContent = `(${data.query_time_ms} ms)`;
|
||||
summaryText.appendChild(timeBadge);
|
||||
}
|
||||
header.appendChild(summaryText);
|
||||
|
||||
// Sort controls
|
||||
@@ -2581,6 +2803,7 @@
|
||||
}
|
||||
|
||||
function showWelcome() {
|
||||
hideProgressBar();
|
||||
const area = document.getElementById("content-area");
|
||||
area.innerHTML = `
|
||||
<div class="welcome">
|
||||
@@ -2598,6 +2821,17 @@
|
||||
<div class="loading-spinner"></div>
|
||||
<div>Recherche en cours...</div>
|
||||
</div>`;
|
||||
showProgressBar();
|
||||
}
|
||||
|
||||
function showProgressBar() {
|
||||
const bar = document.getElementById("search-progress-bar");
|
||||
if (bar) bar.classList.add("active");
|
||||
}
|
||||
|
||||
function hideProgressBar() {
|
||||
const bar = document.getElementById("search-progress-bar");
|
||||
if (bar) bar.classList.remove("active");
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="search-progress-bar" id="search-progress-bar"><div class="search-progress-bar__fill"></div></div>
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Header -->
|
||||
@@ -299,6 +300,73 @@
|
||||
</div>
|
||||
<div class="editor-body" id="config-body">
|
||||
<div class="config-content">
|
||||
|
||||
<!-- Performance Settings — Frontend -->
|
||||
<section class="config-section">
|
||||
<h2>Paramètres de recherche</h2>
|
||||
<p class="config-description">Ces paramètres s'appliquent immédiatement côté client.</p>
|
||||
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-debounce">Délai debounce (ms)</label>
|
||||
<input type="number" id="cfg-debounce" class="config-input config-input--num" min="100" max="2000" step="50" value="300">
|
||||
<span class="config-hint">Délai avant exécution de la recherche (100–2000)</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-results-per-page">Résultats par page</label>
|
||||
<input type="number" id="cfg-results-per-page" class="config-input config-input--num" min="10" max="200" step="10" value="50">
|
||||
<span class="config-hint">Nombre de résultats affichés par page (10–200)</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-min-query">Longueur min. requête</label>
|
||||
<input type="number" id="cfg-min-query" class="config-input config-input--num" min="1" max="5" step="1" value="2">
|
||||
<span class="config-hint">Nombre minimum de caractères avant recherche (1–5)</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-timeout">Timeout recherche (ms)</label>
|
||||
<input type="number" id="cfg-timeout" class="config-input config-input--num" min="5000" max="120000" step="5000" value="30000">
|
||||
<span class="config-hint">Annuler la recherche après ce délai (5000–120000)</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Performance Settings — Backend -->
|
||||
<section class="config-section">
|
||||
<h2>Paramètres backend <span class="config-badge-restart">Redémarrage requis</span></h2>
|
||||
<p class="config-description">Ces paramètres sont sauvegardés sur le serveur. Certains nécessitent un redémarrage ou une réindexation.</p>
|
||||
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-workers">Workers de recherche</label>
|
||||
<input type="number" id="cfg-workers" class="config-input config-input--num" min="1" max="8" step="1" value="2">
|
||||
<span class="config-hint">Threads dédiés à la recherche (1–8)</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-max-content">Taille max contenu (octets)</label>
|
||||
<input type="number" id="cfg-max-content" class="config-input config-input--num" min="10000" max="1000000" step="10000" value="100000">
|
||||
<span class="config-hint">Contenu indexé par fichier (10K–1M). Réindexation requise.</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-title-boost">Boost titre</label>
|
||||
<input type="number" id="cfg-title-boost" class="config-input config-input--num" min="0" max="10" step="0.5" value="3.0">
|
||||
<span class="config-hint">Multiplicateur de pertinence pour les correspondances dans le titre</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-tag-boost">Boost tags</label>
|
||||
<input type="number" id="cfg-tag-boost" class="config-input config-input--num" min="0" max="10" step="0.5" value="2.0">
|
||||
<span class="config-hint">Multiplicateur de pertinence pour les correspondances dans les tags</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label class="config-label" for="cfg-prefix-exp">Expansions préfixe max</label>
|
||||
<input type="number" id="cfg-prefix-exp" class="config-input config-input--num" min="10" max="200" step="10" value="50">
|
||||
<span class="config-hint">Nombre max de tokens élargis par préfixe (10–200)</span>
|
||||
</div>
|
||||
|
||||
<div class="config-actions-row">
|
||||
<button class="config-btn-save" id="cfg-save-backend">Sauvegarder</button>
|
||||
<button class="config-btn-secondary" id="cfg-reindex">Forcer réindexation</button>
|
||||
<button class="config-btn-secondary" id="cfg-reset-defaults">Réinitialiser</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tag Filtering (existing) -->
|
||||
<section class="config-section">
|
||||
<h2>Filtrage de tags</h2>
|
||||
<p class="config-description">Définissez les patterns de tags à masquer dans la sidebar. Vous pouvez utiliser des wildcards pour cibler les tags de template.</p>
|
||||
@@ -314,6 +382,17 @@
|
||||
<small>Regex : <code id="config-regex-code"></code></small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Diagnostics -->
|
||||
<section class="config-section">
|
||||
<h2>Diagnostics</h2>
|
||||
<p class="config-description">Statistiques de l'index et du moteur de recherche.</p>
|
||||
<div id="config-diagnostics" class="config-diagnostics">
|
||||
<div class="config-diag-loading">Chargement...</div>
|
||||
</div>
|
||||
<button class="config-btn-secondary" id="cfg-refresh-diag" style="margin-top:8px">Rafraîchir</button>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1347,6 +1347,39 @@ select {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* --- Search progress bar --- */
|
||||
.search-progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
z-index: 9999;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.search-progress-bar.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.search-progress-bar .search-progress-bar__fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
width: 0%;
|
||||
animation: progress-indeterminate 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes progress-indeterminate {
|
||||
0% { width: 0%; margin-left: 0%; }
|
||||
50% { width: 40%; margin-left: 30%; }
|
||||
100% { width: 0%; margin-left: 100%; }
|
||||
}
|
||||
.search-time-badge {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* --- Editor Modal --- */
|
||||
.editor-modal {
|
||||
display: none;
|
||||
@@ -2111,6 +2144,118 @@ body.resizing-v {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* --- Config rows & controls --- */
|
||||
.config-row {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 120px 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.config-label {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
.config-input--num {
|
||||
flex: none;
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
.config-hint {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.config-badge-restart {
|
||||
display: inline-block;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--danger-bg, #3d1a18);
|
||||
color: var(--danger, #ff7b72);
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.config-actions-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.config-btn-save {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
.config-btn-save:hover { opacity: 0.9; }
|
||||
.config-btn-secondary {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: background 150ms;
|
||||
}
|
||||
.config-btn-secondary:hover { background: var(--bg-hover); }
|
||||
|
||||
/* --- Config diagnostics panel --- */
|
||||
.config-diagnostics {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.7;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.config-diag-loading {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.config-diag-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.config-diag-row .diag-label { color: var(--text-secondary); }
|
||||
.config-diag-row .diag-value { color: var(--text-primary); font-weight: 500; }
|
||||
.config-diag-section {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.config-diag-section:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.config-diag-section-title {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
margin-bottom: 4px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
.config-input--num { width: 100%; }
|
||||
}
|
||||
|
||||
/* --- Toast notifications --- */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user