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>
This commit is contained in:
343
backend/frontend/static/service-worker.js
Normal file
343
backend/frontend/static/service-worker.js
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* 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');
|
||||
Reference in New Issue
Block a user