/* ObsiGate — Offline Integration Module * Ties together: IndexedDB, offline search, sync queue, UI indicator */ import { OfflineDB, NetworkStatus } from './offline-db.js'; import { state } from './state.js'; import { showToast } from './ui.js'; /* ── Init ── */ export async function initOffline() { NetworkStatus.init(); // Open DB early try { await OfflineDB.open(); console.log('[Offline] IndexedDB ready'); } catch (e) { console.warn('[Offline] IndexedDB not available:', e); } NetworkStatus.onChange((online) => { updateOfflineIndicator(); if (online) { syncPendingChanges(); showToast(t('offline.reconnected'), 'info'); } else { showToast(t('offline.mode_offline'), 'warning'); } }); updateOfflineIndicator(); } /* ── UI Indicator ── */ function updateOfflineIndicator() { const badge = document.getElementById('offline-badge'); if (!badge) return; if (!NetworkStatus.isOnline) { // Build or show indicator badge.innerHTML = replaceLucideIcon(badge, 'wifi-off', ''); badge.title = 'Mode hors-ligne'; badge.style.display = 'flex'; badge.className = 'header-icon offline-indicator offline'; // Add pending count if any OfflineDB.getPendingCount().then(count => { if (count > 0) { const span = badge.querySelector('.offline-pending') || document.createElement('span'); span.className = 'offline-pending'; span.textContent = String(count); if (!badge.contains(span)) badge.appendChild(span); } }); } else { badge.style.display = 'none'; badge.className = 'header-icon offline-indicator'; } } function replaceLucideIcon(container, iconName, html) { const existing = container.querySelector('i[data-lucide]'); if (existing && existing.getAttribute('data-lucide') === iconName) return container.innerHTML; return html; } /* ── Sync File Index from Server ── */ export async function syncFileIndexFromServer() { if (!NetworkStatus.isOnline) { console.log('[Offline] Skipping index sync — offline'); return; } try { // Use a wildcard query to get recent files and populate offline index const resp = await fetch('/api/search/advanced?q=**&vault=all&limit=200'); if (!resp.ok) { console.warn('[Offline] Index sync failed — API returned', resp.status); return; } const data = await resp.json(); const files = (data.results || []).map(r => ({ vault: r.vault, path: r.path, title: r.title, tags: r.tags || [], snippet: r.snippet || '', modified: r.modified || '', tokens: [] })); if (files.length > 0) { await OfflineDB.syncFileIndex(files); console.log('[Offline] Index synced:', files.length, 'files'); } } catch (e) { console.warn('[Offline] Index sync failed:', e.message); } } /* ── Cache File When Viewed ── */ export async function cacheViewedFile(vault, path, content, modified) { if (!vault || !path) return; try { await OfflineDB.putFileContent(vault, path, content, modified); await OfflineDB.pruneContentCache(50); } catch (e) { console.warn('[Offline] Cache file failed:', e); } } /* ── Offline Search ── */ export async function searchOffline(query, vaultFilter) { try { return await OfflineDB.searchFileIndex(query, vaultFilter || state.currentVault); } catch (e) { console.error('[Offline] Search failed:', e); return []; } } /* ── Get Cached File ── */ export async function getCachedFile(vault, path) { try { return await OfflineDB.getFileContent(vault, path); } catch (e) { return null; } } /* ── Pending Changes Queue ── */ let _syncing = false; export async function queueOfflineSave(vault, path, content) { try { const id = await OfflineDB.addPending({ vault, path, content, action: 'save' }); updateOfflineIndicator(); console.log('[Offline] Queued save:', vault, path, 'id=', id); return id; } catch (e) { console.error('[Offline] Queue save failed:', e); return null; } } export async function syncPendingChanges() { if (!NetworkStatus.isOnline || _syncing) return; _syncing = true; try { const pending = await OfflineDB.getAllPending(); if (pending.length === 0) { _syncing = false; return; } console.log('[Offline] Syncing', pending.length, 'pending changes...'); for (const entry of pending) { try { const resp = await fetch('/api/file/' + encodeURIComponent(entry.vault) + '/write', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: entry.path, content: entry.content }) }); if (resp.ok) { await OfflineDB.removePendingById(entry.id); console.log('[Offline] Synced:', entry.vault, entry.path); } else if (resp.status === 409) { // Conflict — keep in queue for manual resolution console.warn('[Offline] Conflict on:', entry.vault, entry.path); } else { console.error('[Offline] Sync failed for:', entry.vault, entry.path, resp.status); } } catch (e) { console.error('[Offline] Sync error for:', entry.vault, entry.path, e); break; // Stop on network error } } const remaining = await OfflineDB.getPendingCount(); console.log('[Offline] Sync complete. Remaining:', remaining); updateOfflineIndicator(); if (remaining === 0) { showToast(t('offline.all_synced'), 'success'); } else { showToast(remaining + ' modification(s) en attente de resolution', 'warning'); } } finally { _syncing = false; } } /* ── Conflict Detection ── */ export async function checkConflict(vault, path, localModified) { if (!NetworkStatus.isOnline) return null; try { const resp = await fetch('/api/file/' + encodeURIComponent(vault) + '/info?path=' + encodeURIComponent(path)); if (!resp.ok) return null; const data = await resp.json(); if (data.modified && data.modified !== localModified) { return { serverModified: data.modified, localModified }; } return null; } catch { return null; } }