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';
|
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|
|
|
||||||
|
|||||||
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",
|
activeSidebarTab: "vaults",
|
||||||
filterDebounce: null,
|
filterDebounce: null,
|
||||||
vaultSettings: {},
|
vaultSettings: {},
|
||||||
};
|
};
|
||||||
@ -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|
|
|
||||||
|
|||||||
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