- Changed q=* to q=** in autocomplete.js and offline.js - Backend requires min_query_length=2, single * was 1 char → 422 - Caused blank page on F5 refresh (non-hard-refresh)
210 lines
6.2 KiB
JavaScript
210 lines
6.2 KiB
JavaScript
/* 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',
|
|
'<i data-lucide="wifi-off" style="width:14px;height:14px"></i>');
|
|
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;
|
|
}
|
|
}
|