/* ObsiGate — Sync: SSE client + PWA registration */ import { state } from './state.js'; import { showToast } from './ui.js'; import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js'; import { loadRecentFiles } from './config.js'; import { openFile } from './viewer.js'; // --------------------------------------------------------------------------- // SSE Client — IndexUpdateManager // --------------------------------------------------------------------------- export const IndexUpdateManager = (() => { let eventSource = null; let reconnectTimer = null; let reconnectDelay = 1000; const MAX_RECONNECT_DELAY = 30000; let recentEvents = []; const MAX_RECENT_EVENTS = 20; let connectionState = "disconnected"; // disconnected | connecting | connected function connect() { if (eventSource) { eventSource.close(); } connectionState = "connecting"; _updateBadge(); eventSource = new EventSource("/api/events"); eventSource.addEventListener("connected", (e) => { connectionState = "connected"; reconnectDelay = 1000; _updateBadge(); }); eventSource.addEventListener("index_updated", (e) => { try { const data = JSON.parse(e.data); _addEvent("index_updated", data); _onIndexUpdated(data); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.addEventListener("index_reloaded", (e) => { try { const data = JSON.parse(e.data); _addEvent("index_reloaded", data); _onIndexReloaded(data); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.addEventListener("vault_added", (e) => { try { const data = JSON.parse(e.data); _addEvent("vault_added", data); showToast(`Vault "${data.vault}" ajouté (${data.stats.file_count} fichiers)`, "info"); loadVaults(); loadTags(); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.addEventListener("vault_removed", (e) => { try { const data = JSON.parse(e.data); _addEvent("vault_removed", data); showToast(`Vault "${data.vault}" supprimé`, "info"); loadVaults(); loadTags(); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.addEventListener("index_start", (e) => { try { const data = JSON.parse(e.data); _addEvent("index_start", data); connectionState = "syncing"; _updateBadge(); showToast(`Indexation démarrée (${data.total_vaults} vaults)`, "info"); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.addEventListener("index_progress", (e) => { try { const data = JSON.parse(e.data); _addEvent("index_progress", data); connectionState = "syncing"; _updateBadge(); loadVaults(); loadTags(); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.addEventListener("index_complete", (e) => { try { const data = JSON.parse(e.data); _addEvent("index_complete", data); connectionState = "connected"; _updateBadge(); showToast(`Indexation terminée (${data.total_files} fichiers)`, "success"); loadVaults(); loadTags(); } catch (err) { console.error("SSE parse error:", err); } }); eventSource.onerror = () => { connectionState = "disconnected"; _updateBadge(); eventSource.close(); eventSource = null; _scheduleReconnect(); }; } function _scheduleReconnect() { if (reconnectTimer) clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); connect(); }, reconnectDelay); } function _addEvent(type, data) { recentEvents.unshift({ type, data, timestamp: new Date().toISOString(), }); if (recentEvents.length > MAX_RECENT_EVENTS) { recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS); } } async function _onIndexUpdated(data) { // Brief syncing state connectionState = "syncing"; _updateBadge(); const n = data.total_changes || 0; const vaults = (data.vaults || []).join(", "); // Toast removed: silent auto-indexing — no notification needed // Refresh sidebar and tags if affected vault matches current context const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault); if (affectsCurrentVault) { try { await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]); // Refresh current file if it was updated if (state.currentVault && state.currentPath) { const changed = (data.changes || []).some((c) => c.vault === state.currentVault && c.path === state.currentPath); if (changed) { openFile(state.currentVault, state.currentPath); } } } catch (err) { console.error("Error refreshing after index update:", err); } } // Refresh recent tab if it is active if (state.activeSidebarTab === "recent") { const vaultFilter = document.getElementById("recent-vault-filter"); loadRecentFiles(vaultFilter ? vaultFilter.value || null : null); } setTimeout(() => { connectionState = "connected"; _updateBadge(); }, 1500); } async function _onIndexReloaded(data) { connectionState = "syncing"; _updateBadge(); showToast("Index complet rechargé", "info"); try { await Promise.all([loadVaults(), loadTags()]); } catch (err) { console.error("Error refreshing after full reload:", err); } setTimeout(() => { connectionState = "connected"; _updateBadge(); }, 1500); } function _updateBadge() { const badge = document.getElementById("sync-badge"); if (!badge) return; badge.className = "sync-badge sync-badge--" + connectionState; const labels = { disconnected: "Déconnecté", connecting: "Connexion...", connected: "Synchronisé", syncing: "Mise à jour...", }; badge.title = labels[connectionState] || connectionState; } function disconnect() { if (eventSource) { eventSource.close(); eventSource = null; } if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } connectionState = "disconnected"; _updateBadge(); } function getState() { return connectionState; } function getRecentEvents() { return recentEvents; } return { connect, disconnect, getState, getRecentEvents }; })(); // --------------------------------------------------------------------------- // Sync status badge and panel // --------------------------------------------------------------------------- function initSyncStatus() { const badge = document.getElementById("sync-badge"); if (!badge) return; badge.addEventListener("click", (e) => { e.stopPropagation(); toggleSyncPanel(); }); IndexUpdateManager.connect(); // Initialize AI status tracking _initAIStatus(); } 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 = `