feat: implement initial ObsiGate application with backend API, indexing, search, and basic frontend.
This commit is contained in:
+162
-2
@@ -2261,6 +2261,150 @@
|
||||
area.scrollTop = 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recent files
|
||||
// ---------------------------------------------------------------------------
|
||||
let _recentRefreshTimer = null;
|
||||
let _recentTimestampTimer = null;
|
||||
let _recentFilesCache = [];
|
||||
|
||||
async function loadRecentFiles(vaultFilter) {
|
||||
const listEl = document.getElementById("recent-list");
|
||||
const emptyEl = document.getElementById("recent-empty");
|
||||
if (!listEl) return;
|
||||
|
||||
let url = "/api/recent";
|
||||
if (vaultFilter) url += `?vault=${encodeURIComponent(vaultFilter)}`;
|
||||
try {
|
||||
const data = await api(url);
|
||||
_recentFilesCache = data.files || [];
|
||||
renderRecentList(_recentFilesCache);
|
||||
} catch (err) {
|
||||
console.error("Failed to load recent files:", err);
|
||||
listEl.innerHTML = '';
|
||||
if (emptyEl) { emptyEl.classList.remove("hidden"); }
|
||||
}
|
||||
}
|
||||
|
||||
function renderRecentList(files) {
|
||||
const listEl = document.getElementById("recent-list");
|
||||
const emptyEl = document.getElementById("recent-empty");
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = "";
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
if (emptyEl) { emptyEl.classList.remove("hidden"); safeCreateIcons(); }
|
||||
return;
|
||||
}
|
||||
if (emptyEl) emptyEl.classList.add("hidden");
|
||||
|
||||
files.forEach((f) => {
|
||||
const item = el("div", { class: "recent-item", "data-vault": f.vault, "data-path": f.path });
|
||||
|
||||
// Header row: time + vault badge
|
||||
const header = el("div", { class: "recent-item-header" });
|
||||
const timeSpan = el("span", { class: "recent-time" }, [
|
||||
icon("clock", 11),
|
||||
document.createTextNode(f.mtime_human),
|
||||
]);
|
||||
const badge = el("span", { class: "recent-vault-badge" }, [document.createTextNode(f.vault)]);
|
||||
header.appendChild(timeSpan);
|
||||
header.appendChild(badge);
|
||||
item.appendChild(header);
|
||||
|
||||
// Title
|
||||
const titleEl = el("div", { class: "recent-item-title" }, [
|
||||
document.createTextNode(f.title || f.path.split("/").pop()),
|
||||
]);
|
||||
item.appendChild(titleEl);
|
||||
|
||||
// Path breadcrumb
|
||||
const pathParts = f.path.split("/");
|
||||
if (pathParts.length > 1) {
|
||||
const pathEl = el("div", { class: "recent-item-path" }, [
|
||||
document.createTextNode(pathParts.slice(0, -1).join(" / ")),
|
||||
]);
|
||||
item.appendChild(pathEl);
|
||||
}
|
||||
|
||||
// Preview
|
||||
if (f.preview) {
|
||||
const previewEl = el("div", { class: "recent-item-preview" }, [
|
||||
document.createTextNode(f.preview),
|
||||
]);
|
||||
item.appendChild(previewEl);
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (f.tags && f.tags.length > 0) {
|
||||
const tagsEl = el("div", { class: "recent-item-tags" });
|
||||
f.tags.forEach((t) => {
|
||||
tagsEl.appendChild(el("span", { class: "tag-pill" }, [document.createTextNode(t)]));
|
||||
});
|
||||
item.appendChild(tagsEl);
|
||||
}
|
||||
|
||||
// Click handler
|
||||
item.addEventListener("click", () => {
|
||||
openFile(f.vault, f.path);
|
||||
closeMobileSidebar();
|
||||
});
|
||||
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
safeCreateIcons();
|
||||
}
|
||||
|
||||
function _humanizeDelta(mtime) {
|
||||
const delta = (Date.now() / 1000) - mtime;
|
||||
if (delta < 60) return "à l'instant";
|
||||
if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
|
||||
if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
|
||||
if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
|
||||
return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
|
||||
}
|
||||
|
||||
function _refreshRecentTimestamps() {
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
allVaults.forEach((v) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = v.name;
|
||||
opt.textContent = v.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function initRecentTab() {
|
||||
const select = document.getElementById("recent-vault-filter");
|
||||
if (select) {
|
||||
select.addEventListener("change", () => {
|
||||
loadRecentFiles(select.value || null);
|
||||
});
|
||||
}
|
||||
// Periodic timestamp refresh (every 60s)
|
||||
_recentTimestampTimer = setInterval(_refreshRecentTimestamps, 60000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar tabs
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2283,14 +2427,21 @@
|
||||
});
|
||||
const filterInput = document.getElementById("sidebar-filter-input");
|
||||
if (filterInput) {
|
||||
filterInput.placeholder = tab === "vaults" ? "Filtrer fichiers..." : "Filtrer tags...";
|
||||
const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "" };
|
||||
filterInput.placeholder = placeholders[tab] || "";
|
||||
}
|
||||
const query = filterInput
|
||||
? (sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase())
|
||||
: "";
|
||||
if (query) {
|
||||
if (tab === "vaults") performTreeSearch(query);
|
||||
else filterTagCloud(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2477,6 +2628,7 @@
|
||||
_setField("cfg-title-boost", data.title_boost);
|
||||
_setField("cfg-tag-boost", data.tag_boost);
|
||||
_setField("cfg-prefix-exp", data.prefix_max_expansions);
|
||||
_setField("cfg-recent-limit", data.recent_files_limit || 20);
|
||||
// Watcher config
|
||||
_setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false);
|
||||
_setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true);
|
||||
@@ -2527,6 +2679,7 @@
|
||||
title_boost: _getFieldNum("cfg-title-boost", 3.0),
|
||||
tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
|
||||
prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
|
||||
recent_files_limit: _getFieldNum("cfg-recent-limit", 20),
|
||||
watcher_enabled: _getCheckbox("cfg-watcher-enabled"),
|
||||
watcher_use_polling: _getCheckbox("cfg-watcher-polling"),
|
||||
watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0),
|
||||
@@ -3871,6 +4024,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh recent tab if it is active
|
||||
if (activeSidebarTab === "recent") {
|
||||
const vaultFilter = document.getElementById("recent-vault-filter");
|
||||
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
connectionState = "connected";
|
||||
_updateBadge();
|
||||
@@ -4039,6 +4198,7 @@
|
||||
initEditor();
|
||||
initSyncStatus();
|
||||
initLoginForm();
|
||||
initRecentTab();
|
||||
|
||||
// Check auth status first
|
||||
const authOk = await AuthManager.initAuth();
|
||||
|
||||
Reference in New Issue
Block a user