Files
ObsiGate/frontend/sw.js
T
bruno ac16fc1f0e
CI / lint (push) Successful in 52s
CI / security (push) Successful in 36s
CI / test (push) Successful in 1m6s
CI / build (push) Successful in 1m15s
CI / e2e (push) Failing after 12m40s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
feat: #78 Excalidraw finitions + #67 push + #68 health-detailed + #77 desktop jumplist
#78 Excalidraw — finitions B5/C8/F2/F3
- backend/indexer.py: port Python pur de lz-string decompressFromBase64 (bitsPerChar=6, resetValue=32) validé contre 4 fixtures JS truth (texte accentué, edge cases) — remplace le stub base64 non-fonctionnel
- B5 indexation: .excalidraw.md (format plugin Obsidian) maintenant décompressé côté Python → texte indexé pour recherche TF-IDF
- 7 nouveaux tests B5 dans tests/test_excalidraw.py (13/13 total)
- F2: excalidraw-viewer.test.mjs enregistré dans CI (lint job)
- F3: tests/e2e/excalidraw.spec.js (9 specs — ouverture .excalidraw/.excalidraw.md, création via modale/menu contextuel, toolbar, thème, pop-out, recherche)
- Fixtures test_vault/diagram.excalidraw + diagram.excalidraw.md

#67 Push notifications (Web Push API + VAPID)
- backend/push.py: router + VAPID keys + send_push_notification (pywebpush>=2.3.0)
- frontend/js/push.js: subscription UI + service worker integration
- tests/test_push.py: 10 tests (subscribe/list/unsubscribe/vapid key)
- requirements.txt + sw.js + locales push strings

#68 Health check enrichi
- backend/main.py: GET /api/health/detailed (admin) — memory/cpu/disk/backups/index/SSE connections
- backend/indexer.py: _last_full_index_ts tracking
- tests/conftest.py: admin_client fixture
- tests/test_api_main.py: 3 nouveaux tests health detailed

#77 Desktop jumplist
- desktop/src/jumplist.rs: Windows jumplist integration
- Cargo.toml/lock + capabilities + main.rs
- frontend/js/desktop.js: Tauri bridge (isTauriEnv, invoke, getSystemTheme, syncSystemTheme, shouldShowWizard, crash banner)
- tests/frontend/desktop.test.mjs: 21 tests JSDOM (détection, invoke degradation, theme gating, wizard, crash banner)
- tests/frontend/unit.test.mjs: desktop.js whitelisted (standalone global reader)

# Frontend & CI
- frontend/sw.js: réécriture complète (precache + push event handlers + offline)
- frontend/index.html: section push dans settings + about repositionné
- frontend/js/app.js: initDesktopIntegration + initPush
- CI .gitea/workflows/ci.yml: excalidraw-viewer.test.mjs ajouté au lint job

All checks: ruff clean, 552 backend tests pass, 9 frontend unit, 5 excalidraw-viewer, 21 desktop, 9 pane-manager, validate-imports 33 modules.
2026-09-09 23:18:29 -04:00

220 lines
6.9 KiB
JavaScript

/* ObsiGate — Service Worker (Push notifications + Offline cache)
* Version: 1.0.0
* Scope: /
*/
const CACHE_NAME = 'obsigate-v1';
const OFFLINE_CACHE = 'obsigate-offline-v1';
// Files to cache for offline access
const PRECACHE_URLS = [
'/',
'/index.html',
'/manifest.json',
'/frontend/js/app.js',
'/frontend/js/state.js',
'/frontend/js/auth.js',
'/frontend/js/ui.js',
'/frontend/js/sidebar.js',
'/frontend/js/viewer.js',
'/frontend/js/search.js',
'/frontend/js/config.js',
'/frontend/js/utils.js',
'/frontend/js/i18n.js',
'/frontend/js/offline.js',
'/frontend/js/offline-db.js',
'/frontend/style.css',
'/static/lucide.min.js'
];
// ── Install: precache static assets ────────────────────────────────────────
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[SW] Precaching static assets');
return cache.addAll(PRECACHE_URLS);
}).then(() => self.skipWaiting())
);
});
// ── Activate: clean old caches ─────────────────────────────────────────────
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== OFFLINE_CACHE)
.map((name) => caches.delete(name))
);
}).then(() => self.clients.claim())
);
});
// ── Fetch: Network-first for API, Cache-first for static ───────────────────
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Skip non-GET
if (event.request.method !== 'GET') return;
// Skip cross-origin
if (url.origin !== location.origin) return;
// API requests: Network-first with offline fallback
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstStrategy(event.request));
return;
}
// Static assets: Cache-first
event.respondWith(cacheFirstStrategy(event.request));
});
// Network-first strategy (for API)
async function networkFirstStrategy(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(OFFLINE_CACHE);
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
return new Response(JSON.stringify({ error: 'Offline' }), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
}
}
// Cache-first strategy (for static assets)
async function cacheFirstStrategy(request) {
const cached = await caches.match(request);
if (cached) {
// Stale-while-revalidate: update cache in background
fetch(request).then((response) => {
if (response.ok) {
caches.open(CACHE_NAME).then((cache) => cache.put(request, response));
}
}).catch(() => {});
return cached;
}
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch (error) {
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return caches.match('/index.html');
}
return new Response('Offline', { status: 503 });
}
}
// ── Push Event: Display notification ───────────────────────────────────────
self.addEventListener('push', (event) => {
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body || data.message || 'Nouvelle notification',
icon: '/static/icon-192.png',
badge: '/static/badge-72.png',
vibrate: [200, 100, 200],
tag: data.tag || 'obsigate-notification',
renotify: true,
data: data.data || {},
actions: [
{ action: 'open', title: 'Ouvrir' },
{ action: 'dismiss', title: 'Ignorer' }
],
requireInteraction: false
};
event.waitUntil(
self.registration.showNotification(data.title || 'ObsiGate', options)
);
});
// ── Notification Click: Open the app ───────────────────────────────────────
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') return;
// Default action or 'open' action: navigate to the file
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
// Try to focus existing window
for (const client of clientList) {
if (client.url.includes(urlToOpen) && 'focus' in client) {
return client.focus();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// ── Notification Close ─────────────────────────────────────────────────────
self.addEventListener('notificationclose', (event) => {
console.log('[SW] Notification closed:', event.notification.tag);
});
// ── Background Sync: Periodic index sync ──────────────────────────────────
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-file-index') {
event.waitUntil(syncFileIndex());
}
});
async function syncFileIndex() {
try {
const resp = await fetch('/api/search/advanced?q=**&vault=all&limit=200');
if (resp.ok) {
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 || ''
}));
// Send to main thread for IndexedDB storage
const clients = await self.clients.matchAll();
clients.forEach(client => client.postMessage({
type: 'OFFLINE_INDEX_SYNC',
files
}));
}
} catch (e) {
console.warn('[SW] Background sync failed:', e);
}
}
// ── Periodic Background Sync (if supported) ────────────────────────────────
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'periodic-index-sync') {
event.waitUntil(syncFileIndex());
}
});
// ── Message from main thread ───────────────────────────────────────────────
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});