fix: rebuild ui.js, search.js, viewer.js from app.js with shell script (no truncation)
CI / lint (push) Successful in 12s
CI / security (push) Successful in 8s
CI / test (push) Successful in 16s
CI / build (push) Successful in 3s

This commit is contained in:
2026-05-28 16:54:15 -04:00
parent 97d14d867a
commit 7c927ca54e
3 changed files with 269 additions and 86 deletions
+60 -52
View File
@@ -1,12 +1,13 @@
/* ObsiGate — Auto-extracted module */
import { state } from './state.js';;
/* ObsiGate — Search module */
import { state } from './state.js';
import { safeCreateIcons } from './utils.js';
// ---------------------------------------------------------------------------
// Search History Service (localStorage, LIFO, max 50, dedup)
// ---------------------------------------------------------------------------
const SearchHistory = {
_load() {
try {
const raw = localStorage.getItem(state.SEARCH_HISTORY_KEY);
const raw = localStorage.getItem(SEARCH_HISTORY_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
@@ -14,7 +15,7 @@ const SearchHistory = {
},
_save(entries) {
try {
localStorage.setItem(state.SEARCH_HISTORY_KEY, JSON.stringify(entries));
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries));
} catch {}
},
getAll() {
@@ -25,7 +26,7 @@ const SearchHistory = {
const q = query.trim();
let entries = this._load().filter((e) => e !== q);
entries.unshift(q);
if (entries.length > state.MAX_HISTORY_ENTRIES) entries = entries.slice(0, state.MAX_HISTORY_ENTRIES);
if (entries.length > MAX_HISTORY_ENTRIES) entries = entries.slice(0, MAX_HISTORY_ENTRIES);
this._save(entries);
},
remove(query) {
@@ -44,6 +45,7 @@ const SearchHistory = {
},
};
// ---------------------------------------------------------------------------
// Query Parser — extracts operators (tag:, #, vault:, title:, path:, ext:)
// ---------------------------------------------------------------------------
@@ -119,6 +121,7 @@ const QueryParser = {
},
};
// ---------------------------------------------------------------------------
// Autocomplete Dropdown Controller
// ---------------------------------------------------------------------------
@@ -179,7 +182,7 @@ const AutocompleteDropdown = {
async populate(inputValue, cursorPos) {
if (this._suppressNext) { this._suppressNext = false; return; }
// Cancel previous suggestion request
if (state.suggestAbortController) {
if (suggestAbortController) {
state.suggestAbortController.abort();
state.suggestAbortController = null;
}
@@ -370,29 +373,30 @@ const AutocompleteDropdown = {
navigateDown() {
if (!this.isVisible() || state.dropdownItems.length === 0) return;
if (dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active");
if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active");
state.dropdownActiveIndex = (dropdownActiveIndex + 1) % state.dropdownItems.length;
state.dropdownItems[state.dropdownActiveIndex].classList.add("active");
state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" });
dropdownItems[dropdownActiveIndex].classList.add("active");
dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" });
},
navigateUp() {
if (!this.isVisible() || state.dropdownItems.length === 0) return;
if (dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active");
if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active");
state.dropdownActiveIndex = dropdownActiveIndex <= 0 ? state.dropdownItems.length - 1 : dropdownActiveIndex - 1;
state.dropdownItems[state.dropdownActiveIndex].classList.add("active");
state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" });
dropdownItems[dropdownActiveIndex].classList.add("active");
dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" });
},
selectActive() {
if (dropdownActiveIndex >= 0 && dropdownActiveIndex < state.dropdownItems.length) {
state.dropdownItems[state.dropdownActiveIndex].click();
dropdownItems[dropdownActiveIndex].click();
return true;
}
return false;
},
};
// ---------------------------------------------------------------------------
// Search Chips Controller — renders active filter chips from parsed query
// ---------------------------------------------------------------------------
@@ -454,6 +458,7 @@ const SearchChips = {
},
};
// ---------------------------------------------------------------------------
// Helper: trigger advanced search from input value
// ---------------------------------------------------------------------------
@@ -471,6 +476,7 @@ function _triggerAdvancedSearch(rawQuery) {
}
}
// ---------------------------------------------------------------------------
// Search (enhanced with autocomplete, keyboard nav, global shortcuts)
// ---------------------------------------------------------------------------
@@ -490,10 +496,10 @@ function initSearch() {
const countEl = document.getElementById("search-match-count");
function _updateToggleUI() {
caseBtn.classList.toggle("active", state.searchCaseSensitive);
wordBtn.classList.toggle("active", state.searchWholeWord);
regexBtn.classList.toggle("active", state.searchRegex);
filterBtn.classList.toggle("active", state.searchFilterVisible);
caseBtn.classList.toggle("active", searchCaseSensitive);
wordBtn.classList.toggle("active", searchWholeWord);
regexBtn.classList.toggle("active", searchRegex);
filterBtn.classList.toggle("active", searchFilterVisible);
}
// Toggle buttons
@@ -536,7 +542,7 @@ function initSearch() {
function _research() {
const q = input.value.trim();
if (q.length >= _getEffective("min_query_length", 2)) {
clearTimeout(state.searchTimeout);
clearTimeout(searchTimeout);
state.searchTimeout = setTimeout(() => {
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
@@ -572,14 +578,14 @@ function initSearch() {
AutocompleteDropdown.populate(input.value, input.selectionStart);
// Debounced search execution
clearTimeout(state.searchTimeout);
clearTimeout(searchTimeout);
state.searchTimeout = setTimeout(
() => {
const q = input.value.trim();
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
state.advancedSearchOffset = 0;
if (q.length >= _getEffective("min_query_length", state.MIN_SEARCH_LENGTH) || tagFilter) {
if (q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH) || tagFilter) {
performAdvancedSearch(q, vault, tagFilter);
} else if (q.length === 0) {
SearchChips.clear();
@@ -630,7 +636,7 @@ function initSearch() {
const q = input.value.trim();
if (q) {
SearchHistory.add(q);
clearTimeout(state.searchTimeout);
clearTimeout(searchTimeout);
state.advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
@@ -654,7 +660,7 @@ function initSearch() {
const q = input.value.trim();
if (q) {
SearchHistory.add(q);
clearTimeout(state.searchTimeout);
clearTimeout(searchTimeout);
state.advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
@@ -706,7 +712,7 @@ function _isInputFocused() {
// --- Backward-compatible search (existing /api/search endpoint) ---
async function performSearch(query, vaultFilter, tagFilter) {
if (state.searchAbortController) state.searchAbortController.abort();
if (searchAbortController) state.searchAbortController.abort();
state.searchAbortController = new AbortController();
const searchId = ++state.currentSearchId;
showLoading();
@@ -714,21 +720,21 @@ async function performSearch(query, vaultFilter, tagFilter) {
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
try {
const data = await api(url, { signal: state.searchAbortController.signal });
if (searchId !== state.currentSearchId) return;
if (searchId !== currentSearchId) return;
renderSearchResults(data, query, tagFilter);
} catch (err) {
if (err.name === "AbortError") return;
if (searchId !== state.currentSearchId) return;
if (searchId !== currentSearchId) return;
showWelcome();
} finally {
hideProgressBar();
if (searchId === state.currentSearchId) state.searchAbortController = null;
if (searchId === currentSearchId) state.searchAbortController = null;
}
}
// --- Advanced search with TF-IDF, facets, pagination ---
async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) {
if (state.searchAbortController) state.searchAbortController.abort();
if (searchAbortController) state.searchAbortController.abort();
state.searchAbortController = new AbortController();
const searchId = ++state.currentSearchId;
showLoading();
@@ -741,12 +747,12 @@ async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort
const parsed = QueryParser.parse(query);
SearchChips.update(parsed);
const effectiveLimit = _getEffective("results_per_page", state.ADVANCED_SEARCH_LIMIT);
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)}`;
if (state.searchCaseSensitive) url += "&case_sensitive=true";
if (state.searchWholeWord) url += "&whole_word=true";
if (state.searchRegex) url += "&regex=true";
if (searchCaseSensitive) url += "&case_sensitive=true";
if (searchWholeWord) url += "&whole_word=true";
if (searchRegex) url += "&regex=true";
const includeEl = document.getElementById("search-include-input");
const excludeEl = document.getElementById("search-exclude-input");
if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`;
@@ -755,26 +761,26 @@ async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort
// Search timeout — abort if server takes too long
const timeoutId = setTimeout(
() => {
if (state.searchAbortController) state.searchAbortController.abort();
if (searchAbortController) state.searchAbortController.abort();
},
_getEffective("search_timeout_ms", state.SEARCH_TIMEOUT_MS),
_getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS),
);
try {
const data = await api(url, { signal: state.searchAbortController.signal });
clearTimeout(timeoutId);
if (searchId !== state.currentSearchId) return;
if (searchId !== currentSearchId) return;
state.advancedSearchTotal = data.total;
state.advancedSearchOffset = ofs;
renderAdvancedSearchResults(data, query, tagFilter);
} catch (err) {
clearTimeout(timeoutId);
if (err.name === "AbortError") return;
if (searchId !== state.currentSearchId) return;
if (searchId !== currentSearchId) return;
showWelcome();
} finally {
hideProgressBar();
if (searchId === state.currentSearchId) state.searchAbortController = null;
if (searchId === currentSearchId) state.searchAbortController = null;
}
}
@@ -797,7 +803,7 @@ function renderSearchResults(data, query, tagFilter) {
const titleDiv = el("div", { class: "search-result-title" });
if (query && query.trim()) {
highlightSearchText(titleDiv, r.title, query, state.searchCaseSensitive);
highlightSearchText(titleDiv, r.title, query, searchCaseSensitive);
} else {
titleDiv.textContent = r.title;
}
@@ -805,7 +811,7 @@ function renderSearchResults(data, query, tagFilter) {
if (r.snippet && r.snippet.includes("<mark>")) {
snippetDiv.innerHTML = r.snippet;
} else if (query && query.trim() && r.snippet) {
highlightSearchText(snippetDiv, r.snippet, query, state.searchCaseSensitive);
highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive);
} else {
snippetDiv.textContent = r.snippet || "";
}
@@ -869,9 +875,9 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
// Active filter badges
const filtersRow = el("div", { class: "search-filters-row" });
if (state.searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")]));
if (state.searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")]));
if (state.searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")]));
if (searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")]));
if (searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")]));
if (searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")]));
const inclEl = document.getElementById("search-include-input");
const exclEl = document.getElementById("search-exclude-input");
if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())]));
@@ -913,9 +919,9 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
body: JSON.stringify({
query: query,
vault: document.getElementById("vault-filter")?.value || "all",
case_sensitive: state.searchCaseSensitive,
whole_word: state.searchWholeWord,
regex: state.searchRegex,
case_sensitive: searchCaseSensitive,
whole_word: searchWholeWord,
regex: searchRegex,
include_paths: inclEl?.value || "",
exclude_paths: exclEl?.value || "",
}),
@@ -1012,7 +1018,7 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
const titleDiv = el("div", { class: "search-result-title" });
if (freeText) {
highlightSearchText(titleDiv, r.title, freeText, state.searchCaseSensitive);
highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive);
} else {
titleDiv.textContent = r.title;
}
@@ -1022,7 +1028,7 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
if (r.snippet && r.snippet.includes("<mark>")) {
snippetDiv.innerHTML = r.snippet;
} else if (freeText && r.snippet) {
highlightSearchText(snippetDiv, r.snippet, freeText, state.searchCaseSensitive);
highlightSearchText(snippetDiv, r.snippet, freeText, searchCaseSensitive);
} else {
snippetDiv.textContent = r.snippet || "";
}
@@ -1060,21 +1066,21 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
area.appendChild(container);
// Pagination
if (data.total > state.ADVANCED_SEARCH_LIMIT) {
if (data.total > ADVANCED_SEARCH_LIMIT) {
const paginationDiv = el("div", { class: "search-pagination" });
const prevBtn = el("button", { class: "search-pagination__btn", type: "button" });
prevBtn.textContent = "← Précédent";
prevBtn.disabled = state.advancedSearchOffset === 0;
prevBtn.addEventListener("click", () => {
state.advancedSearchOffset = Math.max(0, advancedSearchOffset - state.ADVANCED_SEARCH_LIMIT);
state.advancedSearchOffset = Math.max(0, advancedSearchOffset - ADVANCED_SEARCH_LIMIT);
const vault = document.getElementById("vault-filter").value;
performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset);
performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset);
document.getElementById("content-area").scrollTop = 0;
});
const info = el("span", { class: "search-pagination__info" });
const from = advancedSearchOffset + 1;
const to = Math.min(advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT, data.total);
const to = Math.min(advancedSearchOffset + ADVANCED_SEARCH_LIMIT, data.total);
info.textContent = `${from}–${to} sur ${data.total}`;
const nextBtn = el("button", { class: "search-pagination__btn", type: "button" });
@@ -1083,7 +1089,7 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
nextBtn.addEventListener("click", () => {
advancedSearchOffset += state.ADVANCED_SEARCH_LIMIT;
const vault = document.getElementById("vault-filter").value;
performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset);
performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset);
document.getElementById("content-area").scrollTop = 0;
});
@@ -1096,4 +1102,6 @@ function renderAdvancedSearchResults(data, query, tagFilter) {
safeCreateIcons();
// Initialize result navigation (select first result)
setTimeout(() => { if (window.navigateSearchResults) window.navigateSearchResults(0); }, 50);
}
}