fix: strip read_file line numbers accidentally injected into JS files
CI / lint (push) Successful in 13s
CI / security (push) Successful in 8s
CI / test (push) Successful in 16s
CI / build (push) Successful in 3s

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