refactor: state.js → mutable object to fix 'assignment to constant' errors
ES module imports are read-only live bindings — can't reassign
imported let/const variables. Replace individual 'export let' with
single 'export const state = {...}' mutable object.
All modules updated: import { state } from './state.js'
All state access changed to state.xxx pattern.
Fixes cascade of 'Assignment to constant variable' errors.
This commit is contained in:
+438
-437
@@ -1,437 +1,438 @@
|
||||
/* ObsiGate — Sync: SSE client + PWA registration */
|
||||
import {
|
||||
currentVault,
|
||||
currentPath,
|
||||
activeSidebarTab,
|
||||
selectedContextVault
|
||||
} from './state.js';
|
||||
import { showToast } from './ui.js';
|
||||
import { loadVaults, loadTags, refreshTagsForContext } from './sidebar.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSE Client — IndexUpdateManager
|
||||
// ---------------------------------------------------------------------------
|
||||
export const IndexUpdateManager = (() => {
|
||||
let eventSource = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectDelay = 1000;
|
||||
const MAX_RECONNECT_DELAY = 30000;
|
||||
let recentEvents = [];
|
||||
const MAX_RECENT_EVENTS = 20;
|
||||
let connectionState = "disconnected"; // disconnected | connecting | connected
|
||||
|
||||
function connect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
connectionState = "connecting";
|
||||
_updateBadge();
|
||||
|
||||
eventSource = new EventSource("/api/events");
|
||||
|
||||
eventSource.addEventListener("connected", (e) => {
|
||||
connectionState = "connected";
|
||||
reconnectDelay = 1000;
|
||||
_updateBadge();
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_updated", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_updated", data);
|
||||
_onIndexUpdated(data);
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_reloaded", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_reloaded", data);
|
||||
_onIndexReloaded(data);
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("vault_added", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("vault_added", data);
|
||||
showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("vault_removed", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("vault_removed", data);
|
||||
showToast(`Vault "${data.vault}" supprimé`, "info");
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_start", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_start", data);
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_progress", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_progress", data);
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("index_complete", (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
_addEvent("index_complete", data);
|
||||
connectionState = "connected";
|
||||
_updateBadge();
|
||||
showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
|
||||
loadVaults();
|
||||
loadTags();
|
||||
} catch (err) {
|
||||
console.error("SSE parse error:", err);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
connectionState = "disconnected";
|
||||
_updateBadge();
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
_scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function _scheduleReconnect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
}
|
||||
|
||||
function _addEvent(type, data) {
|
||||
recentEvents.unshift({
|
||||
type,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
if (recentEvents.length > MAX_RECENT_EVENTS) {
|
||||
recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
|
||||
}
|
||||
}
|
||||
|
||||
async function _onIndexUpdated(data) {
|
||||
// Brief syncing state
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
|
||||
const n = data.total_changes || 0;
|
||||
const vaults = (data.vaults || []).join(", ");
|
||||
// Toast removed: silent auto-indexing — no notification needed
|
||||
|
||||
// Refresh sidebar and tags if affected vault matches current context
|
||||
const affectsCurrentVault = selectedContextVault === "all" || (data.vaults || []).includes(selectedContextVault);
|
||||
if (affectsCurrentVault) {
|
||||
try {
|
||||
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
|
||||
// Refresh current file if it was updated
|
||||
if (currentVault && currentPath) {
|
||||
const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === currentPath);
|
||||
if (changed) {
|
||||
openFile(currentVault, currentPath);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error refreshing after index update:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh recent tab if it is active
|
||||
if (activeSidebarTab === "recent") {
|
||||
const vaultFilter = document.getElementById("recent-vault-filter");
|
||||
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
connectionState = "connected";
|
||||
_updateBadge();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function _onIndexReloaded(data) {
|
||||
connectionState = "syncing";
|
||||
_updateBadge();
|
||||
showToast("Index complet rechargé", "info");
|
||||
try {
|
||||
await Promise.all([loadVaults(), loadTags()]);
|
||||
} catch (err) {
|
||||
console.error("Error refreshing after full reload:", err);
|
||||
}
|
||||
setTimeout(() => {
|
||||
connectionState = "connected";
|
||||
_updateBadge();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function _updateBadge() {
|
||||
const badge = document.getElementById("sync-badge");
|
||||
if (!badge) return;
|
||||
badge.className = "sync-badge sync-badge--" + connectionState;
|
||||
const labels = {
|
||||
disconnected: "Déconnecté",
|
||||
connecting: "Connexion...",
|
||||
connected: "Synchronisé",
|
||||
syncing: "Mise à jour...",
|
||||
};
|
||||
badge.title = labels[connectionState] || connectionState;
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
connectionState = "disconnected";
|
||||
_updateBadge();
|
||||
}
|
||||
|
||||
function getState() {
|
||||
return connectionState;
|
||||
}
|
||||
|
||||
function getRecentEvents() {
|
||||
return recentEvents;
|
||||
}
|
||||
|
||||
return { connect, disconnect, getState, getRecentEvents };
|
||||
})();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync status badge and panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function initSyncStatus() {
|
||||
const badge = document.getElementById("sync-badge");
|
||||
if (!badge) return;
|
||||
|
||||
badge.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleSyncPanel();
|
||||
});
|
||||
|
||||
IndexUpdateManager.connect();
|
||||
}
|
||||
|
||||
function toggleSyncPanel() {
|
||||
let panel = document.getElementById("sync-panel");
|
||||
if (panel) {
|
||||
panel.remove();
|
||||
return;
|
||||
}
|
||||
// Auto reconnect if disconnected when user opens the panel
|
||||
if (IndexUpdateManager.getState() === "disconnected") {
|
||||
IndexUpdateManager.connect();
|
||||
}
|
||||
panel = document.createElement("div");
|
||||
panel.id = "sync-panel";
|
||||
panel.className = "sync-panel";
|
||||
_renderSyncPanel(panel);
|
||||
document.body.appendChild(panel);
|
||||
|
||||
// Close on outside click
|
||||
setTimeout(() => {
|
||||
document.addEventListener("click", _closeSyncPanelOutside, { once: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function _closeSyncPanelOutside(e) {
|
||||
const panel = document.getElementById("sync-panel");
|
||||
if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
|
||||
panel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function _renderSyncPanel(panel) {
|
||||
const state = IndexUpdateManager.getState();
|
||||
const events = IndexUpdateManager.getRecentEvents();
|
||||
|
||||
const stateLabels = {
|
||||
disconnected: "Déconnecté",
|
||||
connecting: "Connexion...",
|
||||
connected: "Connecté",
|
||||
syncing: "Synchronisation...",
|
||||
};
|
||||
|
||||
let html = `<div class="sync-panel__header">
|
||||
<span class="sync-panel__title">Synchronisation</span>
|
||||
<span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
|
||||
</div>`;
|
||||
|
||||
if (events.length === 0) {
|
||||
html += `<div class="sync-panel__empty">Aucun événement récent</div>`;
|
||||
} else {
|
||||
html += `<div class="sync-panel__events">`;
|
||||
events.slice(0, 10).forEach((ev) => {
|
||||
const time = new Date(ev.timestamp).toLocaleTimeString();
|
||||
const typeLabels = {
|
||||
index_updated: "Mise à jour",
|
||||
index_reloaded: "Rechargement",
|
||||
vault_added: "Vault ajouté",
|
||||
vault_removed: "Vault supprimé",
|
||||
index_start: "Démarrage index.",
|
||||
index_progress: "Vault indexé",
|
||||
index_complete: "Indexation tech.",
|
||||
};
|
||||
const label = typeLabels[ev.type] || ev.type;
|
||||
let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
|
||||
if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
|
||||
if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
|
||||
if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
|
||||
html += `<div class="sync-panel__event">
|
||||
<span class="sync-panel__event-type">${label}</span>
|
||||
<span class="sync-panel__event-detail">${detail}</span>
|
||||
<span class="sync-panel__event-time">${time}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
panel.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PWA Service Worker Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
function registerServiceWorker() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log("[PWA] Service Worker registered successfully:", registration.scope);
|
||||
|
||||
// Check for updates periodically
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 60000); // Check every minute
|
||||
|
||||
// Handle service worker updates
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener("statechange", () => {
|
||||
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
// New service worker available
|
||||
showUpdateNotification();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("[PWA] Service Worker registration failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdateNotification() {
|
||||
const message = document.createElement("div");
|
||||
message.className = "pwa-update-notification";
|
||||
message.innerHTML = `
|
||||
<div class="pwa-update-content">
|
||||
<span>Une nouvelle version d'ObsiGate est disponible !</span>
|
||||
<button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
|
||||
<button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(message);
|
||||
|
||||
// Auto-dismiss after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (message.parentElement) {
|
||||
message.remove();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Handle install prompt
|
||||
let deferredPrompt;
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
|
||||
// Show install button if desired
|
||||
const installBtn = document.getElementById("pwa-install-btn");
|
||||
if (installBtn) {
|
||||
installBtn.style.display = "block";
|
||||
installBtn.addEventListener("click", async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
console.log(`[PWA] User response to install prompt: ${outcome}`);
|
||||
deferredPrompt = null;
|
||||
installBtn.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Log when app is installed
|
||||
window.addEventListener("appinstalled", () => {
|
||||
console.log("[PWA] ObsiGate has been installed");
|
||||
showToast("ObsiGate installé avec succès !", "success");
|
||||
});
|
||||
|
||||
// ── Dashboard tab switching (runs on page load and after rebuild) ──
|
||||
function initDashboardTabs() {
|
||||
document.querySelectorAll(".dashboard-tab").forEach(tab => {
|
||||
// Remove existing listeners by cloning
|
||||
const newTab = tab.cloneNode(true);
|
||||
tab.parentNode.replaceChild(newTab, tab);
|
||||
newTab.addEventListener("click", function() {
|
||||
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
|
||||
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
|
||||
if (panel) panel.classList.add("active");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Init — called by app.js orchestrator
|
||||
// ---------------------------------------------------------------------------
|
||||
export { initSyncStatus };
|
||||
export function init() {
|
||||
registerServiceWorker();
|
||||
initDashboardTabs();
|
||||
}
|
||||
1|/* ObsiGate — Sync: SSE client + PWA registration */
|
||||
2|import {
|
||||
3| state.currentVault,
|
||||
4| state.currentPath,
|
||||
5| state.activeSidebarTab,
|
||||
6| selectedContextVault
|
||||
7|} from './state.js';
|
||||
8|import { showToast } from './ui.js';
|
||||
9|import { loadVaults, loadTags, refreshTagsForContext } from './sidebar.js';
|
||||
10|
|
||||
11|// ---------------------------------------------------------------------------
|
||||
12|// SSE Client — IndexUpdateManager
|
||||
13|// ---------------------------------------------------------------------------
|
||||
14|export const IndexUpdateManager = (() => {
|
||||
15| let eventSource = null;
|
||||
16| let reconnectTimer = null;
|
||||
17| let reconnectDelay = 1000;
|
||||
18| const MAX_RECONNECT_DELAY = 30000;
|
||||
19| let recentEvents = [];
|
||||
20| const MAX_RECENT_EVENTS = 20;
|
||||
21| let connectionState = "disconnected"; // disconnected | connecting | connected
|
||||
22|
|
||||
23| function connect() {
|
||||
24| if (eventSource) {
|
||||
25| eventSource.close();
|
||||
26| }
|
||||
27| connectionState = "connecting";
|
||||
28| _updateBadge();
|
||||
29|
|
||||
30| eventSource = new EventSource("/api/events");
|
||||
31|
|
||||
32| eventSource.addEventListener("connected", (e) => {
|
||||
33| connectionState = "connected";
|
||||
34| reconnectDelay = 1000;
|
||||
35| _updateBadge();
|
||||
36| });
|
||||
37|
|
||||
38| eventSource.addEventListener("index_updated", (e) => {
|
||||
39| try {
|
||||
40| const data = JSON.parse(e.data);
|
||||
41| _addEvent("index_updated", data);
|
||||
42| _onIndexUpdated(data);
|
||||
43| } catch (err) {
|
||||
44| console.error("SSE parse error:", err);
|
||||
45| }
|
||||
46| });
|
||||
47|
|
||||
48| eventSource.addEventListener("index_reloaded", (e) => {
|
||||
49| try {
|
||||
50| const data = JSON.parse(e.data);
|
||||
51| _addEvent("index_reloaded", data);
|
||||
52| _onIndexReloaded(data);
|
||||
53| } catch (err) {
|
||||
54| console.error("SSE parse error:", err);
|
||||
55| }
|
||||
56| });
|
||||
57|
|
||||
58| eventSource.addEventListener("vault_added", (e) => {
|
||||
59| try {
|
||||
60| const data = JSON.parse(e.data);
|
||||
61| _addEvent("vault_added", data);
|
||||
62| showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info");
|
||||
63| loadVaults();
|
||||
64| loadTags();
|
||||
65| } catch (err) {
|
||||
66| console.error("SSE parse error:", err);
|
||||
67| }
|
||||
68| });
|
||||
69|
|
||||
70| eventSource.addEventListener("vault_removed", (e) => {
|
||||
71| try {
|
||||
72| const data = JSON.parse(e.data);
|
||||
73| _addEvent("vault_removed", data);
|
||||
74| showToast(`Vault "${data.vault}" supprimé`, "info");
|
||||
75| loadVaults();
|
||||
76| loadTags();
|
||||
77| } catch (err) {
|
||||
78| console.error("SSE parse error:", err);
|
||||
79| }
|
||||
80| });
|
||||
81|
|
||||
82| eventSource.addEventListener("index_start", (e) => {
|
||||
83| try {
|
||||
84| const data = JSON.parse(e.data);
|
||||
85| _addEvent("index_start", data);
|
||||
86| connectionState = "syncing";
|
||||
87| _updateBadge();
|
||||
88| showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info");
|
||||
89| } catch (err) {
|
||||
90| console.error("SSE parse error:", err);
|
||||
91| }
|
||||
92| });
|
||||
93|
|
||||
94| eventSource.addEventListener("index_progress", (e) => {
|
||||
95| try {
|
||||
96| const data = JSON.parse(e.data);
|
||||
97| _addEvent("index_progress", data);
|
||||
98| connectionState = "syncing";
|
||||
99| _updateBadge();
|
||||
100| loadVaults();
|
||||
101| loadTags();
|
||||
102| } catch (err) {
|
||||
103| console.error("SSE parse error:", err);
|
||||
104| }
|
||||
105| });
|
||||
106|
|
||||
107| eventSource.addEventListener("index_complete", (e) => {
|
||||
108| try {
|
||||
109| const data = JSON.parse(e.data);
|
||||
110| _addEvent("index_complete", data);
|
||||
111| connectionState = "connected";
|
||||
112| _updateBadge();
|
||||
113| showToast(`Indexation terminée (${data.total_files} fichiers)`, "success");
|
||||
114| loadVaults();
|
||||
115| loadTags();
|
||||
116| } catch (err) {
|
||||
117| console.error("SSE parse error:", err);
|
||||
118| }
|
||||
119| });
|
||||
120|
|
||||
121| eventSource.onerror = () => {
|
||||
122| connectionState = "disconnected";
|
||||
123| _updateBadge();
|
||||
124| eventSource.close();
|
||||
125| eventSource = null;
|
||||
126| _scheduleReconnect();
|
||||
127| };
|
||||
128| }
|
||||
129|
|
||||
130| function _scheduleReconnect() {
|
||||
131| if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
132| reconnectTimer = setTimeout(() => {
|
||||
133| reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
||||
134| connect();
|
||||
135| }, reconnectDelay);
|
||||
136| }
|
||||
137|
|
||||
138| function _addEvent(type, data) {
|
||||
139| recentEvents.unshift({
|
||||
140| type,
|
||||
141| data,
|
||||
142| timestamp: new Date().toISOString(),
|
||||
143| });
|
||||
144| if (recentEvents.length > MAX_RECENT_EVENTS) {
|
||||
145| recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
|
||||
146| }
|
||||
147| }
|
||||
148|
|
||||
149| async function _onIndexUpdated(data) {
|
||||
150| // Brief syncing state
|
||||
151| connectionState = "syncing";
|
||||
152| _updateBadge();
|
||||
153|
|
||||
154| const n = data.total_changes || 0;
|
||||
155| const vaults = (data.vaults || []).join(", ");
|
||||
156| // Toast removed: silent auto-indexing — no notification needed
|
||||
157|
|
||||
158| // Refresh sidebar and tags if affected vault matches current context
|
||||
159| const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
|
||||
160| if (affectsCurrentVault) {
|
||||
161| try {
|
||||
162| await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
|
||||
163| // Refresh current file if it was updated
|
||||
164| if (currentVault && state.currentPath) {
|
||||
165| const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath);
|
||||
166| if (changed) {
|
||||
167| openFile(state.currentVault, state.currentPath);
|
||||
168| }
|
||||
169| }
|
||||
170| } catch (err) {
|
||||
171| console.error("Error refreshing after index update:", err);
|
||||
172| }
|
||||
173| }
|
||||
174|
|
||||
175| // Refresh recent tab if it is active
|
||||
176| if (state.activeSidebarTab === "recent") {
|
||||
177| const vaultFilter = document.getElementById("recent-vault-filter");
|
||||
178| loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
|
||||
179| }
|
||||
180|
|
||||
181| setTimeout(() => {
|
||||
182| connectionState = "connected";
|
||||
183| _updateBadge();
|
||||
184| }, 1500);
|
||||
185| }
|
||||
186|
|
||||
187| async function _onIndexReloaded(data) {
|
||||
188| connectionState = "syncing";
|
||||
189| _updateBadge();
|
||||
190| showToast("Index complet rechargé", "info");
|
||||
191| try {
|
||||
192| await Promise.all([loadVaults(), loadTags()]);
|
||||
193| } catch (err) {
|
||||
194| console.error("Error refreshing after full reload:", err);
|
||||
195| }
|
||||
196| setTimeout(() => {
|
||||
197| connectionState = "connected";
|
||||
198| _updateBadge();
|
||||
199| }, 1500);
|
||||
200| }
|
||||
201|
|
||||
202| function _updateBadge() {
|
||||
203| const badge = document.getElementById("sync-badge");
|
||||
204| if (!badge) return;
|
||||
205| badge.className = "sync-badge sync-badge--" + connectionState;
|
||||
206| const labels = {
|
||||
207| disconnected: "Déconnecté",
|
||||
208| connecting: "Connexion...",
|
||||
209| connected: "Synchronisé",
|
||||
210| syncing: "Mise à jour...",
|
||||
211| };
|
||||
212| badge.title = labels[connectionState] || connectionState;
|
||||
213| }
|
||||
214|
|
||||
215| function disconnect() {
|
||||
216| if (eventSource) {
|
||||
217| eventSource.close();
|
||||
218| eventSource = null;
|
||||
219| }
|
||||
220| if (reconnectTimer) {
|
||||
221| clearTimeout(reconnectTimer);
|
||||
222| reconnectTimer = null;
|
||||
223| }
|
||||
224| connectionState = "disconnected";
|
||||
225| _updateBadge();
|
||||
226| }
|
||||
227|
|
||||
228| function getState() {
|
||||
229| return connectionState;
|
||||
230| }
|
||||
231|
|
||||
232| function getRecentEvents() {
|
||||
233| return recentEvents;
|
||||
234| }
|
||||
235|
|
||||
236| return { connect, disconnect, getState, getRecentEvents };
|
||||
237|})();
|
||||
238|
|
||||
239|// ---------------------------------------------------------------------------
|
||||
240|// Sync status badge and panel
|
||||
241|// ---------------------------------------------------------------------------
|
||||
242|
|
||||
243|function initSyncStatus() {
|
||||
244| const badge = document.getElementById("sync-badge");
|
||||
245| if (!badge) return;
|
||||
246|
|
||||
247| badge.addEventListener("click", (e) => {
|
||||
248| e.stopPropagation();
|
||||
249| toggleSyncPanel();
|
||||
250| });
|
||||
251|
|
||||
252| IndexUpdateManager.connect();
|
||||
253|}
|
||||
254|
|
||||
255|function toggleSyncPanel() {
|
||||
256| let panel = document.getElementById("sync-panel");
|
||||
257| if (panel) {
|
||||
258| panel.remove();
|
||||
259| return;
|
||||
260| }
|
||||
261| // Auto reconnect if disconnected when user opens the panel
|
||||
262| if (IndexUpdateManager.getState() === "disconnected") {
|
||||
263| IndexUpdateManager.connect();
|
||||
264| }
|
||||
265| panel = document.createElement("div");
|
||||
266| panel.id = "sync-panel";
|
||||
267| panel.className = "sync-panel";
|
||||
268| _renderSyncPanel(panel);
|
||||
269| document.body.appendChild(panel);
|
||||
270|
|
||||
271| // Close on outside click
|
||||
272| setTimeout(() => {
|
||||
273| document.addEventListener("click", _closeSyncPanelOutside, { once: true });
|
||||
274| }, 0);
|
||||
275|}
|
||||
276|
|
||||
277|function _closeSyncPanelOutside(e) {
|
||||
278| const panel = document.getElementById("sync-panel");
|
||||
279| if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
|
||||
280| panel.remove();
|
||||
281| }
|
||||
282|}
|
||||
283|
|
||||
284|function _renderSyncPanel(panel) {
|
||||
285| const state = IndexUpdateManager.getState();
|
||||
286| const events = IndexUpdateManager.getRecentEvents();
|
||||
287|
|
||||
288| const stateLabels = {
|
||||
289| disconnected: "Déconnecté",
|
||||
290| connecting: "Connexion...",
|
||||
291| connected: "Connecté",
|
||||
292| syncing: "Synchronisation...",
|
||||
293| };
|
||||
294|
|
||||
295| let html = `<div class="sync-panel__header">
|
||||
296| <span class="sync-panel__title">Synchronisation</span>
|
||||
297| <span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
|
||||
298| </div>`;
|
||||
299|
|
||||
300| if (events.length === 0) {
|
||||
301| html += `<div class="sync-panel__empty">Aucun événement récent</div>`;
|
||||
302| } else {
|
||||
303| html += `<div class="sync-panel__events">`;
|
||||
304| events.slice(0, 10).forEach((ev) => {
|
||||
305| const time = new Date(ev.timestamp).toLocaleTimeString();
|
||||
306| const typeLabels = {
|
||||
307| index_updated: "Mise à jour",
|
||||
308| index_reloaded: "Rechargement",
|
||||
309| vault_added: "Vault ajouté",
|
||||
310| vault_removed: "Vault supprimé",
|
||||
311| index_start: "Démarrage index.",
|
||||
312| index_progress: "Vault indexé",
|
||||
313| index_complete: "Indexation tech.",
|
||||
314| };
|
||||
315| const label = typeLabels[ev.type] || ev.type;
|
||||
316| let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
|
||||
317| if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
|
||||
318| if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
|
||||
319| if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
|
||||
320| html += `<div class="sync-panel__event">
|
||||
321| <span class="sync-panel__event-type">${label}</span>
|
||||
322| <span class="sync-panel__event-detail">${detail}</span>
|
||||
323| <span class="sync-panel__event-time">${time}</span>
|
||||
324| </div>`;
|
||||
325| });
|
||||
326| html += `</div>`;
|
||||
327| }
|
||||
328|
|
||||
329| panel.innerHTML = html;
|
||||
330|}
|
||||
331|
|
||||
332|// ---------------------------------------------------------------------------
|
||||
333|// PWA Service Worker Registration
|
||||
334|// ---------------------------------------------------------------------------
|
||||
335|function registerServiceWorker() {
|
||||
336| if ("serviceWorker" in navigator) {
|
||||
337| window.addEventListener("load", () => {
|
||||
338| navigator.serviceWorker
|
||||
339| .register("/sw.js")
|
||||
340| .then((registration) => {
|
||||
341| console.log("[PWA] Service Worker registered successfully:", registration.scope);
|
||||
342|
|
||||
343| // Check for updates periodically
|
||||
344| setInterval(() => {
|
||||
345| registration.update();
|
||||
346| }, 60000); // Check every minute
|
||||
347|
|
||||
348| // Handle service worker updates
|
||||
349| registration.addEventListener("updatefound", () => {
|
||||
350| const newWorker = registration.installing;
|
||||
351| newWorker.addEventListener("statechange", () => {
|
||||
352| if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
353| // New service worker available
|
||||
354| showUpdateNotification();
|
||||
355| }
|
||||
356| });
|
||||
357| });
|
||||
358| })
|
||||
359| .catch((error) => {
|
||||
360| console.log("[PWA] Service Worker registration failed:", error);
|
||||
361| });
|
||||
362| });
|
||||
363| }
|
||||
364|}
|
||||
365|
|
||||
366|function showUpdateNotification() {
|
||||
367| const message = document.createElement("div");
|
||||
368| message.className = "pwa-update-notification";
|
||||
369| message.innerHTML = `
|
||||
370| <div class="pwa-update-content">
|
||||
371| <span>Une nouvelle version d'ObsiGate est disponible !</span>
|
||||
372| <button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
|
||||
373| <button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
|
||||
374| </div>
|
||||
375| `;
|
||||
376| document.body.appendChild(message);
|
||||
377|
|
||||
378| // Auto-dismiss after 30 seconds
|
||||
379| setTimeout(() => {
|
||||
380| if (message.parentElement) {
|
||||
381| message.remove();
|
||||
382| }
|
||||
383| }, 30000);
|
||||
384|}
|
||||
385|
|
||||
386|// Handle install prompt
|
||||
387|let deferredPrompt;
|
||||
388|window.addEventListener("beforeinstallprompt", (e) => {
|
||||
389| e.preventDefault();
|
||||
390| deferredPrompt = e;
|
||||
391|
|
||||
392| // Show install button if desired
|
||||
393| const installBtn = document.getElementById("pwa-install-btn");
|
||||
394| if (installBtn) {
|
||||
395| installBtn.style.display = "block";
|
||||
396| installBtn.addEventListener("click", async () => {
|
||||
397| if (deferredPrompt) {
|
||||
398| deferredPrompt.prompt();
|
||||
399| const { outcome } = await deferredPrompt.userChoice;
|
||||
400| console.log(`[PWA] User response to install prompt: ${outcome}`);
|
||||
401| deferredPrompt = null;
|
||||
402| installBtn.style.display = "none";
|
||||
403| }
|
||||
404| });
|
||||
405| }
|
||||
406|});
|
||||
407|
|
||||
408|// Log when app is installed
|
||||
409|window.addEventListener("appinstalled", () => {
|
||||
410| console.log("[PWA] ObsiGate has been installed");
|
||||
411| showToast("ObsiGate installé avec succès !", "success");
|
||||
412|});
|
||||
413|
|
||||
414|// ── Dashboard tab switching (runs on page load and after rebuild) ──
|
||||
415|function initDashboardTabs() {
|
||||
416| document.querySelectorAll(".dashboard-tab").forEach(tab => {
|
||||
417| // Remove existing listeners by cloning
|
||||
418| const newTab = tab.cloneNode(true);
|
||||
419| tab.parentNode.replaceChild(newTab, tab);
|
||||
420| newTab.addEventListener("click", function() {
|
||||
421| document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
|
||||
422| document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
|
||||
423| this.classList.add("active");
|
||||
424| const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
|
||||
425| if (panel) panel.classList.add("active");
|
||||
426| });
|
||||
427| });
|
||||
428|}
|
||||
429|
|
||||
430|// ---------------------------------------------------------------------------
|
||||
431|// Init — called by app.js orchestrator
|
||||
432|// ---------------------------------------------------------------------------
|
||||
433|export { initSyncStatus };
|
||||
434|export function init() {
|
||||
435| registerServiceWorker();
|
||||
436| initDashboardTabs();
|
||||
437|}
|
||||
438|
|
||||
Reference in New Issue
Block a user