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:
360
backend/frontend/static/js/modules/api-helpers.js
Normal file
360
backend/frontend/static/js/modules/api-helpers.js
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user