Files
ObsiGate/frontend/js/dashboard.js
T
bruno 4836d6f1d0
CI / lint (push) Successful in 10s
CI / security (push) Successful in 8s
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
refactor: split app.js (8875 lines) into 12 ES modules
frontend/js/ structure:
  state.js      (55 lines)  — Shared mutable state, constants
  utils.js      (510 lines) — EXT_ICONS, getFileIcon, escapeHtml, safeCreateIcons
  auth.js       (547 lines) — api(), AuthManager, initLoginForm, AdminPanel
  search.js     (1106 lines)— SearchHistory, QueryParser, Autocomplete, performSearch
  sidebar.js    (1091 lines)— Vault tree, sidebar filter, TagFilterService, loadTags
  viewer.js     (1554 lines)— openFile, Outline, ScrollSpy, Frontmatter, Editor
  ui.js         (2250 lines)— Theme, Toast, Sidebar, Dropdowns, Tabs, ContextMenu
  dashboard.js  (461 lines) — Dashboard widgets (Recent, Stats, Bookmarks)
  config.js     (999 lines) — Config panel, Hidden files, About, Sidebar tabs
  sync.js       (436 lines) — SSE/IndexUpdateManager, PWA registration
  graph.js      (401 lines) — GraphViewManager (force-directed canvas graph)
  legacy.js     (550 lines) — Remaining bridge functions (goHome, showWelcome, initSearch)
  app.js        (80 lines)  — Thin orchestrator: imports all modules, calls init()

index.html: switched from <script src="app.js"> to <script type="module" src="js/app.js">
Original app.js preserved for backward compatibility.
All 14 modules pass node --check syntax validation.
2026-05-28 14:04:50 -04:00

462 lines
18 KiB
JavaScript

// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
import { selectedContextVault, allVaults } from './state.js';
// ---------------------------------------------------------------------------
// Recent files
// ---------------------------------------------------------------------------
let _recentRefreshTimer = null;
let _recentTimestampTimer = null;
let _recentFilesCache = [];
// ---------------------------------------------------------------------------
// Dashboard Recent Files Widget
// ---------------------------------------------------------------------------
// ── Dashboard Stats Widget ──
const DashboardStatsWidget = {
async load() {
const grid = document.getElementById("dashboard-stats-grid");
if (!grid) return;
grid.innerHTML = '<div class="dashboard-stats-loading">Chargement...</div>';
try {
const data = await api("/api/dashboard");
this.render(data);
} catch (err) {
grid.innerHTML = `<div class="dashboard-recent-empty">Erreur: ${escapeHtml(err.message)}</div>`;
}
},
render(data) {
const grid = document.getElementById("dashboard-stats-grid");
if (!grid) return;
const fmtSize = (bytes) => bytes < 1024 ? `${bytes} o` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)} Ko` : bytes < 1073741824 ? `${(bytes/1048576).toFixed(1)} Mo` : `${(bytes/1073741824).toFixed(1)} Go`;
const items = [
{ icon: "files", label: "Fichiers", value: data.total_files.toLocaleString() },
{ icon: "tags", label: "Tags uniques", value: data.total_tags.toLocaleString() },
{ icon: "hard-drive", label: "Taille totale", value: fmtSize(data.total_size_bytes) },
{ icon: "folder-open", label: "Vaults", value: data.vaults.length.toString() },
];
grid.innerHTML = items.map(i => `
<div class="stat-card">
<i data-lucide="${i.icon}" class="stat-icon"></i>
<span class="stat-value">${i.value}</span>
<span class="stat-label">${i.label}</span>
</div>
`).join("");
safeCreateIcons();
}
};
// ── Dashboard Shared Widget ──
const DashboardSharedWidget = {
async load() {
const grid = document.getElementById("dashboard-shared-grid");
const empty = document.getElementById("dashboard-shared-empty");
if (!grid) return;
try {
const shares = await api("/api/shares");
if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
if (empty) empty.style.display = "none";
grid.innerHTML = shares.map(s => `
<div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
<div class="shared-card-header">
<i data-lucide="file-text" style="width:14px;height:14px"></i>
<span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
<span class="shared-card-vault">${escapeHtml(s.vault)}</span>
</div>
<div class="shared-card-meta">
<span>${s.access_count || 0} vue(s)</span>
${s.expires_at ? `<span>Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")}</span>` : ""}
</div>
<div class="shared-card-actions">
<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">📋 Copier</button>
<button class="shared-open-btn">📂 Ouvrir</button>
<button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
</div>
</div>
`).join("");
lucide.createIcons();
grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
const url = b.dataset.url;
try { await navigator.clipboard.writeText(url); } catch { 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("Lien copié !", "success");
}));
grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
e.stopPropagation();
const card = b.closest(".shared-card");
if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
}));
grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
showToast("Partage révoqué", "success");
this.load();
}));
grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
TabManager.openPreview(card.dataset.vault, card.dataset.path);
}));
} catch (err) { if (empty) empty.style.display = ""; }
}
};
// ── Dashboard Conflicts Widget ──
const DashboardConflictsWidget = {
async load() {
const container = document.getElementById("dashboard-conflicts-container");
if (!container) return;
try {
const data = await api("/api/conflicts");
if (data.total === 0) { container.innerHTML = ""; return; }
this.render(data.conflicts, container);
} catch (err) { container.innerHTML = ""; }
},
render(conflicts, container) {
container.innerHTML = `
<div class="dashboard-section">
<div class="dashboard-header">
<div class="dashboard-title-row">
<i data-lucide="alert-triangle" class="dashboard-icon" style="color:var(--accent-orange)"></i>
<h2>Conflits de synchronisation</h2>
<span class="dashboard-badge" style="background:var(--accent-orange)">${conflicts.length}</span>
</div>
</div>
<div class="dashboard-conflicts-grid">
${conflicts.map(c => `
<div class="conflict-card">
<div class="conflict-info">
<span class="conflict-vault">${escapeHtml(c.vault)}</span>
<span class="conflict-name">${escapeHtml(c.conflict_path.split("/").pop())}</span>
<span class="conflict-date">Conflit du ${c.conflict_date.replace(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/, "$3/$2/$1 $4:$5")}</span>
</div>
<div class="conflict-actions">
<button class="conflict-btn keep-local" data-vault="${escapeHtml(c.vault)}" data-conflict="${escapeHtml(c.conflict_path)}" data-original="${escapeHtml(c.original_path)}">Garder l'original</button>
<button class="conflict-btn keep-conflict" data-vault="${escapeHtml(c.vault)}" data-conflict="${escapeHtml(c.conflict_path)}" data-original="${escapeHtml(c.original_path)}">Garder le conflit</button>
</div>
</div>
`).join("")}
</div>
</div>`;
lucide.createIcons();
container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local")));
container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict")));
},
async _resolve(d, action) {
try {
await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) });
showToast("Conflit résolu", "success");
this.load();
} catch (err) { showToast("Erreur: " + err.message, "error"); }
}
};
const DashboardRecentWidget = {
_cache: [],
_currentFilter: "",
async load(vaultFilter = "") {
const v = vaultFilter || selectedContextVault || "all";
this._currentFilter = v;
this.showLoading();
let url = "/api/recent?mode=opened";
if (v !== "all") url += `&vault=${encodeURIComponent(v)}`;
try {
const data = await api(url);
this._cache = data.files || [];
this.render();
} catch (err) {
console.error("Dashboard: Failed to load recent files:", err);
this.showError();
}
},
async toggleBookmark(vault, path, title, card) {
try {
const data = await api("/api/bookmarks/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vault, path, title }),
});
// Refresh both widgets to keep sync
DashboardBookmarkWidget.load();
// Update current card icon if it exists
if (card) {
const btn = card.querySelector(".dashboard-card-bookmark-btn");
if (btn) {
btn.classList.toggle("active", data.bookmarked);
const icon = btn.querySelector("i");
if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus");
safeCreateIcons();
}
}
// Check if we need to refresh the current list to reflect bookmark status across all cards
// To avoid flickering, just update the cache and re-render if needed or do a silent refresh
this._cache.forEach(f => {
if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked;
});
} catch (err) {
console.error("Failed to toggle bookmark:", err);
showToast("Erreur lors de l'épinglage", "error");
}
},
showLoading() {
const grid = document.getElementById("dashboard-recent-grid");
const loading = document.getElementById("dashboard-loading");
const empty = document.getElementById("dashboard-recent-empty");
const count = document.getElementById("dashboard-count");
if (grid) grid.innerHTML = "";
if (loading) loading.classList.add("active");
if (empty) empty.classList.add("hidden");
if (count) count.textContent = "";
},
render() {
const grid = document.getElementById("dashboard-recent-grid");
const loading = document.getElementById("dashboard-loading");
const empty = document.getElementById("dashboard-recent-empty");
const count = document.getElementById("dashboard-count");
if (loading) loading.classList.remove("active");
if (!this._cache || this._cache.length === 0) {
this.showEmpty();
return;
}
if (empty) empty.classList.add("hidden");
if (count) count.textContent = `${this._cache.length} fichier${this._cache.length > 1 ? "s" : ""}`;
if (!grid) return;
grid.innerHTML = "";
this._cache.forEach((f, index) => {
const card = this._createCard(f, index);
grid.appendChild(card);
});
safeCreateIcons();
},
_createCard(file, index) {
const card = document.createElement("div");
card.className = "dashboard-card";
card.setAttribute("data-vault", file.vault);
card.setAttribute("data-path", file.path);
card.style.animationDelay = `${Math.min(index * 50, 400)}ms`;
// Header with icon and vault badge
const header = document.createElement("div");
header.className = "dashboard-card-header";
const iconContainer = document.createElement("div");
iconContainer.className = "dashboard-card-icon";
const fileIconName = getFileIcon(file.path);
try {
iconContainer.appendChild(icon(fileIconName, 24));
} catch (e) {
console.error("Error creating icon:", fileIconName, e);
// Fallback to default file icon
iconContainer.appendChild(icon("file", 24));
}
const badge = document.createElement("span");
badge.className = "dashboard-vault-badge";
badge.textContent = file.vault;
const bookmarkBtn = document.createElement("button");
bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`;
bookmarkBtn.title = file.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
bookmarkBtn.innerHTML = `<i data-lucide="${file.bookmarked ? "bookmark" : "bookmark-plus"}" style="width:14px;height:14px"></i>`;
bookmarkBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.toggleBookmark(file.vault, file.path, file.title, card);
});
header.appendChild(iconContainer);
header.appendChild(badge);
header.appendChild(bookmarkBtn);
card.appendChild(header);
// Title
const title = document.createElement("h3");
title.className = "dashboard-card-title";
title.textContent = file.title || file.path.split("/").pop();
title.title = file.title || file.path;
card.appendChild(title);
// Path (compact)
const pathParts = file.path.split("/");
if (pathParts.length > 1) {
const path = document.createElement("div");
path.className = "dashboard-card-path";
path.textContent = pathParts.slice(0, -1).join(" / ");
path.title = file.path;
card.appendChild(path);
}
// Footer with time and tags
const footer = document.createElement("div");
footer.className = "dashboard-card-footer";
const time = document.createElement("span");
time.className = "dashboard-card-time";
time.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> ${file.mtime_human || this._humanizeDelta(file.mtime)}`;
footer.appendChild(time);
// Tags
if (file.tags && file.tags.length > 0) {
const tags = document.createElement("div");
tags.className = "dashboard-card-tags";
file.tags.slice(0, 3).forEach((tag) => {
const tagEl = document.createElement("span");
tagEl.className = "tag-pill";
tagEl.textContent = tag;
tags.appendChild(tagEl);
});
footer.appendChild(tags);
}
card.appendChild(footer);
// Click handler
card.addEventListener("click", () => {
openFile(file.vault, file.path);
});
return card;
},
showEmpty() {
const grid = document.getElementById("dashboard-recent-grid");
const loading = document.getElementById("dashboard-loading");
const empty = document.getElementById("dashboard-recent-empty");
const count = document.getElementById("dashboard-count");
if (grid) grid.innerHTML = "";
if (loading) loading.classList.remove("active");
if (empty) empty.classList.remove("hidden");
if (count) count.textContent = "0 fichiers";
safeCreateIcons();
},
showError() {
this.showEmpty();
const empty = document.getElementById("dashboard-recent-empty");
if (empty) {
const msg = empty.querySelector("span");
if (msg) msg.textContent = "Erreur de chargement";
}
},
_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" });
},
populateVaultFilter() {
const select = document.getElementById("dashboard-vault-filter");
if (!select) return;
// Keep first option "Tous les vaults"
while (select.options.length > 1) select.remove(1);
if (typeof allVaults !== "undefined" && Array.isArray(allVaults)) {
allVaults.forEach((v) => {
const opt = document.createElement("option");
opt.value = v.name;
opt.textContent = v.name;
select.appendChild(opt);
});
}
syncVaultSelectors();
},
init() {
const select = document.getElementById("dashboard-vault-filter");
if (select) {
select.addEventListener("change", async () => {
await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
});
}
this.populateVaultFilter();
},
};
// ── Dashboard Bookmarks Widget ──
// (moved from app.js 3810-3870; logically belongs with dashboard widgets)
const DashboardBookmarkWidget = {
_cache: [],
_currentFilter: "",
async load(vaultFilter = "") {
const v = vaultFilter || selectedContextVault || "all";
this._currentFilter = v;
this.showLoading();
let url = "/api/bookmarks";
if (v !== "all") url += `?vault=${encodeURIComponent(v)}`;
try {
const data = await api(url);
this._cache = data.files || [];
this.render();
} catch (err) {
console.error("Dashboard: Failed to load bookmarks:", err);
this.showEmpty();
}
},
showLoading() {
const grid = document.getElementById("dashboard-bookmarks-grid");
const empty = document.getElementById("dashboard-bookmarks-empty");
const section = document.getElementById("dashboard-bookmarks-section");
if (grid) grid.innerHTML = "";
if (empty) empty.classList.add("hidden");
},
render() {
const grid = document.getElementById("dashboard-bookmarks-grid");
const empty = document.getElementById("dashboard-bookmarks-empty");
const section = document.getElementById("dashboard-bookmarks-section");
if (!this._cache || this._cache.length === 0) {
if (grid) grid.innerHTML = "";
if (empty) empty.classList.remove("hidden");
return;
}
if (empty) empty.classList.add("hidden");
if (!grid) return;
grid.innerHTML = "";
this._cache.forEach((f, idx) => {
const card = DashboardRecentWidget._createCard(f, idx);
grid.appendChild(card);
});
safeCreateIcons();
},
showEmpty() {
const grid = document.getElementById("dashboard-bookmarks-grid");
const empty = document.getElementById("dashboard-bookmarks-empty");
if (grid) grid.innerHTML = "";
if (empty) empty.classList.remove("hidden");
}
};
export { DashboardRecentWidget, DashboardStatsWidget, DashboardBookmarkWidget };