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>
361 lines
10 KiB
JavaScript
361 lines
10 KiB
JavaScript
/**
|
|
* BreakPilot Studio - API Helpers Module
|
|
*
|
|
* Gemeinsame Funktionen für API-Aufrufe und Status-Verwaltung:
|
|
* - fetchJSON: Wrapper für fetch mit Error-Handling
|
|
* - postJSON: POST-Requests mit JSON-Body
|
|
* - setStatus: Status-Leiste aktualisieren
|
|
* - showNotification: Toast-Benachrichtigungen
|
|
*
|
|
* Refactored: 2026-01-19
|
|
*/
|
|
|
|
import { t } from './i18n.js';
|
|
|
|
// Status-Bar Element-Referenzen (werden bei init gesetzt)
|
|
let statusBar = null;
|
|
let statusDot = null;
|
|
let statusMain = null;
|
|
let statusSub = null;
|
|
|
|
/**
|
|
* Initialisiert die Status-Bar Referenzen
|
|
* Sollte beim DOMContentLoaded aufgerufen werden
|
|
*/
|
|
export function initStatusBar() {
|
|
statusBar = document.getElementById('status-bar');
|
|
statusDot = document.getElementById('status-dot');
|
|
statusMain = document.getElementById('status-main');
|
|
statusSub = document.getElementById('status-sub');
|
|
}
|
|
|
|
/**
|
|
* Setzt den Status in der Status-Leiste
|
|
* @param {string} type - 'ready'|'working'|'success'|'error'
|
|
* @param {string} main - Haupttext
|
|
* @param {string} [sub] - Optionaler Untertext
|
|
*/
|
|
export function setStatus(type, main, sub = '') {
|
|
if (!statusBar || !statusDot || !statusMain) {
|
|
console.log(`[Status ${type}]: ${main}`, sub);
|
|
return;
|
|
}
|
|
|
|
// Alle Status-Klassen entfernen
|
|
statusBar.classList.remove('status-ready', 'status-working', 'status-success', 'status-error');
|
|
statusDot.classList.remove('dot-ready', 'dot-working', 'dot-success', 'dot-error');
|
|
|
|
// Neue Status-Klasse setzen
|
|
statusBar.classList.add(`status-${type}`);
|
|
statusDot.classList.add(`dot-${type}`);
|
|
|
|
// Texte setzen
|
|
statusMain.textContent = main;
|
|
if (statusSub) {
|
|
statusSub.textContent = sub;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setzt den Status auf "Bereit"
|
|
*/
|
|
export function setStatusReady() {
|
|
setStatus('ready', t('status_ready') || 'Bereit', '');
|
|
}
|
|
|
|
/**
|
|
* Setzt den Status auf "Arbeitet..."
|
|
* @param {string} message - Was gerade gemacht wird
|
|
*/
|
|
export function setStatusWorking(message) {
|
|
setStatus('working', message, '');
|
|
}
|
|
|
|
/**
|
|
* Setzt den Status auf "Erfolg"
|
|
* @param {string} message - Erfolgsmeldung
|
|
* @param {string} [details] - Optionale Details
|
|
*/
|
|
export function setStatusSuccess(message, details = '') {
|
|
setStatus('success', message, details);
|
|
}
|
|
|
|
/**
|
|
* Setzt den Status auf "Fehler"
|
|
* @param {string} message - Fehlermeldung
|
|
* @param {string} [details] - Optionale Details
|
|
*/
|
|
export function setStatusError(message, details = '') {
|
|
setStatus('error', message, details);
|
|
}
|
|
|
|
/**
|
|
* Führt einen GET-Request aus und parst JSON
|
|
* @param {string} url - Die URL
|
|
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
|
* @returns {Promise<any>} - Das geparste JSON
|
|
* @throws {Error} - Bei Netzwerk- oder Parse-Fehlern
|
|
*/
|
|
export async function fetchJSON(url, options = {}) {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Führt einen POST-Request mit JSON-Body aus
|
|
* @param {string} url - Die URL
|
|
* @param {Object} data - Die zu sendenden Daten
|
|
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
|
* @returns {Promise<any>} - Das geparste JSON
|
|
* @throws {Error} - Bei Netzwerk- oder Parse-Fehlern
|
|
*/
|
|
export async function postJSON(url, data = {}, options = {}) {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Führt einen POST-Request ohne Body aus (für Trigger-Endpoints)
|
|
* @param {string} url - Die URL
|
|
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
|
* @returns {Promise<any>} - Das geparste JSON
|
|
*/
|
|
export async function postTrigger(url, options = {}) {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
...options,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Führt einen DELETE-Request aus
|
|
* @param {string} url - Die URL
|
|
* @param {Object} [options] - Zusätzliche fetch-Optionen
|
|
* @returns {Promise<any>} - Das geparste JSON
|
|
*/
|
|
export async function deleteRequest(url, options = {}) {
|
|
const response = await fetch(url, {
|
|
method: 'DELETE',
|
|
...options,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Lädt eine Datei hoch
|
|
* @param {string} url - Die Upload-URL
|
|
* @param {File|FormData} file - Die Datei oder FormData
|
|
* @param {function} [onProgress] - Progress-Callback (0-100)
|
|
* @returns {Promise<any>} - Das geparste JSON
|
|
*/
|
|
export async function uploadFile(url, file, onProgress = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.open('POST', url);
|
|
|
|
if (onProgress && xhr.upload) {
|
|
xhr.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable) {
|
|
const percent = Math.round((e.loaded / e.total) * 100);
|
|
onProgress(percent);
|
|
}
|
|
});
|
|
}
|
|
|
|
xhr.onload = () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
resolve(JSON.parse(xhr.responseText));
|
|
} catch (e) {
|
|
resolve({ status: 'OK', message: xhr.responseText });
|
|
}
|
|
} else {
|
|
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
}
|
|
};
|
|
|
|
xhr.onerror = () => reject(new Error('Network error during upload'));
|
|
|
|
// FormData erstellen falls nötig
|
|
let formData;
|
|
if (file instanceof FormData) {
|
|
formData = file;
|
|
} else {
|
|
formData = new FormData();
|
|
formData.append('file', file);
|
|
}
|
|
|
|
xhr.send(formData);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Zeigt eine kurze Benachrichtigung (Toast)
|
|
* @param {string} message - Die Nachricht
|
|
* @param {string} [type='info'] - 'info'|'success'|'error'|'warning'
|
|
* @param {number} [duration=3000] - Anzeigedauer in ms
|
|
*/
|
|
export function showNotification(message, type = 'info', duration = 3000) {
|
|
// Prüfe ob Toast-Container existiert, sonst erstellen
|
|
let container = document.getElementById('toast-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = 'toast-container';
|
|
container.style.cssText = 'position:fixed;top:16px;right:16px;z-index:10000;display:flex;flex-direction:column;gap:8px;';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
// Toast erstellen
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.style.cssText = `
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
background: var(--bp-card-bg, #1e293b);
|
|
color: var(--bp-text, #e2e8f0);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
font-size: 13px;
|
|
animation: slideIn 0.3s ease;
|
|
border-left: 4px solid ${type === 'success' ? '#22c55e' : type === 'error' ? '#ef4444' : type === 'warning' ? '#f59e0b' : '#3b82f6'};
|
|
`;
|
|
toast.textContent = message;
|
|
|
|
container.appendChild(toast);
|
|
|
|
// Nach duration entfernen
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, duration);
|
|
}
|
|
|
|
/**
|
|
* Wrapper für API-Aufrufe mit Status-Anzeige und Error-Handling
|
|
* @param {function} apiCall - Die async API-Funktion
|
|
* @param {Object} options - Optionen
|
|
* @param {string} options.workingMessage - Nachricht während des Ladens
|
|
* @param {string} options.successMessage - Nachricht bei Erfolg
|
|
* @param {string} options.errorMessage - Nachricht bei Fehler
|
|
* @returns {Promise<any>} - Das Ergebnis oder null bei Fehler
|
|
*/
|
|
export async function withStatus(apiCall, options = {}) {
|
|
const {
|
|
workingMessage = 'Wird geladen...',
|
|
successMessage = 'Erfolgreich',
|
|
errorMessage = 'Fehler',
|
|
} = options;
|
|
|
|
setStatusWorking(workingMessage);
|
|
|
|
try {
|
|
const result = await apiCall();
|
|
setStatusSuccess(successMessage);
|
|
return result;
|
|
} catch (error) {
|
|
console.error(errorMessage, error);
|
|
setStatusError(errorMessage, String(error.message || error));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debounce-Funktion für häufige Events
|
|
* @param {function} func - Die zu debouncende Funktion
|
|
* @param {number} wait - Wartezeit in ms
|
|
* @returns {function} - Die gedebouncte Funktion
|
|
*/
|
|
export function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Throttle-Funktion für Rate-Limiting
|
|
* @param {function} func - Die zu throttlende Funktion
|
|
* @param {number} limit - Minimaler Abstand in ms
|
|
* @returns {function} - Die gethrottlete Funktion
|
|
*/
|
|
export function throttle(func, limit) {
|
|
let inThrottle;
|
|
return function(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
// CSS für Toast-Animationen (einmal injizieren)
|
|
if (typeof document !== 'undefined' && !document.getElementById('toast-styles')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'toast-styles';
|
|
style.textContent = `
|
|
@keyframes slideIn {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
@keyframes slideOut {
|
|
from { transform: translateX(0); opacity: 1; }
|
|
to { transform: translateX(100%); opacity: 0; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|