refactor: state.js → mutable object to fix 'assignment to constant' errors
CI / lint (push) Successful in 13s
CI / security (push) Successful in 8s
CI / test (push) Successful in 16s
CI / build (push) Successful in 6s

ES module imports are read-only live bindings — can't reassign
imported let/const variables. Replace individual 'export let' with
single 'export const state = {...}' mutable object.

All modules updated: import { state } from './state.js'
All state access changed to state.xxx pattern.

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