ObsiGate/frontend/js/dashboard.js
Bruno Charest 7edbd7a31a
All checks were successful
CI / lint (push) Successful in 14s
CI / security (push) Successful in 9s
CI / test (push) Successful in 18s
CI / build (push) Successful in 5s
fix: imports manquants — TabManager, showToast, closeHeaderMenu, closeMobileSidebar, ContextMenuManager, RightSidebarManager
2026-05-29 09:01:33 -04:00

466 lines
18 KiB
JavaScript

// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
import { api } from './auth.js';
import { state } from './state.js';
import { escapeHtml, safeCreateIcons, getFileIcon } from './utils.js';
import { icon } from './viewer.js';
import { TabManager, showToast } from './ui.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 || state.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 state.allVaults !== "undefined" && Array.isArray(state.allVaults)) {
state.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 || state.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, DashboardSharedWidget };