Files
ObsiGate/frontend/js/sync.js
T
bruno 6700b78004
CI / lint (push) Failing after 4s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in 8s
fix: import manquant openFile dans sync.js (ReferenceError)
sync.js appelait openFile() après un index update SSE mais la fonction
n'était pas importée depuis viewer.js. Ajout de l'import manquant.
2026-06-04 07:36:52 -04:00

550 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 = `<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;
// ── AI Status section ──
_renderAIStatusSection(panel);
}
// ---------------------------------------------------------------------------
// AI Status Indicator (inside sync panel + header badge animation)
// ---------------------------------------------------------------------------
let _aiProcessing = false;
let _aiStatusChecked = false;
let _aiConfigured = null; // null = unknown, true/false
/** Check if AI is configured by calling the status endpoint. */
async function _checkAIStatus() {
if (_aiStatusChecked) return;
try {
const resp = await api('/api/ai/status');
_aiConfigured = !!(resp && resp.configured);
} catch (e) {
_aiConfigured = false;
}
_aiStatusChecked = true;
}
function _renderAIStatusSection(panel) {
const section = document.createElement('div');
section.className = 'sync-panel__ai-section';
section.id = 'sync-panel-ai';
let statusIcon, statusText, statusClass;
if (_aiProcessing) {
statusIcon = 'loader';
statusText = 'Traitement en cours…';
statusClass = 'sync-panel__ai--processing';
} else if (_aiConfigured === true) {
statusIcon = 'check-circle';
statusText = 'IA configurée et active';
statusClass = 'sync-panel__ai--active';
} else if (_aiConfigured === false) {
statusIcon = 'x-circle';
statusText = 'IA non configurée';
statusClass = 'sync-panel__ai--inactive';
} else {
statusIcon = 'help-circle';
statusText = 'Statut IA inconnu';
statusClass = 'sync-panel__ai--unknown';
}
section.innerHTML = `
<div class="sync-panel__ai-header">
<span class="sync-panel__ai-title">Intelligence Artificielle</span>
<span class="sync-panel__ai-state ${statusClass}" data-lucide-icon="${statusIcon}">
<i data-lucide="${statusIcon}" style="width:14px;height:14px"></i>
${statusText}
</span>
</div>
`;
panel.appendChild(section);
// Trigger icon refresh via global
if (window._obsigateRefreshIcons) window._obsigateRefreshIcons();
}
/** Update the AI section in the open sync panel. */
function _updateAIStatusInPanel() {
const section = document.getElementById('sync-panel-ai');
if (!section) return;
section.innerHTML = '';
const panel = document.getElementById('sync-panel');
if (panel) {
// Re-render just the AI section
const oldSection = panel.querySelector('.sync-panel__ai-section');
if (oldSection) oldSection.remove();
_renderAIStatusSection(panel);
}
}
/** Flash the sync badge white during AI processing. */
function _setBadgeAIProcessing(active) {
const badge = document.getElementById('sync-badge');
if (!badge) return;
if (active) {
badge.classList.add('sync-badge--ai-processing');
} else {
badge.classList.remove('sync-badge--ai-processing');
}
}
/** Initialize AI status tracking. */
function _initAIStatus() {
// Listen for AI processing events from ai.js
document.addEventListener('ai-processing-start', () => {
_aiProcessing = true;
_setBadgeAIProcessing(true);
_updateAIStatusInPanel();
});
document.addEventListener('ai-processing-end', () => {
_aiProcessing = false;
_setBadgeAIProcessing(false);
_updateAIStatusInPanel();
});
// Check AI status on first sync panel open
const badge = document.getElementById('sync-badge');
if (badge) {
const origClick = badge.onclick;
badge.addEventListener('click', () => {
_checkAIStatus().then(() => _updateAIStatusInPanel());
}, { once: false });
}
}
// ---------------------------------------------------------------------------
// 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, initDashboardTabs };
export function init() {
registerServiceWorker();
initDashboardTabs();
}