feat: implement initial ObsiGate application with backend API, indexing, search, and basic frontend.

This commit is contained in:
2026-03-24 12:56:00 -04:00
parent d6ae501f51
commit c5e395005f
4 changed files with 318 additions and 2 deletions
+162 -2
View File
@@ -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();
+29
View File
@@ -282,6 +282,11 @@
<i data-lucide="tag" style="width:13px;height:13px"></i>
<span>Tags</span>
</button>
<button class="sidebar-tab" id="sidebar-tab-recent" role="tab"
aria-selected="false" aria-controls="sidebar-panel-recent" data-tab="recent">
<i data-lucide="clock" style="width:13px;height:13px"></i>
<span>Récent</span>
</button>
</div>
<!-- Vaults panel -->
@@ -304,6 +309,20 @@
<div class="tag-cloud" id="tag-cloud"></div>
</div>
<!-- Recent panel -->
<div class="sidebar-tab-panel" id="sidebar-panel-recent" role="tabpanel" aria-labelledby="sidebar-tab-recent">
<div class="recent-filter-bar">
<select id="recent-vault-filter">
<option value="">Tous les vaults</option>
</select>
</div>
<div id="recent-list" class="recent-list"></div>
<div id="recent-empty" class="recent-empty hidden">
<i data-lucide="inbox" style="width:32px;height:32px"></i>
<span>Aucun fichier récent</span>
</div>
</div>
</aside>
<!-- Sidebar resize handle -->
@@ -383,6 +402,16 @@
</div>
</section>
<!-- Historique récent -->
<section class="config-section">
<h2>📋 Historique récent <span class="config-badge-restart">Redémarrage non requis</span></h2>
<div class="config-row">
<label class="config-label" for="cfg-recent-limit">Nombre de fichiers dans l'historique</label>
<input type="number" id="cfg-recent-limit" class="config-input config-input--num" min="5" max="100" step="5" value="20">
<span class="config-hint">Entre 5 et 100 fichiers (sauvegardé sur le serveur)</span>
</div>
</section>
<!-- Performance Settings — Backend -->
<section class="config-section">
<h2>Paramètres backend <span class="config-badge-restart">Redémarrage requis</span></h2>
+66
View File
@@ -3353,3 +3353,69 @@ body.popup-mode .content-area {
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
box-sizing: border-box;
}
/* ---------------------------------------------------------------------------
Recent Tab Styles
--------------------------------------------------------------------------- */
/* Liste récente */
.recent-list { display: flex; flex-direction: column; gap: 2px; padding: 4px 0; }
/* Item */
.recent-item {
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
border-left: 3px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.recent-item:hover {
background: var(--bg-secondary);
border-left-color: var(--accent);
}
.recent-item.active { border-left-color: var(--accent); background: var(--bg-secondary); }
/* Header: temps + badge vault */
.recent-item-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 3px;
}
.recent-time { font-size: 11px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
.recent-vault-badge {
font-size: 10px; font-weight: 600; padding: 1px 6px;
border-radius: 10px; background: var(--accent); color: #fff; opacity: 0.85;
}
/* Titre */
.recent-item-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 2px; }
/* Breadcrumb path */
.recent-item-path { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; font-family: monospace; }
/* Preview */
.recent-item-preview {
font-size: 12px; color: var(--text-muted);
display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; line-height: 1.4; margin-bottom: 5px;
}
/* Tags */
.recent-item-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.recent-item-tags .tag-pill {
font-size: 10px; padding: 1px 6px; border-radius: 10px;
background: var(--bg-secondary); color: var(--accent); border: 1px solid var(--border);
}
/* Filtre vault */
.recent-filter-bar { padding: 8px 12px 4px; }
.recent-filter-bar select {
width: 100%; padding: 4px 8px; font-size: 12px;
background: var(--bg-secondary); color: var(--text-primary);
border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
}
/* État vide */
.recent-empty {
display: flex; flex-direction: column; align-items: center;
padding: 32px 16px; color: var(--text-muted); gap: 8px; font-size: 13px;
}