fix: strip read_file line numbers accidentally injected into JS files
This commit is contained in:
parent
7866f93778
commit
643a73e0f5
1093
frontend/js/auth.js
1093
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,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';
|
||||
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|
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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(state.allVaults)) {
|
||||
state.allVaults.forEach((v) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = v.name;
|
||||
opt.textContent = v.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
syncVaultSelectors();
|
||||
},
|
||||
|
||||
init() {
|
||||
const select = document.getElementById("dashboard-vault-filter");
|
||||
if (select) {
|
||||
select.addEventListener("change", async () => {
|
||||
await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
|
||||
});
|
||||
}
|
||||
|
||||
this.populateVaultFilter();
|
||||
},
|
||||
};
|
||||
|
||||
// ── Dashboard Bookmarks Widget ──
|
||||
// (moved from app.js 3810-3870; logically belongs with dashboard widgets)
|
||||
const DashboardBookmarkWidget = {
|
||||
_cache: [],
|
||||
_currentFilter: "",
|
||||
|
||||
async load(vaultFilter = "") {
|
||||
const v = vaultFilter || 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 };
|
||||
|
||||
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
@ -56,4 +56,4 @@ export const state = {
|
||||
activeSidebarTab: "vaults",
|
||||
filterDebounce: null,
|
||||
vaultSettings: {},
|
||||
};
|
||||
};
|
||||
@ -1,438 +1,437 @@
|
||||
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|
|
||||
1|/* ObsiGate — Sync: SSE client + PWA registration */
|
||||
import {
|
||||
state.currentVault,
|
||||
state.currentPath,
|
||||
state.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 = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
|
||||
if (affectsCurrentVault) {
|
||||
try {
|
||||
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
|
||||
// Refresh current file if it was updated
|
||||
if (currentVault && state.currentPath) {
|
||||
const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath);
|
||||
if (changed) {
|
||||
openFile(state.currentVault, state.currentPath);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error refreshing after index update:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh recent tab if it is active
|
||||
if (state.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();
|
||||
}
|
||||
|
||||
3296
frontend/js/ui.js
3296
frontend/js/ui.js
File diff suppressed because it is too large
Load Diff
1019
frontend/js/utils.js
1019
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