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>
633 lines
19 KiB
JavaScript
633 lines
19 KiB
JavaScript
/**
|
|
* BreakPilot Customer Portal - Slim JavaScript
|
|
*
|
|
* Features:
|
|
* - Login/Register
|
|
* - My Consents View
|
|
* - Data Export Request
|
|
* - Legal Documents
|
|
* - Theme Toggle
|
|
*/
|
|
|
|
// API Base URLs
|
|
const CONSENT_SERVICE_URL = 'http://localhost:8081';
|
|
const BACKEND_URL = '';
|
|
|
|
// State
|
|
let currentUser = null;
|
|
let authToken = localStorage.getItem('bp_token');
|
|
|
|
// Initialize on DOM ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initTheme();
|
|
initEventListeners();
|
|
checkAuth();
|
|
});
|
|
|
|
// ============================================================
|
|
// Theme
|
|
// ============================================================
|
|
|
|
function initTheme() {
|
|
const saved = localStorage.getItem('bp_theme') || 'light';
|
|
document.body.setAttribute('data-theme', saved);
|
|
updateThemeIcon(saved);
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const current = document.body.getAttribute('data-theme');
|
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
document.body.setAttribute('data-theme', next);
|
|
localStorage.setItem('bp_theme', next);
|
|
updateThemeIcon(next);
|
|
}
|
|
|
|
function updateThemeIcon(theme) {
|
|
const icon = document.getElementById('theme-icon');
|
|
if (icon) icon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
|
}
|
|
|
|
// ============================================================
|
|
// Event Listeners
|
|
// ============================================================
|
|
|
|
function initEventListeners() {
|
|
// Theme toggle
|
|
const themeBtn = document.getElementById('btn-theme');
|
|
if (themeBtn) themeBtn.addEventListener('click', toggleTheme);
|
|
|
|
// Login button
|
|
const loginBtn = document.getElementById('btn-login');
|
|
if (loginBtn) loginBtn.addEventListener('click', showLoginModal);
|
|
|
|
// Legal button
|
|
const legalBtn = document.getElementById('btn-legal');
|
|
if (legalBtn) legalBtn.addEventListener('click', () => showLegalDocument('privacy'));
|
|
|
|
// User menu toggle
|
|
const userMenuBtn = document.getElementById('user-menu-btn');
|
|
if (userMenuBtn) {
|
|
userMenuBtn.addEventListener('click', () => {
|
|
document.getElementById('user-menu').classList.toggle('open');
|
|
});
|
|
}
|
|
|
|
// Close user menu on outside click
|
|
document.addEventListener('click', (e) => {
|
|
const userMenu = document.getElementById('user-menu');
|
|
if (userMenu && !userMenu.contains(e.target)) {
|
|
userMenu.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
// Login form
|
|
const loginForm = document.getElementById('login-form');
|
|
if (loginForm) loginForm.addEventListener('submit', handleLogin);
|
|
|
|
// Register form
|
|
const registerForm = document.getElementById('register-form');
|
|
if (registerForm) registerForm.addEventListener('submit', handleRegister);
|
|
|
|
// Forgot password form
|
|
const forgotForm = document.getElementById('forgot-form');
|
|
if (forgotForm) forgotForm.addEventListener('submit', handleForgotPassword);
|
|
|
|
// Profile form
|
|
const profileForm = document.getElementById('profile-form');
|
|
if (profileForm) profileForm.addEventListener('submit', handleProfileUpdate);
|
|
|
|
// Password form
|
|
const passwordForm = document.getElementById('password-form');
|
|
if (passwordForm) passwordForm.addEventListener('submit', handlePasswordChange);
|
|
}
|
|
|
|
// ============================================================
|
|
// Authentication
|
|
// ============================================================
|
|
|
|
async function checkAuth() {
|
|
if (!authToken) {
|
|
showLoggedOutState();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/me`, {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
currentUser = await res.json();
|
|
showLoggedInState();
|
|
} else {
|
|
localStorage.removeItem('bp_token');
|
|
authToken = null;
|
|
showLoggedOutState();
|
|
}
|
|
} catch (err) {
|
|
console.error('Auth check failed:', err);
|
|
showLoggedOutState();
|
|
}
|
|
}
|
|
|
|
function showLoggedInState() {
|
|
document.getElementById('btn-login')?.classList.add('hidden');
|
|
document.getElementById('user-menu')?.classList.remove('hidden');
|
|
document.getElementById('welcome-section')?.classList.add('hidden');
|
|
document.getElementById('dashboard')?.classList.remove('hidden');
|
|
|
|
if (currentUser) {
|
|
const initials = getInitials(currentUser.name || currentUser.email);
|
|
document.querySelectorAll('.user-avatar').forEach(el => el.textContent = initials);
|
|
document.getElementById('user-name').textContent = currentUser.name || 'Benutzer';
|
|
document.getElementById('dropdown-name').textContent = currentUser.name || 'Benutzer';
|
|
document.getElementById('dropdown-email').textContent = currentUser.email;
|
|
}
|
|
|
|
loadConsentCount();
|
|
}
|
|
|
|
function showLoggedOutState() {
|
|
document.getElementById('btn-login')?.classList.remove('hidden');
|
|
document.getElementById('user-menu')?.classList.add('hidden');
|
|
document.getElementById('welcome-section')?.classList.remove('hidden');
|
|
document.getElementById('dashboard')?.classList.add('hidden');
|
|
}
|
|
|
|
function getInitials(name) {
|
|
if (!name) return 'BP';
|
|
const parts = name.split(' ').filter(p => p.length > 0);
|
|
if (parts.length >= 2) {
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
return name.substring(0, 2).toUpperCase();
|
|
}
|
|
|
|
async function handleLogin(e) {
|
|
e.preventDefault();
|
|
const email = document.getElementById('login-email').value;
|
|
const password = document.getElementById('login-password').value;
|
|
const messageEl = document.getElementById('login-message');
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (res.ok) {
|
|
authToken = data.token;
|
|
localStorage.setItem('bp_token', authToken);
|
|
currentUser = data.user;
|
|
showMessage(messageEl, 'Erfolgreich angemeldet!', 'success');
|
|
setTimeout(() => {
|
|
closeAllModals();
|
|
showLoggedInState();
|
|
}, 500);
|
|
} else {
|
|
showMessage(messageEl, data.error || 'Anmeldung fehlgeschlagen', 'error');
|
|
}
|
|
} catch (err) {
|
|
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
|
}
|
|
}
|
|
|
|
async function handleRegister(e) {
|
|
e.preventDefault();
|
|
const name = document.getElementById('register-name').value;
|
|
const email = document.getElementById('register-email').value;
|
|
const password = document.getElementById('register-password').value;
|
|
const passwordConfirm = document.getElementById('register-password-confirm').value;
|
|
const messageEl = document.getElementById('register-message');
|
|
|
|
if (password !== passwordConfirm) {
|
|
showMessage(messageEl, 'Passwörter stimmen nicht überein', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, email, password })
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (res.ok) {
|
|
showMessage(messageEl, 'Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success');
|
|
setTimeout(() => switchAuthTab('login'), 1500);
|
|
} else {
|
|
showMessage(messageEl, data.error || 'Registrierung fehlgeschlagen', 'error');
|
|
}
|
|
} catch (err) {
|
|
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
|
}
|
|
}
|
|
|
|
async function handleForgotPassword(e) {
|
|
e.preventDefault();
|
|
const email = document.getElementById('forgot-email').value;
|
|
const messageEl = document.getElementById('forgot-message');
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/request-password-reset`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email })
|
|
});
|
|
|
|
showMessage(messageEl, 'Wenn ein Konto mit dieser E-Mail existiert, erhalten Sie einen Link zum Zurücksetzen.', 'success');
|
|
} catch (err) {
|
|
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('bp_token');
|
|
authToken = null;
|
|
currentUser = null;
|
|
showLoggedOutState();
|
|
document.getElementById('user-menu')?.classList.remove('open');
|
|
}
|
|
|
|
// ============================================================
|
|
// Modals
|
|
// ============================================================
|
|
|
|
function showLoginModal() {
|
|
document.getElementById('login-modal')?.classList.add('active');
|
|
switchAuthTab('login');
|
|
}
|
|
|
|
function showMyConsents() {
|
|
document.getElementById('consents-modal')?.classList.add('active');
|
|
document.getElementById('user-menu')?.classList.remove('open');
|
|
loadMyConsents();
|
|
}
|
|
|
|
function showDataExport() {
|
|
document.getElementById('export-modal')?.classList.add('active');
|
|
document.getElementById('user-menu')?.classList.remove('open');
|
|
loadExportRequests();
|
|
}
|
|
|
|
function showAccountSettings() {
|
|
document.getElementById('settings-modal')?.classList.add('active');
|
|
document.getElementById('user-menu')?.classList.remove('open');
|
|
|
|
if (currentUser) {
|
|
document.getElementById('profile-name').value = currentUser.name || '';
|
|
document.getElementById('profile-email').value = currentUser.email || '';
|
|
}
|
|
}
|
|
|
|
function showLegalDocument(type) {
|
|
const modal = document.getElementById('legal-modal');
|
|
const titleEl = document.getElementById('legal-title');
|
|
const loadingEl = document.getElementById('legal-loading');
|
|
const contentEl = document.getElementById('legal-content');
|
|
|
|
const titles = {
|
|
privacy: 'Datenschutzerklärung',
|
|
terms: 'Allgemeine Geschäftsbedingungen',
|
|
imprint: 'Impressum',
|
|
cookies: 'Cookie-Richtlinie'
|
|
};
|
|
|
|
titleEl.textContent = titles[type] || 'Dokument';
|
|
loadingEl.style.display = 'block';
|
|
contentEl.innerHTML = '';
|
|
modal?.classList.add('active');
|
|
|
|
loadLegalDocument(type).then(html => {
|
|
loadingEl.style.display = 'none';
|
|
contentEl.innerHTML = html;
|
|
}).catch(() => {
|
|
loadingEl.style.display = 'none';
|
|
contentEl.innerHTML = '<p>Dokument konnte nicht geladen werden.</p>';
|
|
});
|
|
}
|
|
|
|
function showForgotPassword() {
|
|
document.getElementById('login-form')?.classList.add('hidden');
|
|
document.getElementById('register-form')?.classList.add('hidden');
|
|
document.getElementById('forgot-form')?.classList.remove('hidden');
|
|
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
|
|
}
|
|
|
|
function closeAllModals() {
|
|
document.querySelectorAll('.modal.active').forEach(m => m.classList.remove('active'));
|
|
}
|
|
|
|
function switchAuthTab(tab) {
|
|
document.querySelectorAll('.auth-tab').forEach(t => {
|
|
t.classList.toggle('active', t.dataset.tab === tab);
|
|
});
|
|
document.getElementById('login-form')?.classList.toggle('hidden', tab !== 'login');
|
|
document.getElementById('register-form')?.classList.toggle('hidden', tab !== 'register');
|
|
document.getElementById('forgot-form')?.classList.add('hidden');
|
|
|
|
// Clear messages
|
|
document.querySelectorAll('.form-message').forEach(m => {
|
|
m.className = 'form-message';
|
|
m.textContent = '';
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// Consents
|
|
// ============================================================
|
|
|
|
async function loadConsentCount() {
|
|
if (!authToken) return;
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/consents/my`, {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const count = data.consents?.length || 0;
|
|
const badge = document.getElementById('consent-count');
|
|
if (badge) badge.textContent = `${count} aktiv`;
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load consent count:', err);
|
|
}
|
|
}
|
|
|
|
async function loadMyConsents() {
|
|
const loadingEl = document.getElementById('consents-loading');
|
|
const listEl = document.getElementById('consents-list');
|
|
const emptyEl = document.getElementById('consents-empty');
|
|
|
|
loadingEl.style.display = 'block';
|
|
listEl.innerHTML = '';
|
|
emptyEl?.classList.add('hidden');
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/consents/my`, {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const consents = data.consents || [];
|
|
|
|
loadingEl.style.display = 'none';
|
|
|
|
if (consents.length === 0) {
|
|
emptyEl?.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = consents.map(c => `
|
|
<div class="consent-item">
|
|
<div class="consent-info">
|
|
<h3>${escapeHtml(c.document_name || c.document_type)}</h3>
|
|
<p>Version ${c.version || '1.0'}</p>
|
|
</div>
|
|
<div class="consent-date">
|
|
Zugestimmt am ${formatDate(c.created_at)}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
loadingEl.style.display = 'none';
|
|
listEl.innerHTML = '<p>Fehler beim Laden der Zustimmungen.</p>';
|
|
}
|
|
} catch (err) {
|
|
loadingEl.style.display = 'none';
|
|
listEl.innerHTML = '<p>Verbindungsfehler.</p>';
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Data Export (GDPR)
|
|
// ============================================================
|
|
|
|
async function requestDataExport() {
|
|
const messageEl = document.getElementById('export-message');
|
|
const btn = document.getElementById('btn-request-export');
|
|
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const res = await fetch(`${BACKEND_URL}/api/gdpr/request-export`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${authToken}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (res.ok) {
|
|
showMessage(messageEl, 'Ihre Anfrage wurde erfolgreich eingereicht. Sie erhalten eine E-Mail, sobald der Export bereit ist.', 'success');
|
|
loadExportRequests();
|
|
} else {
|
|
const data = await res.json();
|
|
showMessage(messageEl, data.error || 'Anfrage fehlgeschlagen', 'error');
|
|
}
|
|
} catch (err) {
|
|
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function loadExportRequests() {
|
|
const statusEl = document.getElementById('export-status');
|
|
const listEl = document.getElementById('export-requests-list');
|
|
|
|
if (!statusEl || !listEl) return;
|
|
|
|
try {
|
|
const res = await fetch(`${BACKEND_URL}/api/gdpr/my-requests`, {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const requests = data.requests || [];
|
|
|
|
if (requests.length > 0) {
|
|
statusEl.classList.remove('hidden');
|
|
listEl.innerHTML = requests.map(r => `
|
|
<div class="consent-item">
|
|
<div class="consent-info">
|
|
<h3>${r.type === 'export' ? 'Datenexport' : 'Datenlöschung'}</h3>
|
|
<p>Status: ${r.status}</p>
|
|
</div>
|
|
<div class="consent-date">
|
|
${formatDate(r.created_at)}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} else {
|
|
statusEl.classList.add('hidden');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load export requests:', err);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Legal Documents
|
|
// ============================================================
|
|
|
|
async function loadLegalDocument(type) {
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/type/${type}/published`);
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
return data.content || '<p>Dokument ist leer.</p>';
|
|
}
|
|
|
|
return '<p>Dokument nicht gefunden.</p>';
|
|
} catch (err) {
|
|
return '<p>Fehler beim Laden.</p>';
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Profile & Settings
|
|
// ============================================================
|
|
|
|
async function handleProfileUpdate(e) {
|
|
e.preventDefault();
|
|
const name = document.getElementById('profile-name').value;
|
|
const messageEl = document.getElementById('profile-message');
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/update-profile`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ name })
|
|
});
|
|
|
|
if (res.ok) {
|
|
currentUser.name = name;
|
|
showMessage(messageEl, 'Profil aktualisiert', 'success');
|
|
showLoggedInState();
|
|
} else {
|
|
showMessage(messageEl, 'Aktualisierung fehlgeschlagen', 'error');
|
|
}
|
|
} catch (err) {
|
|
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
|
}
|
|
}
|
|
|
|
async function handlePasswordChange(e) {
|
|
e.preventDefault();
|
|
const currentPassword = document.getElementById('current-password').value;
|
|
const newPassword = document.getElementById('new-password').value;
|
|
const confirmPassword = document.getElementById('new-password-confirm').value;
|
|
const messageEl = document.getElementById('password-message');
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
showMessage(messageEl, 'Passwörter stimmen nicht überein', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/change-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
current_password: currentPassword,
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
showMessage(messageEl, 'Passwort geändert', 'success');
|
|
document.getElementById('password-form').reset();
|
|
} else {
|
|
const data = await res.json();
|
|
showMessage(messageEl, data.error || 'Änderung fehlgeschlagen', 'error');
|
|
}
|
|
} catch (err) {
|
|
showMessage(messageEl, 'Verbindungsfehler', 'error');
|
|
}
|
|
}
|
|
|
|
function confirmDeleteAccount() {
|
|
if (confirm('Sind Sie sicher, dass Sie Ihr Konto löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
|
if (confirm('Letzte Warnung: Alle Ihre Daten werden unwiderruflich gelöscht. Fortfahren?')) {
|
|
deleteAccount();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deleteAccount() {
|
|
try {
|
|
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/delete-account`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (res.ok) {
|
|
alert('Ihr Konto wurde gelöscht.');
|
|
logout();
|
|
closeAllModals();
|
|
} else {
|
|
alert('Kontoloöschung fehlgeschlagen.');
|
|
}
|
|
} catch (err) {
|
|
alert('Verbindungsfehler');
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Utilities
|
|
// ============================================================
|
|
|
|
function showMessage(el, text, type) {
|
|
if (!el) return;
|
|
el.textContent = text;
|
|
el.className = `form-message ${type}`;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
}
|
|
|
|
// Export for global access
|
|
window.showLoginModal = showLoginModal;
|
|
window.showMyConsents = showMyConsents;
|
|
window.showDataExport = showDataExport;
|
|
window.showAccountSettings = showAccountSettings;
|
|
window.showLegalDocument = showLegalDocument;
|
|
window.showForgotPassword = showForgotPassword;
|
|
window.closeAllModals = closeAllModals;
|
|
window.switchAuthTab = switchAuthTab;
|
|
window.logout = logout;
|
|
window.requestDataExport = requestDataExport;
|
|
window.confirmDeleteAccount = confirmDeleteAccount;
|