refactor: state.js → mutable object to fix 'assignment to constant' errors
ES module imports are read-only live bindings — can't reassign
imported let/const variables. Replace individual 'export let' with
single 'export const state = {...}' mutable object.
All modules updated: import { state } from './state.js'
All state access changed to state.xxx pattern.
Fixes cascade of 'Assignment to constant variable' errors.
This commit is contained in:
parent
16e0860a69
commit
7866f93778
1095
frontend/js/auth.js
1095
frontend/js/auth.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,461 +1,462 @@
|
||||
// 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, DashboardSharedWidget };
|
||||
1|// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
|
||||
import { state } from './state.js';
|
||||
3|
|
||||
4|// ---------------------------------------------------------------------------
|
||||
5|// Recent files
|
||||
6|// ---------------------------------------------------------------------------
|
||||
7|let _recentRefreshTimer = null;
|
||||
8|let _recentTimestampTimer = null;
|
||||
9|let _recentFilesCache = [];
|
||||
10|
|
||||
11|// ---------------------------------------------------------------------------
|
||||
12|// Dashboard Recent Files Widget
|
||||
13|// ---------------------------------------------------------------------------
|
||||
14|// ── Dashboard Stats Widget ──
|
||||
15|const DashboardStatsWidget = {
|
||||
16| async load() {
|
||||
17| const grid = document.getElementById("dashboard-stats-grid");
|
||||
18| if (!grid) return;
|
||||
19| grid.innerHTML = '<div class="dashboard-stats-loading">Chargement...</div>';
|
||||
20| try {
|
||||
21| const data = await api("/api/dashboard");
|
||||
22| this.render(data);
|
||||
23| } catch (err) {
|
||||
24| grid.innerHTML = `<div class="dashboard-recent-empty">Erreur: ${escapeHtml(err.message)}</div>`;
|
||||
25| }
|
||||
26| },
|
||||
27| render(data) {
|
||||
28| const grid = document.getElementById("dashboard-stats-grid");
|
||||
29| if (!grid) return;
|
||||
30| 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`;
|
||||
31| const items = [
|
||||
32| { icon: "files", label: "Fichiers", value: data.total_files.toLocaleString() },
|
||||
33| { icon: "tags", label: "Tags uniques", value: data.total_tags.toLocaleString() },
|
||||
34| { icon: "hard-drive", label: "Taille totale", value: fmtSize(data.total_size_bytes) },
|
||||
35| { icon: "folder-open", label: "Vaults", value: data.vaults.length.toString() },
|
||||
36| ];
|
||||
37| grid.innerHTML = items.map(i => `
|
||||
38| <div class="stat-card">
|
||||
39| <i data-lucide="${i.icon}" class="stat-icon"></i>
|
||||
40| <span class="stat-value">${i.value}</span>
|
||||
41| <span class="stat-label">${i.label}</span>
|
||||
42| </div>
|
||||
43| `).join("");
|
||||
44| safeCreateIcons();
|
||||
45| }
|
||||
46|};
|
||||
47|
|
||||
48|// ── Dashboard Shared Widget ──
|
||||
49|const DashboardSharedWidget = {
|
||||
50| async load() {
|
||||
51| const grid = document.getElementById("dashboard-shared-grid");
|
||||
52| const empty = document.getElementById("dashboard-shared-empty");
|
||||
53| if (!grid) return;
|
||||
54| try {
|
||||
55| const shares = await api("/api/shares");
|
||||
56| if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
|
||||
57| if (empty) empty.style.display = "none";
|
||||
58| grid.innerHTML = shares.map(s => `
|
||||
59| <div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
|
||||
60| <div class="shared-card-header">
|
||||
61| <i data-lucide="file-text" style="width:14px;height:14px"></i>
|
||||
62| <span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
|
||||
63| <span class="shared-card-vault">${escapeHtml(s.vault)}</span>
|
||||
64| </div>
|
||||
65| <div class="shared-card-meta">
|
||||
66| <span>${s.access_count || 0} vue(s)</span>
|
||||
67| ${s.expires_at ? `<span>Expire le ${new Date(s.expires_at).toLocaleDateString("fr-FR")}</span>` : ""}
|
||||
68| </div>
|
||||
69| <div class="shared-card-actions">
|
||||
70| <button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">📋 Copier</button>
|
||||
71| <button class="shared-open-btn">📂 Ouvrir</button>
|
||||
72| <button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
|
||||
73| </div>
|
||||
74| </div>
|
||||
75| `).join("");
|
||||
76| lucide.createIcons();
|
||||
77| grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||
78| e.stopPropagation();
|
||||
79| const url = b.dataset.url;
|
||||
80| 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); }
|
||||
81| showToast("Lien copié !", "success");
|
||||
82| }));
|
||||
83| grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
|
||||
84| e.stopPropagation();
|
||||
85| const card = b.closest(".shared-card");
|
||||
86| if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||
87| }));
|
||||
88| grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
|
||||
89| e.stopPropagation();
|
||||
90| await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
|
||||
91| showToast("Partage révoqué", "success");
|
||||
92| this.load();
|
||||
93| }));
|
||||
94| grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
|
||||
95| TabManager.openPreview(card.dataset.vault, card.dataset.path);
|
||||
96| }));
|
||||
97| } catch (err) { if (empty) empty.style.display = ""; }
|
||||
98| }
|
||||
99|};
|
||||
100|
|
||||
101|// ── Dashboard Conflicts Widget ──
|
||||
102|const DashboardConflictsWidget = {
|
||||
103| async load() {
|
||||
104| const container = document.getElementById("dashboard-conflicts-container");
|
||||
105| if (!container) return;
|
||||
106| try {
|
||||
107| const data = await api("/api/conflicts");
|
||||
108| if (data.total === 0) { container.innerHTML = ""; return; }
|
||||
109| this.render(data.conflicts, container);
|
||||
110| } catch (err) { container.innerHTML = ""; }
|
||||
111| },
|
||||
112| render(conflicts, container) {
|
||||
113| container.innerHTML = `
|
||||
114| <div class="dashboard-section">
|
||||
115| <div class="dashboard-header">
|
||||
116| <div class="dashboard-title-row">
|
||||
117| <i data-lucide="alert-triangle" class="dashboard-icon" style="color:var(--accent-orange)"></i>
|
||||
118| <h2>Conflits de synchronisation</h2>
|
||||
119| <span class="dashboard-badge" style="background:var(--accent-orange)">${conflicts.length}</span>
|
||||
120| </div>
|
||||
121| </div>
|
||||
122| <div class="dashboard-conflicts-grid">
|
||||
123| ${conflicts.map(c => `
|
||||
124| <div class="conflict-card">
|
||||
125| <div class="conflict-info">
|
||||
126| <span class="conflict-vault">${escapeHtml(c.vault)}</span>
|
||||
127| <span class="conflict-name">${escapeHtml(c.conflict_path.split("/").pop())}</span>
|
||||
128| <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>
|
||||
129| </div>
|
||||
130| <div class="conflict-actions">
|
||||
131| <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>
|
||||
132| <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>
|
||||
133| </div>
|
||||
134| </div>
|
||||
135| `).join("")}
|
||||
136| </div>
|
||||
137| </div>`;
|
||||
138| lucide.createIcons();
|
||||
139| container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local")));
|
||||
140| container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict")));
|
||||
141| },
|
||||
142| async _resolve(d, action) {
|
||||
143| try {
|
||||
144| await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) });
|
||||
145| showToast("Conflit résolu", "success");
|
||||
146| this.load();
|
||||
147| } catch (err) { showToast("Erreur: " + err.message, "error"); }
|
||||
148| }
|
||||
149|};
|
||||
150|
|
||||
151|const DashboardRecentWidget = {
|
||||
152| _cache: [],
|
||||
153| _currentFilter: "",
|
||||
154|
|
||||
155| async load(vaultFilter = "") {
|
||||
156| const v = vaultFilter || selectedContextVault || "all";
|
||||
157| this._currentFilter = v;
|
||||
158| this.showLoading();
|
||||
159|
|
||||
160| let url = "/api/recent?mode=opened";
|
||||
161| if (v !== "all") url += `&vault=${encodeURIComponent(v)}`;
|
||||
162|
|
||||
163| try {
|
||||
164| const data = await api(url);
|
||||
165| this._cache = data.files || [];
|
||||
166| this.render();
|
||||
167| } catch (err) {
|
||||
168| console.error("Dashboard: Failed to load recent files:", err);
|
||||
169| this.showError();
|
||||
170| }
|
||||
171| },
|
||||
172|
|
||||
173| async toggleBookmark(vault, path, title, card) {
|
||||
174| try {
|
||||
175| const data = await api("/api/bookmarks/toggle", {
|
||||
176| method: "POST",
|
||||
177| headers: { "Content-Type": "application/json" },
|
||||
178| body: JSON.stringify({ vault, path, title }),
|
||||
179| });
|
||||
180|
|
||||
181| // Refresh both widgets to keep sync
|
||||
182| DashboardBookmarkWidget.load();
|
||||
183|
|
||||
184| // Update current card icon if it exists
|
||||
185| if (card) {
|
||||
186| const btn = card.querySelector(".dashboard-card-bookmark-btn");
|
||||
187| if (btn) {
|
||||
188| btn.classList.toggle("active", data.bookmarked);
|
||||
189| const icon = btn.querySelector("i");
|
||||
190| if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus");
|
||||
191| safeCreateIcons();
|
||||
192| }
|
||||
193| }
|
||||
194|
|
||||
195| // Check if we need to refresh the current list to reflect bookmark status across all cards
|
||||
196| // To avoid flickering, just update the cache and re-render if needed or do a silent refresh
|
||||
197| this._cache.forEach(f => {
|
||||
198| if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked;
|
||||
199| });
|
||||
200| } catch (err) {
|
||||
201| console.error("Failed to toggle bookmark:", err);
|
||||
202| showToast("Erreur lors de l'épinglage", "error");
|
||||
203| }
|
||||
204| },
|
||||
205|
|
||||
206| showLoading() {
|
||||
207| const grid = document.getElementById("dashboard-recent-grid");
|
||||
208| const loading = document.getElementById("dashboard-loading");
|
||||
209| const empty = document.getElementById("dashboard-recent-empty");
|
||||
210| const count = document.getElementById("dashboard-count");
|
||||
211|
|
||||
212| if (grid) grid.innerHTML = "";
|
||||
213| if (loading) loading.classList.add("active");
|
||||
214| if (empty) empty.classList.add("hidden");
|
||||
215| if (count) count.textContent = "";
|
||||
216| },
|
||||
217|
|
||||
218| render() {
|
||||
219| const grid = document.getElementById("dashboard-recent-grid");
|
||||
220| const loading = document.getElementById("dashboard-loading");
|
||||
221| const empty = document.getElementById("dashboard-recent-empty");
|
||||
222| const count = document.getElementById("dashboard-count");
|
||||
223|
|
||||
224| if (loading) loading.classList.remove("active");
|
||||
225|
|
||||
226| if (!this._cache || this._cache.length === 0) {
|
||||
227| this.showEmpty();
|
||||
228| return;
|
||||
229| }
|
||||
230|
|
||||
231| if (empty) empty.classList.add("hidden");
|
||||
232| if (count) count.textContent = `${this._cache.length} fichier${this._cache.length > 1 ? "s" : ""}`;
|
||||
233|
|
||||
234| if (!grid) return;
|
||||
235| grid.innerHTML = "";
|
||||
236|
|
||||
237| this._cache.forEach((f, index) => {
|
||||
238| const card = this._createCard(f, index);
|
||||
239| grid.appendChild(card);
|
||||
240| });
|
||||
241|
|
||||
242| safeCreateIcons();
|
||||
243| },
|
||||
244|
|
||||
245| _createCard(file, index) {
|
||||
246| const card = document.createElement("div");
|
||||
247| card.className = "dashboard-card";
|
||||
248| card.setAttribute("data-vault", file.vault);
|
||||
249| card.setAttribute("data-path", file.path);
|
||||
250| card.style.animationDelay = `${Math.min(index * 50, 400)}ms`;
|
||||
251|
|
||||
252| // Header with icon and vault badge
|
||||
253| const header = document.createElement("div");
|
||||
254| header.className = "dashboard-card-header";
|
||||
255|
|
||||
256| const iconContainer = document.createElement("div");
|
||||
257| iconContainer.className = "dashboard-card-icon";
|
||||
258| const fileIconName = getFileIcon(file.path);
|
||||
259| try {
|
||||
260| iconContainer.appendChild(icon(fileIconName, 24));
|
||||
261| } catch (e) {
|
||||
262| console.error("Error creating icon:", fileIconName, e);
|
||||
263| // Fallback to default file icon
|
||||
264| iconContainer.appendChild(icon("file", 24));
|
||||
265| }
|
||||
266|
|
||||
267| const badge = document.createElement("span");
|
||||
268| badge.className = "dashboard-vault-badge";
|
||||
269| badge.textContent = file.vault;
|
||||
270|
|
||||
271| const bookmarkBtn = document.createElement("button");
|
||||
272| bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`;
|
||||
273| bookmarkBtn.title = file.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
|
||||
274| bookmarkBtn.innerHTML = `<i data-lucide="${file.bookmarked ? "bookmark" : "bookmark-plus"}" style="width:14px;height:14px"></i>`;
|
||||
275|
|
||||
276| bookmarkBtn.addEventListener("click", (e) => {
|
||||
277| e.stopPropagation();
|
||||
278| this.toggleBookmark(file.vault, file.path, file.title, card);
|
||||
279| });
|
||||
280|
|
||||
281| header.appendChild(iconContainer);
|
||||
282| header.appendChild(badge);
|
||||
283| header.appendChild(bookmarkBtn);
|
||||
284| card.appendChild(header);
|
||||
285|
|
||||
286| // Title
|
||||
287| const title = document.createElement("h3");
|
||||
288| title.className = "dashboard-card-title";
|
||||
289| title.textContent = file.title || file.path.split("/").pop();
|
||||
290| title.title = file.title || file.path;
|
||||
291| card.appendChild(title);
|
||||
292|
|
||||
293| // Path (compact)
|
||||
294| const pathParts = file.path.split("/");
|
||||
295| if (pathParts.length > 1) {
|
||||
296| const path = document.createElement("div");
|
||||
297| path.className = "dashboard-card-path";
|
||||
298| path.textContent = pathParts.slice(0, -1).join(" / ");
|
||||
299| path.title = file.path;
|
||||
300| card.appendChild(path);
|
||||
301| }
|
||||
302|
|
||||
303| // Footer with time and tags
|
||||
304| const footer = document.createElement("div");
|
||||
305| footer.className = "dashboard-card-footer";
|
||||
306|
|
||||
307| const time = document.createElement("span");
|
||||
308| time.className = "dashboard-card-time";
|
||||
309| 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)}`;
|
||||
310|
|
||||
311| footer.appendChild(time);
|
||||
312|
|
||||
313| // Tags
|
||||
314| if (file.tags && file.tags.length > 0) {
|
||||
315| const tags = document.createElement("div");
|
||||
316| tags.className = "dashboard-card-tags";
|
||||
317| file.tags.slice(0, 3).forEach((tag) => {
|
||||
318| const tagEl = document.createElement("span");
|
||||
319| tagEl.className = "tag-pill";
|
||||
320| tagEl.textContent = tag;
|
||||
321| tags.appendChild(tagEl);
|
||||
322| });
|
||||
323| footer.appendChild(tags);
|
||||
324| }
|
||||
325|
|
||||
326| card.appendChild(footer);
|
||||
327|
|
||||
328| // Click handler
|
||||
329| card.addEventListener("click", () => {
|
||||
330| openFile(file.vault, file.path);
|
||||
331| });
|
||||
332|
|
||||
333| return card;
|
||||
334| },
|
||||
335|
|
||||
336| showEmpty() {
|
||||
337| const grid = document.getElementById("dashboard-recent-grid");
|
||||
338| const loading = document.getElementById("dashboard-loading");
|
||||
339| const empty = document.getElementById("dashboard-recent-empty");
|
||||
340| const count = document.getElementById("dashboard-count");
|
||||
341|
|
||||
342| if (grid) grid.innerHTML = "";
|
||||
343| if (loading) loading.classList.remove("active");
|
||||
344| if (empty) empty.classList.remove("hidden");
|
||||
345| if (count) count.textContent = "0 fichiers";
|
||||
346| safeCreateIcons();
|
||||
347| },
|
||||
348|
|
||||
349| showError() {
|
||||
350| this.showEmpty();
|
||||
351| const empty = document.getElementById("dashboard-recent-empty");
|
||||
352| if (empty) {
|
||||
353| const msg = empty.querySelector("span");
|
||||
354| if (msg) msg.textContent = "Erreur de chargement";
|
||||
355| }
|
||||
356| },
|
||||
357|
|
||||
358| _humanizeDelta(mtime) {
|
||||
359| const delta = Date.now() / 1000 - mtime;
|
||||
360| if (delta < 60) return "à l'instant";
|
||||
361| if (delta < 3600) return `il y a ${Math.floor(delta / 60)} min`;
|
||||
362| if (delta < 86400) return `il y a ${Math.floor(delta / 3600)} h`;
|
||||
363| if (delta < 604800) return `il y a ${Math.floor(delta / 86400)} j`;
|
||||
364| return new Date(mtime * 1000).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" });
|
||||
365| },
|
||||
366|
|
||||
367| populateVaultFilter() {
|
||||
368| const select = document.getElementById("dashboard-vault-filter");
|
||||
369| if (!select) return;
|
||||
370|
|
||||
371| // Keep first option "Tous les vaults"
|
||||
372| while (select.options.length > 1) select.remove(1);
|
||||
373|
|
||||
374| if (typeof allVaults !== "undefined" && Array.isArray(state.allVaults)) {
|
||||
375| state.allVaults.forEach((v) => {
|
||||
376| const opt = document.createElement("option");
|
||||
377| opt.value = v.name;
|
||||
378| opt.textContent = v.name;
|
||||
379| select.appendChild(opt);
|
||||
380| });
|
||||
381| }
|
||||
382| syncVaultSelectors();
|
||||
383| },
|
||||
384|
|
||||
385| init() {
|
||||
386| const select = document.getElementById("dashboard-vault-filter");
|
||||
387| if (select) {
|
||||
388| select.addEventListener("change", async () => {
|
||||
389| await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
|
||||
390| });
|
||||
391| }
|
||||
392|
|
||||
393| this.populateVaultFilter();
|
||||
394| },
|
||||
395|};
|
||||
396|
|
||||
397|// ── Dashboard Bookmarks Widget ──
|
||||
398|// (moved from app.js 3810-3870; logically belongs with dashboard widgets)
|
||||
399|const DashboardBookmarkWidget = {
|
||||
400| _cache: [],
|
||||
401| _currentFilter: "",
|
||||
402|
|
||||
403| async load(vaultFilter = "") {
|
||||
404| const v = vaultFilter || selectedContextVault || "all";
|
||||
405| this._currentFilter = v;
|
||||
406| this.showLoading();
|
||||
407|
|
||||
408| let url = "/api/bookmarks";
|
||||
409| if (v !== "all") url += `?vault=${encodeURIComponent(v)}`;
|
||||
410|
|
||||
411| try {
|
||||
412| const data = await api(url);
|
||||
413| this._cache = data.files || [];
|
||||
414| this.render();
|
||||
415| } catch (err) {
|
||||
416| console.error("Dashboard: Failed to load bookmarks:", err);
|
||||
417| this.showEmpty();
|
||||
418| }
|
||||
419| },
|
||||
420|
|
||||
421| showLoading() {
|
||||
422| const grid = document.getElementById("dashboard-bookmarks-grid");
|
||||
423| const empty = document.getElementById("dashboard-bookmarks-empty");
|
||||
424| const section = document.getElementById("dashboard-bookmarks-section");
|
||||
425|
|
||||
426| if (grid) grid.innerHTML = "";
|
||||
427| if (empty) empty.classList.add("hidden");
|
||||
428| },
|
||||
429|
|
||||
430| render() {
|
||||
431| const grid = document.getElementById("dashboard-bookmarks-grid");
|
||||
432| const empty = document.getElementById("dashboard-bookmarks-empty");
|
||||
433| const section = document.getElementById("dashboard-bookmarks-section");
|
||||
434|
|
||||
435| if (!this._cache || this._cache.length === 0) {
|
||||
436| if (grid) grid.innerHTML = "";
|
||||
437| if (empty) empty.classList.remove("hidden");
|
||||
438| return;
|
||||
439| }
|
||||
440|
|
||||
441| if (empty) empty.classList.add("hidden");
|
||||
442| if (!grid) return;
|
||||
443| grid.innerHTML = "";
|
||||
444|
|
||||
445| this._cache.forEach((f, idx) => {
|
||||
446| const card = DashboardRecentWidget._createCard(f, idx);
|
||||
447| grid.appendChild(card);
|
||||
448| });
|
||||
449|
|
||||
450| safeCreateIcons();
|
||||
451| },
|
||||
452|
|
||||
453| showEmpty() {
|
||||
454| const grid = document.getElementById("dashboard-bookmarks-grid");
|
||||
455| const empty = document.getElementById("dashboard-bookmarks-empty");
|
||||
456| if (grid) grid.innerHTML = "";
|
||||
457| if (empty) empty.classList.remove("hidden");
|
||||
458| }
|
||||
459|};
|
||||
460|
|
||||
461|export { DashboardRecentWidget, DashboardStatsWidget, DashboardBookmarkWidget, DashboardSharedWidget };
|
||||
462|
|
||||
1471
frontend/js/graph.js
1471
frontend/js/graph.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,55 +1,59 @@
|
||||
/* ObsiGate — Shared application state */
|
||||
export const APP_VERSION = "1.5.0";
|
||||
/* ObsiGate — Shared mutable application state.
|
||||
Use state.xxx to read/write any value. ES module imports are read-only,
|
||||
so we export a single mutable object instead of individual let bindings. */
|
||||
export const state = {
|
||||
APP_VERSION: "1.5.0",
|
||||
|
||||
// Core navigation state
|
||||
export let currentVault = null;
|
||||
export let currentPath = null;
|
||||
export let allVaults = [];
|
||||
export let selectedContextVault = "all";
|
||||
// Core navigation
|
||||
currentVault: null,
|
||||
currentPath: null,
|
||||
allVaults: [],
|
||||
selectedContextVault: "all",
|
||||
|
||||
// Search state
|
||||
export let searchTimeout = null;
|
||||
export let searchAbortController = null;
|
||||
export let advancedSearchOffset = 0;
|
||||
export let advancedSearchTotal = 0;
|
||||
export let advancedSearchSort = "relevance";
|
||||
export let advancedSearchLastQuery = "";
|
||||
export let suggestAbortController = null;
|
||||
export let dropdownActiveIndex = -1;
|
||||
export let dropdownItems = [];
|
||||
export let currentSearchId = 0;
|
||||
export let selectedTags = [];
|
||||
export let searchCaseSensitive = false;
|
||||
export let searchWholeWord = false;
|
||||
export let searchRegex = false;
|
||||
export let searchFilterVisible = false;
|
||||
// Search
|
||||
searchTimeout: null,
|
||||
searchAbortController: null,
|
||||
advancedSearchOffset: 0,
|
||||
advancedSearchTotal: 0,
|
||||
advancedSearchSort: "relevance",
|
||||
advancedSearchLastQuery: "",
|
||||
suggestAbortController: null,
|
||||
dropdownActiveIndex: -1,
|
||||
dropdownItems: [],
|
||||
currentSearchId: 0,
|
||||
selectedTags: [],
|
||||
searchCaseSensitive: false,
|
||||
searchWholeWord: false,
|
||||
searchRegex: false,
|
||||
searchFilterVisible: false,
|
||||
|
||||
// Search constants
|
||||
export const SEARCH_HISTORY_KEY = "obsigate_search_history";
|
||||
export const MAX_HISTORY_ENTRIES = 50;
|
||||
export const SUGGEST_DEBOUNCE_MS = 150;
|
||||
export const ADVANCED_SEARCH_LIMIT = 50;
|
||||
export const MIN_SEARCH_LENGTH = 2;
|
||||
export const SEARCH_TIMEOUT_MS = 30000;
|
||||
// Search constants
|
||||
SEARCH_HISTORY_KEY: "obsigate_search_history",
|
||||
MAX_HISTORY_ENTRIES: 50,
|
||||
SUGGEST_DEBOUNCE_MS: 150,
|
||||
ADVANCED_SEARCH_LIMIT: 50,
|
||||
MIN_SEARCH_LENGTH: 2,
|
||||
SEARCH_TIMEOUT_MS: 30000,
|
||||
|
||||
// Viewer state
|
||||
export let showingSource = false;
|
||||
export let cachedRawSource = null;
|
||||
export let editorView = null;
|
||||
export let editorVault = null;
|
||||
export let editorPath = null;
|
||||
export let fallbackEditorEl = null;
|
||||
export let _iconDebounceTimer = null;
|
||||
// Viewer state
|
||||
showingSource: false,
|
||||
cachedRawSource: null,
|
||||
editorView: null,
|
||||
editorVault: null,
|
||||
editorPath: null,
|
||||
fallbackEditorEl: null,
|
||||
_iconDebounceTimer: null,
|
||||
|
||||
// Outline/TOC state
|
||||
export let outlineObserver = null;
|
||||
export let activeHeadingId = null;
|
||||
export let headingsCache = [];
|
||||
export let rightSidebarVisible = true;
|
||||
export let rightSidebarWidth = 280;
|
||||
// Outline/TOC
|
||||
outlineObserver: null,
|
||||
activeHeadingId: null,
|
||||
headingsCache: [],
|
||||
rightSidebarVisible: true,
|
||||
rightSidebarWidth: 280,
|
||||
|
||||
// Sidebar state
|
||||
export let sidebarFilterCaseSensitive = false;
|
||||
export let activeSidebarTab = "vaults";
|
||||
export let filterDebounce = null;
|
||||
export let vaultSettings = {};
|
||||
// Sidebar
|
||||
sidebarFilterCaseSensitive: false,
|
||||
activeSidebarTab: "vaults",
|
||||
filterDebounce: null,
|
||||
vaultSettings: {},
|
||||
};
|
||||
|
||||
@ -1,437 +1,438 @@
|
||||
/* ObsiGate — Sync: SSE client + PWA registration */
|
||||
import {
|
||||
currentVault,
|
||||
currentPath,
|
||||
activeSidebarTab,
|
||||
selectedContextVault
|
||||
} from './state.js';
|
||||
import { showToast } from './ui.js';
|
||||
import { loadVaults, loadTags, refreshTagsForContext } from './sidebar.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSE Client — IndexUpdateManager
|
||||
// ---------------------------------------------------------------------------
|
||||
export const IndexUpdateManager = (() => {
|
||||
let eventSource = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectDelay = 1000;
|
||||
const MAX_RECONNECT_DELAY = 30000;
|
||||
let recentEvents = [];
|
||||
const MAX_RECENT_EVENTS = 20;
|
||||
let connectionState = "disconnected"; // disconnected | connecting | connected
|
||||
|
||||
function connect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
connectionState = "connecting";
|
||||
_updateBadge();
|
||||
|
||||
eventSource = new EventSource("/api/events");
|
||||
|
||||
eventSource.addEventListener("connected", (e) => {
|
||||
connectionState = "connected";
|
||||
reconnectDelay = 1000;
|
||||
_updateBadge();
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_updated", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_updated", data);
|
||||
_onIndexUpdated(data);
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_reloaded", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_reloaded", data);
|
||||
_onIndexReloaded(data);
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("vault_added", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("vault_added", data);
|
||||
showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("vault_removed", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("vault_removed", data);
|
||||
showToast(`Vault "${data.vault}" supprimé`, "info");
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_start", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_start", data);
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_progress", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_progress", data);
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_complete", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_complete", data);
|
||||
connectionState = "connected";
|
||||
_updateBadge();
|
||||
showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
connectionState = "disconnected";
|
||||
_updateBadge();
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
_scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function _scheduleReconnect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
function _addEvent(type, data) {
|
||||
recentEvents.unshift({
|
||||
type,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
if (recentEvents.length > MAX_RECENT_EVENTS) {
|
||||
recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
|
||||
}
|
||||
}
|
||||
|
||||
async function _onIndexUpdated(data) {
|
||||
// Brief syncing state
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
|
||||
const n = data.total_changes || 0;
|
||||
const vaults = (data.vaults || []).join(", ");
|
||||
// Toast removed: silent auto-indexing — no notification needed
|
||||
|
||||
// Refresh sidebar and tags if affected vault matches current context
|
||||
const affectsCurrentVault = selectedContextVault === "all" || (data.vaults || []).includes(selectedContextVault);
|
||||
if (affectsCurrentVault) {
|
||||
try {
|
||||
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
|
||||
// Refresh current file if it was updated
|
||||
if (currentVault && currentPath) {
|
||||
const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === currentPath);
|
||||
if (changed) {
|
||||
openFile(currentVault, currentPath);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error refreshing after index update:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function _onIndexReloaded(data) {
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
showToast("Index complet rechargé", "info");
|
||||
try {
|
||||
await Promise.all([loadVaults(), loadTags()]);
|
||||
} catch (err) {
|
||||
console.error("Error refreshing after full reload:", err);
|
||||
}
|
||||
setTimeout(() => {
|
||||
connectionState = "connected";
|
||||
_updateBadge();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function _updateBadge() {
|
||||
const badge = document.getElementById("sync-badge");
|
||||
if (!badge) return;
|
||||
badge.className = "sync-badge sync-badge--" + connectionState;
|
||||
const labels = {
|
||||
disconnected: "Déconnecté",
|
||||
connecting: "Connexion...",
|
||||
connected: "Synchronisé",
|
||||
syncing: "Mise à jour...",
|
||||
};
|
||||
badge.title = labels[connectionState] || connectionState;
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
connectionState = "disconnected";
|
||||
_updateBadge();
|
||||
}
|
||||
|
||||
function getState() {
|
||||
return connectionState;
|
||||
}
|
||||
|
||||
function getRecentEvents() {
|
||||
return recentEvents;
|
||||
}
|
||||
|
||||
return { connect, disconnect, getState, getRecentEvents };
|
||||
})();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync status badge and panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function initSyncStatus() {
|
||||
const badge = document.getElementById("sync-badge");
|
||||
if (!badge) return;
|
||||
|
||||
badge.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleSyncPanel();
|
||||
});
|
||||
|
||||
IndexUpdateManager.connect();
|
||||
}
|
||||
|
||||
function toggleSyncPanel() {
|
||||
let panel = document.getElementById("sync-panel");
|
||||
if (panel) {
|
||||
panel.remove();
|
||||
return;
|
||||
}
|
||||
// Auto reconnect if disconnected when user opens the panel
|
||||
if (IndexUpdateManager.getState() === "disconnected") {
|
||||
IndexUpdateManager.connect();
|
||||
}
|
||||
panel = document.createElement("div");
|
||||
panel.id = "sync-panel";
|
||||
panel.className = "sync-panel";
|
||||
_renderSyncPanel(panel);
|
||||
document.body.appendChild(panel);
|
||||
|
||||
// Close on outside click
|
||||
setTimeout(() => {
|
||||
document.addEventListener("click", _closeSyncPanelOutside, { once: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _closeSyncPanelOutside(e) {
|
||||
const panel = document.getElementById("sync-panel");
|
||||
if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
|
||||
panel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function _renderSyncPanel(panel) {
|
||||
const state = IndexUpdateManager.getState();
|
||||
const events = IndexUpdateManager.getRecentEvents();
|
||||
|
||||
const stateLabels = {
|
||||
disconnected: "Déconnecté",
|
||||
connecting: "Connexion...",
|
||||
connected: "Connecté",
|
||||
syncing: "Synchronisation...",
|
||||
};
|
||||
|
||||
let html = `<div class="sync-panel__header">
|
||||
<span class="sync-panel__title">Synchronisation</span>
|
||||
<span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
|
||||
</div>`;
|
||||
|
||||
if (events.length === 0) {
|
||||
html += `<div class="sync-panel__empty">Aucun événement récent</div>`;
|
||||
} else {
|
||||
html += `<div class="sync-panel__events">`;
|
||||
events.slice(0, 10).forEach((ev) => {
|
||||
const time = new Date(ev.timestamp).toLocaleTimeString();
|
||||
const typeLabels = {
|
||||
index_updated: "Mise à jour",
|
||||
index_reloaded: "Rechargement",
|
||||
vault_added: "Vault ajouté",
|
||||
vault_removed: "Vault supprimé",
|
||||
index_start: "Démarrage index.",
|
||||
index_progress: "Vault indexé",
|
||||
index_complete: "Indexation tech.",
|
||||
};
|
||||
const label = typeLabels[ev.type] || ev.type;
|
||||
let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
|
||||
if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
|
||||
if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
|
||||
if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
|
||||
html += `<div class="sync-panel__event">
|
||||
<span class="sync-panel__event-type">${label}</span>
|
||||
<span class="sync-panel__event-detail">${detail}</span>
|
||||
<span class="sync-panel__event-time">${time}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
panel.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PWA Service Worker Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
function registerServiceWorker() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log("[PWA] Service Worker registered successfully:", registration.scope);
|
||||
|
||||
// Check for updates periodically
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 60000); // Check every minute
|
||||
|
||||
// Handle service worker updates
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
// New service worker available
|
||||
showUpdateNotification();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("[PWA] Service Worker registration failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdateNotification() {
|
||||
const message = document.createElement("div");
|
||||
message.className = "pwa-update-notification";
|
||||
message.innerHTML = `
|
||||
<div class="pwa-update-content">
|
||||
<span>Une nouvelle version d'ObsiGate est disponible !</span>
|
||||
<button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
|
||||
<button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Auto-dismiss after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (message.parentElement) {
|
||||
message.remove();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Handle install prompt
|
||||
let deferredPrompt;
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
|
||||
// Show install button if desired
|
||||
const installBtn = document.getElementById("pwa-install-btn");
|
||||
if (installBtn) {
|
||||
installBtn.style.display = "block";
|
||||
installBtn.addEventListener("click", async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
console.log(`[PWA] User response to install prompt: ${outcome}`);
|
||||
deferredPrompt = null;
|
||||
installBtn.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Log when app is installed
|
||||
window.addEventListener("appinstalled", () => {
|
||||
console.log("[PWA] ObsiGate has been installed");
|
||||
showToast("ObsiGate installé avec succès !", "success");
|
||||
});
|
||||
|
||||
// ── Dashboard tab switching (runs on page load and after rebuild) ──
|
||||
function initDashboardTabs() {
|
||||
document.querySelectorAll(".dashboard-tab").forEach(tab => {
|
||||
// Remove existing listeners by cloning
|
||||
const newTab = tab.cloneNode(true);
|
||||
tab.parentNode.replaceChild(newTab, tab);
|
||||
newTab.addEventListener("click", function() {
|
||||
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
|
||||
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
|
||||
if (panel) panel.classList.add("active");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Init — called by app.js orchestrator
|
||||
// ---------------------------------------------------------------------------
|
||||
export { initSyncStatus };
|
||||
export function init() {
|
||||
registerServiceWorker();
|
||||
initDashboardTabs();
|
||||
}
|
||||
1|/* ObsiGate — Sync: SSE client + PWA registration */
|
||||
2|import {
|
||||
3| state.currentVault,
|
||||
4| state.currentPath,
|
||||
5| state.activeSidebarTab,
|
||||
6| selectedContextVault
|
||||
7|} from './state.js';
|
||||
8|import { showToast } from './ui.js';
|
||||
9|import { loadVaults, loadTags, refreshTagsForContext } from './sidebar.js';
|
||||
10|
|
||||
11|// ---------------------------------------------------------------------------
|
||||
12|// SSE Client — IndexUpdateManager
|
||||
13|// ---------------------------------------------------------------------------
|
||||
14|export const IndexUpdateManager = (() => {
|
||||
15| let eventSource = null;
|
||||
16| let reconnectTimer = null;
|
||||
17| let reconnectDelay = 1000;
|
||||
18| const MAX_RECONNECT_DELAY = 30000;
|
||||
19| let recentEvents = [];
|
||||
20| const MAX_RECENT_EVENTS = 20;
|
||||
21| let connectionState = "disconnected"; // disconnected | connecting | connected
|
||||
22|
|
||||
23| function connect() {
|
||||
24| if (eventSource) {
|
||||
25| eventSource.close();
|
||||
26| }
|
||||
27| connectionState = "connecting";
|
||||
28| _updateBadge();
|
||||
29|
|
||||
30| eventSource = new EventSource("/api/events");
|
||||
31|
|
||||
32| eventSource.addEventListener("connected", (e) => {
|
||||
33| connectionState = "connected";
|
||||
34| reconnectDelay = 1000;
|
||||
35| _updateBadge();
|
||||
36| });
|
||||
37|
|
||||
38| eventSource.addEventListener("index_updated", (e) => {
|
||||
39| try {
|
||||
40| const data = JSON.parse(e.data);
|
||||
41| _addEvent("index_updated", data);
|
||||
42| _onIndexUpdated(data);
|
||||
43| } catch (err) {
|
||||
44| console.error("SSE parse error:", err);
|
||||
45| }
|
||||
46| });
|
||||
47|
|
||||
48| eventSource.addEventListener("index_reloaded", (e) => {
|
||||
49| try {
|
||||
50| const data = JSON.parse(e.data);
|
||||
51| _addEvent("index_reloaded", data);
|
||||
52| _onIndexReloaded(data);
|
||||
53| } catch (err) {
|
||||
54| console.error("SSE parse error:", err);
|
||||
55| }
|
||||
56| });
|
||||
57|
|
||||
58| eventSource.addEventListener("vault_added", (e) => {
|
||||
59| try {
|
||||
60| const data = JSON.parse(e.data);
|
||||
61| _addEvent("vault_added", data);
|
||||
62| showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
|
||||
63| loadVaults();
|
||||
64| loadTags();
|
||||
65| } catch (err) {
|
||||
66| console.error("SSE parse error:", err);
|
||||
67| }
|
||||
68| });
|
||||
69|
|
||||
70| eventSource.addEventListener("vault_removed", (e) => {
|
||||
71| try {
|
||||
72| const data = JSON.parse(e.data);
|
||||
73| _addEvent("vault_removed", data);
|
||||
74| showToast(`Vault "${data.vault}" supprimé`, "info");
|
||||
75| loadVaults();
|
||||
76| loadTags();
|
||||
77| } catch (err) {
|
||||
78| console.error("SSE parse error:", err);
|
||||
79| }
|
||||
80| });
|
||||
81|
|
||||
82| eventSource.addEventListener("index_start", (e) => {
|
||||
83| try {
|
||||
84| const data = JSON.parse(e.data);
|
||||
85| _addEvent("index_start", data);
|
||||
86| connectionState = "syncing";
|
||||
87| _updateBadge();
|
||||
88| showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
|
||||
89| } catch (err) {
|
||||
90| console.error("SSE parse error:", err);
|
||||
91| }
|
||||
92| });
|
||||
93|
|
||||
94| eventSource.addEventListener("index_progress", (e) => {
|
||||
95| try {
|
||||
96| const data = JSON.parse(e.data);
|
||||
97| _addEvent("index_progress", data);
|
||||
98| connectionState = "syncing";
|
||||
99| _updateBadge();
|
||||
100| loadVaults();
|
||||
101| loadTags();
|
||||
102| } catch (err) {
|
||||
103| console.error("SSE parse error:", err);
|
||||
104| }
|
||||
105| });
|
||||
106|
|
||||
107| eventSource.addEventListener("index_complete", (e) => {
|
||||
108| try {
|
||||
109| const data = JSON.parse(e.data);
|
||||
110| _addEvent("index_complete", data);
|
||||
111| connectionState = "connected";
|
||||
112| _updateBadge();
|
||||
113| showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
|
||||
114| loadVaults();
|
||||
115| loadTags();
|
||||
116| } catch (err) {
|
||||
117| console.error("SSE parse error:", err);
|
||||
118| }
|
||||
119| });
|
||||
120|
|
||||
121| eventSource.onerror = () => {
|
||||
122| connectionState = "disconnected";
|
||||
123| _updateBadge();
|
||||
124| eventSource.close();
|
||||
125| eventSource = null;
|
||||
126| _scheduleReconnect();
|
||||
127| };
|
||||
128| }
|
||||
129|
|
||||
130| function _scheduleReconnect() {
|
||||
131| if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
132| reconnectTimer = setTimeout(() => {
|
||||
133| reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
||||
134| connect();
|
||||
135| }, reconnectDelay);
|
||||
136| }
|
||||
137|
|
||||
138| function _addEvent(type, data) {
|
||||
139| recentEvents.unshift({
|
||||
140| type,
|
||||
141| data,
|
||||
142| timestamp: new Date().toISOString(),
|
||||
143| });
|
||||
144| if (recentEvents.length > MAX_RECENT_EVENTS) {
|
||||
145| recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
|
||||
146| }
|
||||
147| }
|
||||
148|
|
||||
149| async function _onIndexUpdated(data) {
|
||||
150| // Brief syncing state
|
||||
151| connectionState = "syncing";
|
||||
152| _updateBadge();
|
||||
153|
|
||||
154| const n = data.total_changes || 0;
|
||||
155| const vaults = (data.vaults || []).join(", ");
|
||||
156| // Toast removed: silent auto-indexing — no notification needed
|
||||
157|
|
||||
158| // Refresh sidebar and tags if affected vault matches current context
|
||||
159| const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
|
||||
160| if (affectsCurrentVault) {
|
||||
161| try {
|
||||
162| await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
|
||||
163| // Refresh current file if it was updated
|
||||
164| if (currentVault && state.currentPath) {
|
||||
165| const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath);
|
||||
166| if (changed) {
|
||||
167| openFile(state.currentVault, state.currentPath);
|
||||
168| }
|
||||
169| }
|
||||
170| } catch (err) {
|
||||
171| console.error("Error refreshing after index update:", err);
|
||||
172| }
|
||||
173| }
|
||||
174|
|
||||
175| // Refresh recent tab if it is active
|
||||
176| if (state.activeSidebarTab === "recent") {
|
||||
177| const vaultFilter = document.getElementById("recent-vault-filter");
|
||||
178| loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
|
||||
179| }
|
||||
180|
|
||||
181| setTimeout(() => {
|
||||
182| connectionState = "connected";
|
||||
183| _updateBadge();
|
||||
184| }, 1500);
|
||||
185| }
|
||||
186|
|
||||
187| async function _onIndexReloaded(data) {
|
||||
188| connectionState = "syncing";
|
||||
189| _updateBadge();
|
||||
190| showToast("Index complet rechargé", "info");
|
||||
191| try {
|
||||
192| await Promise.all([loadVaults(), loadTags()]);
|
||||
193| } catch (err) {
|
||||
194| console.error("Error refreshing after full reload:", err);
|
||||
195| }
|
||||
196| setTimeout(() => {
|
||||
197| connectionState = "connected";
|
||||
198| _updateBadge();
|
||||
199| }, 1500);
|
||||
200| }
|
||||
201|
|
||||
202| function _updateBadge() {
|
||||
203| const badge = document.getElementById("sync-badge");
|
||||
204| if (!badge) return;
|
||||
205| badge.className = "sync-badge sync-badge--" + connectionState;
|
||||
206| const labels = {
|
||||
207| disconnected: "Déconnecté",
|
||||
208| connecting: "Connexion...",
|
||||
209| connected: "Synchronisé",
|
||||
210| syncing: "Mise à jour...",
|
||||
211| };
|
||||
212| badge.title = labels[connectionState] || connectionState;
|
||||
213| }
|
||||
214|
|
||||
215| function disconnect() {
|
||||
216| if (eventSource) {
|
||||
217| eventSource.close();
|
||||
218| eventSource = null;
|
||||
219| }
|
||||
220| if (reconnectTimer) {
|
||||
221| clearTimeout(reconnectTimer);
|
||||
222| reconnectTimer = null;
|
||||
223| }
|
||||
224| connectionState = "disconnected";
|
||||
225| _updateBadge();
|
||||
226| }
|
||||
227|
|
||||
228| function getState() {
|
||||
229| return connectionState;
|
||||
230| }
|
||||
231|
|
||||
232| function getRecentEvents() {
|
||||
233| return recentEvents;
|
||||
234| }
|
||||
235|
|
||||
236| return { connect, disconnect, getState, getRecentEvents };
|
||||
237|})();
|
||||
238|
|
||||
239|// ---------------------------------------------------------------------------
|
||||
240|// Sync status badge and panel
|
||||
241|// ---------------------------------------------------------------------------
|
||||
242|
|
||||
243|function initSyncStatus() {
|
||||
244| const badge = document.getElementById("sync-badge");
|
||||
245| if (!badge) return;
|
||||
246|
|
||||
247| badge.addEventListener("click", (e) => {
|
||||
248| e.stopPropagation();
|
||||
249| toggleSyncPanel();
|
||||
250| });
|
||||
251|
|
||||
252| IndexUpdateManager.connect();
|
||||
253|}
|
||||
254|
|
||||
255|function toggleSyncPanel() {
|
||||
256| let panel = document.getElementById("sync-panel");
|
||||
257| if (panel) {
|
||||
258| panel.remove();
|
||||
259| return;
|
||||
260| }
|
||||
261| // Auto reconnect if disconnected when user opens the panel
|
||||
262| if (IndexUpdateManager.getState() === "disconnected") {
|
||||
263| IndexUpdateManager.connect();
|
||||
264| }
|
||||
265| panel = document.createElement("div");
|
||||
266| panel.id = "sync-panel";
|
||||
267| panel.className = "sync-panel";
|
||||
268| _renderSyncPanel(panel);
|
||||
269| document.body.appendChild(panel);
|
||||
270|
|
||||
271| // Close on outside click
|
||||
272| setTimeout(() => {
|
||||
273| document.addEventListener("click", _closeSyncPanelOutside, { once: true });
|
||||
274| }, 0);
|
||||
275|}
|
||||
276|
|
||||
277|function _closeSyncPanelOutside(e) {
|
||||
278| const panel = document.getElementById("sync-panel");
|
||||
279| if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
|
||||
280| panel.remove();
|
||||
281| }
|
||||
282|}
|
||||
283|
|
||||
284|function _renderSyncPanel(panel) {
|
||||
285| const state = IndexUpdateManager.getState();
|
||||
286| const events = IndexUpdateManager.getRecentEvents();
|
||||
287|
|
||||
288| const stateLabels = {
|
||||
289| disconnected: "Déconnecté",
|
||||
290| connecting: "Connexion...",
|
||||
291| connected: "Connecté",
|
||||
292| syncing: "Synchronisation...",
|
||||
293| };
|
||||
294|
|
||||
295| let html = `<div class="sync-panel__header">
|
||||
296| <span class="sync-panel__title">Synchronisation</span>
|
||||
297| <span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
|
||||
298| </div>`;
|
||||
299|
|
||||
300| if (events.length === 0) {
|
||||
301| html += `<div class="sync-panel__empty">Aucun événement récent</div>`;
|
||||
302| } else {
|
||||
303| html += `<div class="sync-panel__events">`;
|
||||
304| events.slice(0, 10).forEach((ev) => {
|
||||
305| const time = new Date(ev.timestamp).toLocaleTimeString();
|
||||
306| const typeLabels = {
|
||||
307| index_updated: "Mise à jour",
|
||||
308| index_reloaded: "Rechargement",
|
||||
309| vault_added: "Vault ajouté",
|
||||
310| vault_removed: "Vault supprimé",
|
||||
311| index_start: "Démarrage index.",
|
||||
312| index_progress: "Vault indexé",
|
||||
313| index_complete: "Indexation tech.",
|
||||
314| };
|
||||
315| const label = typeLabels[ev.type] || ev.type;
|
||||
316| let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
|
||||
317| if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
|
||||
318| if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
|
||||
319| if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
|
||||
320| html += `<div class="sync-panel__event">
|
||||
321| <span class="sync-panel__event-type">${label}</span>
|
||||
322| <span class="sync-panel__event-detail">${detail}</span>
|
||||
323| <span class="sync-panel__event-time">${time}</span>
|
||||
324| </div>`;
|
||||
325| });
|
||||
326| html += `</div>`;
|
||||
327| }
|
||||
328|
|
||||
329| panel.innerHTML = html;
|
||||
330|}
|
||||
331|
|
||||
332|// ---------------------------------------------------------------------------
|
||||
333|// PWA Service Worker Registration
|
||||
334|// ---------------------------------------------------------------------------
|
||||
335|function registerServiceWorker() {
|
||||
336| if ("serviceWorker" in navigator) {
|
||||
337| window.addEventListener("load", () => {
|
||||
338| navigator.serviceWorker
|
||||
339| .register("/sw.js")
|
||||
340| .then((registration) => {
|
||||
341| console.log("[PWA] Service Worker registered successfully:", registration.scope);
|
||||
342|
|
||||
343| // Check for updates periodically
|
||||
344| setInterval(() => {
|
||||
345| registration.update();
|
||||
346| }, 60000); // Check every minute
|
||||
347|
|
||||
348| // Handle service worker updates
|
||||
349| registration.addEventListener("updatefound", () => {
|
||||
350| const newWorker = registration.installing;
|
||||
351| newWorker.addEventListener("statechange", () => {
|
||||
352| if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
353| // New service worker available
|
||||
354| showUpdateNotification();
|
||||
355| }
|
||||
356| });
|
||||
357| });
|
||||
358| })
|
||||
359| .catch((error) => {
|
||||
360| console.log("[PWA] Service Worker registration failed:", error);
|
||||
361| });
|
||||
362| });
|
||||
363| }
|
||||
364|}
|
||||
365|
|
||||
366|function showUpdateNotification() {
|
||||
367| const message = document.createElement("div");
|
||||
368| message.className = "pwa-update-notification";
|
||||
369| message.innerHTML = `
|
||||
370| <div class="pwa-update-content">
|
||||
371| <span>Une nouvelle version d'ObsiGate est disponible !</span>
|
||||
372| <button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
|
||||
373| <button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
374| </div>
|
||||
375| `;
|
||||
376| document.body.appendChild(message);
|
||||
377|
|
||||
378| // Auto-dismiss after 30 seconds
|
||||
379| setTimeout(() => {
|
||||
380| if (message.parentElement) {
|
||||
381| message.remove();
|
||||
382| }
|
||||
383| }, 30000);
|
||||
384|}
|
||||
385|
|
||||
386|// Handle install prompt
|
||||
387|let deferredPrompt;
|
||||
388|window.addEventListener("beforeinstallprompt", (e) => {
|
||||
389| e.preventDefault();
|
||||
390| deferredPrompt = e;
|
||||
391|
|
||||
392| // Show install button if desired
|
||||
393| const installBtn = document.getElementById("pwa-install-btn");
|
||||
394| if (installBtn) {
|
||||
395| installBtn.style.display = "block";
|
||||
396| installBtn.addEventListener("click", async () => {
|
||||
397| if (deferredPrompt) {
|
||||
398| deferredPrompt.prompt();
|
||||
399| const { outcome } = await deferredPrompt.userChoice;
|
||||
400| console.log(`[PWA] User response to install prompt: ${outcome}`);
|
||||
401| deferredPrompt = null;
|
||||
402| installBtn.style.display = "none";
|
||||
403| }
|
||||
404| });
|
||||
405| }
|
||||
406|});
|
||||
407|
|
||||
408|// Log when app is installed
|
||||
409|window.addEventListener("appinstalled", () => {
|
||||
410| console.log("[PWA] ObsiGate has been installed");
|
||||
411| showToast("ObsiGate installé avec succès !", "success");
|
||||
412|});
|
||||
413|
|
||||
414|// ── Dashboard tab switching (runs on page load and after rebuild) ──
|
||||
415|function initDashboardTabs() {
|
||||
416| document.querySelectorAll(".dashboard-tab").forEach(tab => {
|
||||
417| // Remove existing listeners by cloning
|
||||
418| const newTab = tab.cloneNode(true);
|
||||
419| tab.parentNode.replaceChild(newTab, tab);
|
||||
420| newTab.addEventListener("click", function() {
|
||||
421| document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
|
||||
422| document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
|
||||
423| this.classList.add("active");
|
||||
424| const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
|
||||
425| if (panel) panel.classList.add("active");
|
||||
426| });
|
||||
427| });
|
||||
428|}
|
||||
429|
|
||||
430|// ---------------------------------------------------------------------------
|
||||
431|// Init — called by app.js orchestrator
|
||||
432|// ---------------------------------------------------------------------------
|
||||
433|export { initSyncStatus };
|
||||
434|export function init() {
|
||||
435| registerServiceWorker();
|
||||
436| initDashboardTabs();
|
||||
437|}
|
||||
438|
|
||||
4247
frontend/js/ui.js
4247
frontend/js/ui.js
File diff suppressed because it is too large
Load Diff
1021
frontend/js/utils.js
1021
frontend/js/utils.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user