Files
bruno 2508c37ef2
CI / lint (push) Failing after 19s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Failing after 10s
fix: 422 error on normal refresh — min query length violation
- 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)
2026-06-19 15:16:11 -04:00

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;
}
}