This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/static/service-worker.js
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

344 lines
10 KiB
JavaScript

/**
* BreakPilot PWA Service Worker
*
* Funktionen:
* 1. Cacht statische Assets fuer Offline-Nutzung
* 2. Cacht LLM-Modell fuer lokale Header-Extraktion
* 3. Background Sync fuer Korrektur-Uploads
*/
const APP_CACHE = 'breakpilot-app-v1';
const LLM_CACHE = 'breakpilot-llm-v1';
const DATA_CACHE = 'breakpilot-data-v1';
// Statische Assets zum Cachen beim Install
const STATIC_ASSETS = [
'/',
'/app',
'/static/css/main.css',
'/static/js/main.js'
];
// LLM/OCR Assets (groessere Dateien, separater Cache)
const LLM_ASSETS = [
// Tesseract.js (OCR)
'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js',
'https://cdn.jsdelivr.net/npm/tesseract.js-core@5.0.0/tesseract-core.wasm.js',
// Tesseract Deutsch Sprachpaket
'https://tessdata.projectnaptha.com/4.0.0/deu.traineddata.gz',
// Transformers.js (fuer zukuenftige Vision-Modelle)
'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.1/dist/transformers.min.js'
];
// ============================================================
// INSTALL EVENT
// ============================================================
self.addEventListener('install', (event) => {
console.log('[ServiceWorker] Installing...');
event.waitUntil(
Promise.all([
// App Cache
caches.open(APP_CACHE).then((cache) => {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(STATIC_ASSETS);
}),
// LLM Cache (optional, bei Fehler ignorieren)
caches.open(LLM_CACHE).then((cache) => {
console.log('[ServiceWorker] Pre-caching LLM assets');
return Promise.allSettled(
LLM_ASSETS.map(url =>
cache.add(url).catch(err => {
console.warn('[ServiceWorker] Failed to cache:', url, err);
})
)
);
})
]).then(() => {
console.log('[ServiceWorker] Install complete');
return self.skipWaiting();
})
);
});
// ============================================================
// ACTIVATE EVENT
// ============================================================
self.addEventListener('activate', (event) => {
console.log('[ServiceWorker] Activating...');
event.waitUntil(
// Alte Caches aufräumen
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter(name => {
// Nur alte Versionen loeschen
return name.startsWith('breakpilot-') &&
![APP_CACHE, LLM_CACHE, DATA_CACHE].includes(name);
})
.map(name => {
console.log('[ServiceWorker] Deleting old cache:', name);
return caches.delete(name);
})
);
}).then(() => {
console.log('[ServiceWorker] Claiming clients');
return self.clients.claim();
})
);
});
// ============================================================
// FETCH EVENT
// ============================================================
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// API-Requests: Network-First (immer frische Daten)
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstStrategy(event.request));
return;
}
// LLM/OCR Assets: Cache-First (grosse Dateien)
if (isLLMAsset(url)) {
event.respondWith(cacheFirstStrategy(event.request, LLM_CACHE));
return;
}
// Statische Assets: Cache-First mit Network-Fallback
if (isStaticAsset(url)) {
event.respondWith(cacheFirstStrategy(event.request, APP_CACHE));
return;
}
// Alles andere: Network-First
event.respondWith(networkFirstStrategy(event.request));
});
// ============================================================
// CACHING STRATEGIES
// ============================================================
/**
* Cache-First Strategy: Versucht Cache, dann Network.
* Gut fuer statische Assets und LLM-Modelle.
*/
async function cacheFirstStrategy(request, cacheName) {
const cache = await caches.open(cacheName);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// Im Hintergrund aktualisieren (Stale-While-Revalidate)
fetchAndCache(request, cache);
return cachedResponse;
}
// Nicht im Cache: Von Network holen und cachen
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[ServiceWorker] Fetch failed:', error);
return new Response('Offline - Bitte Internetverbindung pruefen', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
/**
* Network-First Strategy: Versucht Network, dann Cache.
* Gut fuer API-Calls und dynamische Inhalte.
*/
async function networkFirstStrategy(request) {
const cache = await caches.open(DATA_CACHE);
try {
const networkResponse = await fetch(request);
// Erfolgreiche GET-Requests cachen
if (networkResponse.ok && request.method === 'GET') {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// Network failed: Versuche Cache
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Kein Cache: Offline-Fehler
return new Response(JSON.stringify({
error: 'offline',
message: 'Keine Internetverbindung'
}), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
}
}
/**
* Holt Resource von Network und cached sie (im Hintergrund).
*/
async function fetchAndCache(request, cache) {
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
} catch (error) {
// Ignorieren - Cache ist noch gueltig
}
}
// ============================================================
// HELPERS
// ============================================================
function isStaticAsset(url) {
return url.pathname.startsWith('/static/') ||
url.pathname.endsWith('.css') ||
url.pathname.endsWith('.js') ||
url.pathname.endsWith('.png') ||
url.pathname.endsWith('.jpg') ||
url.pathname.endsWith('.svg') ||
url.pathname.endsWith('.woff2');
}
function isLLMAsset(url) {
const llmDomains = [
'cdn.jsdelivr.net',
'huggingface.co',
'tessdata.projectnaptha.com'
];
const llmExtensions = [
'.onnx',
'.wasm',
'.traineddata',
'.traineddata.gz',
'.bin'
];
return llmDomains.some(d => url.hostname.includes(d)) ||
llmExtensions.some(e => url.pathname.endsWith(e));
}
// ============================================================
// BACKGROUND SYNC (fuer Offline-Uploads)
// ============================================================
self.addEventListener('sync', (event) => {
console.log('[ServiceWorker] Sync event:', event.tag);
if (event.tag === 'upload-exams') {
event.waitUntil(uploadPendingExams());
}
});
async function uploadPendingExams() {
// Hole ausstehende Uploads aus IndexedDB
const db = await openDB('breakpilot-pending', 1);
const pendingUploads = await db.getAll('uploads');
for (const upload of pendingUploads) {
try {
const response = await fetch(upload.url, {
method: 'POST',
body: upload.formData
});
if (response.ok) {
await db.delete('uploads', upload.id);
// Benachrichtigung an Client
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({
type: 'upload-complete',
id: upload.id
});
});
});
}
} catch (error) {
console.error('[ServiceWorker] Upload failed:', error);
}
}
}
// Simple IndexedDB wrapper
function openDB(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('uploads')) {
db.createObjectStore('uploads', { keyPath: 'id', autoIncrement: true });
}
};
});
}
// ============================================================
// PUSH NOTIFICATIONS (fuer Korrektur-Fortschritt)
// ============================================================
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
const options = {
body: data.body || 'Neue Benachrichtigung',
icon: '/static/icons/icon-192.png',
badge: '/static/icons/badge-72.png',
tag: data.tag || 'default',
data: data.url || '/',
actions: data.actions || []
};
event.waitUntil(
self.registration.showNotification(
data.title || 'BreakPilot',
options
)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
// Existierendes Fenster fokussieren
for (const client of clientList) {
if (client.url.includes('/app') && 'focus' in client) {
return client.focus();
}
}
// Neues Fenster oeffnen
if (clients.openWindow) {
return clients.openWindow(event.notification.data || '/app');
}
})
);
});
console.log('[ServiceWorker] Loaded');