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
Benjamin Admin 21a844cb8a 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

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