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>
515 lines
22 KiB
Python
515 lines
22 KiB
Python
"""
|
|
Legal Modal Component - AGB, Datenschutz, Cookies, Community Guidelines, GDPR Rights
|
|
"""
|
|
|
|
def get_legal_modal_css() -> str:
|
|
"""CSS für Legal Modal zurückgeben"""
|
|
return """
|
|
/* ==========================================
|
|
LEGAL MODAL STYLES
|
|
========================================== */
|
|
.legal-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 10000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.legal-modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.legal-modal-content {
|
|
background: var(--bp-surface);
|
|
border-radius: 16px;
|
|
width: 90%;
|
|
max-width: 700px;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
border: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.legal-modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.legal-modal-header h2 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.legal-modal-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 28px;
|
|
cursor: pointer;
|
|
color: var(--bp-text-muted);
|
|
padding: 0;
|
|
line-height: 1;
|
|
}
|
|
|
|
.legal-modal-close:hover {
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.legal-tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 12px 24px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
.legal-tab {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--bp-text-muted);
|
|
cursor: pointer;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.legal-tab:hover {
|
|
background: var(--bp-border-subtle);
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.legal-tab.active {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.legal-body {
|
|
padding: 24px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.legal-content {
|
|
display: none;
|
|
}
|
|
|
|
.legal-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.legal-content h3 {
|
|
margin-top: 0;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.legal-content p {
|
|
line-height: 1.6;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.legal-content ul {
|
|
color: var(--bp-text-muted);
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.cookie-categories {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.cookie-category {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 12px;
|
|
background: var(--bp-surface-elevated);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--bp-border);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.cookie-category input {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.cookie-category span {
|
|
flex: 1;
|
|
font-size: 13px;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.gdpr-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.gdpr-action {
|
|
padding: 16px;
|
|
background: var(--bp-surface-elevated);
|
|
border-radius: 12px;
|
|
border: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.gdpr-action h4 {
|
|
margin: 0 0 8px 0;
|
|
font-size: 14px;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.gdpr-action p {
|
|
margin: 0 0 12px 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--bp-danger) !important;
|
|
border-color: var(--bp-danger) !important;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
filter: brightness(1.1);
|
|
}
|
|
|
|
[data-theme="light"] .legal-modal-content {
|
|
background: #FFFFFF;
|
|
border-color: #E0E0E0;
|
|
}
|
|
|
|
[data-theme="light"] .legal-tabs {
|
|
background: #F5F5F5;
|
|
}
|
|
|
|
[data-theme="light"] .legal-tab.active {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
[data-theme="light"] .cookie-category,
|
|
[data-theme="light"] .gdpr-action {
|
|
background: #F8F8F8;
|
|
border-color: #E0E0E0;
|
|
}
|
|
"""
|
|
|
|
|
|
def get_legal_modal_html() -> str:
|
|
"""HTML für Legal Modal zurückgeben"""
|
|
return """
|
|
<!-- Legal Modal -->
|
|
<div id="legal-modal" class="legal-modal">
|
|
<div class="legal-modal-content">
|
|
<div class="legal-modal-header">
|
|
<h2>Rechtliches</h2>
|
|
<button id="legal-modal-close" class="legal-modal-close">×</button>
|
|
</div>
|
|
<div class="legal-tabs">
|
|
<button class="legal-tab active" data-tab="terms">AGB</button>
|
|
<button class="legal-tab" data-tab="privacy">Datenschutz</button>
|
|
<button class="legal-tab" data-tab="community">Community Guidelines</button>
|
|
<button class="legal-tab" data-tab="cookies">Cookie-Einstellungen</button>
|
|
<button class="legal-tab" data-tab="gdpr">DSGVO-Rechte</button>
|
|
</div>
|
|
<div class="legal-body">
|
|
<div id="legal-terms" class="legal-content active">
|
|
<div id="legal-terms-content">
|
|
<div class="legal-loading">Lade AGB...</div>
|
|
</div>
|
|
</div>
|
|
<div id="legal-privacy" class="legal-content">
|
|
<div id="legal-privacy-content">
|
|
<div class="legal-loading">Lade Datenschutzerklärung...</div>
|
|
</div>
|
|
</div>
|
|
<div id="legal-community" class="legal-content">
|
|
<div id="legal-community-content">
|
|
<div class="legal-loading">Lade Community Guidelines...</div>
|
|
</div>
|
|
</div>
|
|
<div id="legal-cookies" class="legal-content">
|
|
<h3>Cookie-Einstellungen</h3>
|
|
<p>Wir verwenden Cookies, um Ihnen die bestmögliche Erfahrung zu bieten. Hier können Sie Ihre Präferenzen jederzeit anpassen.</p>
|
|
<div id="cookie-categories-container" class="cookie-categories">
|
|
<div class="legal-loading">Lade Cookie-Kategorien...</div>
|
|
</div>
|
|
<button class="btn btn-primary" style="margin-top:16px" onclick="saveCookiePreferences()">Einstellungen speichern</button>
|
|
</div>
|
|
<div id="legal-gdpr" class="legal-content">
|
|
<h3>Ihre DSGVO-Rechte</h3>
|
|
<p>Nach der Datenschutz-Grundverordnung haben Sie folgende Rechte:</p>
|
|
<div class="gdpr-actions">
|
|
<div class="gdpr-action">
|
|
<h4>📋 Datenauskunft (Art. 15)</h4>
|
|
<p>Erfahren Sie, welche Daten wir über Sie gespeichert haben.</p>
|
|
<button class="btn btn-sm" onclick="requestDataExport()">Meine Daten anfordern</button>
|
|
</div>
|
|
<div class="gdpr-action">
|
|
<h4>📤 Datenübertragbarkeit (Art. 20)</h4>
|
|
<p>Exportieren Sie Ihre Daten in einem maschinenlesbaren Format.</p>
|
|
<button class="btn btn-sm" onclick="requestDataDownload()">Daten exportieren</button>
|
|
</div>
|
|
<div class="gdpr-action">
|
|
<h4>🗑️ Recht auf Löschung (Art. 17)</h4>
|
|
<p>Beantragen Sie die vollständige Löschung Ihrer Daten.</p>
|
|
<button class="btn btn-sm btn-danger" onclick="requestDataDeletion()">Löschung beantragen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Imprint Modal (Impressum - muss direkt erreichbar sein) -->
|
|
<div id="imprint-modal" class="legal-modal">
|
|
<div class="legal-modal-content">
|
|
<div class="legal-modal-header">
|
|
<h2>Impressum</h2>
|
|
<button id="imprint-modal-close" class="legal-modal-close">×</button>
|
|
</div>
|
|
<div class="legal-body" style="padding: 24px;">
|
|
<div id="imprint-content">
|
|
<div class="legal-loading">Lade Impressum...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def get_legal_modal_js() -> str:
|
|
"""JavaScript für Legal Modal zurückgeben"""
|
|
return """
|
|
const legalModal = document.getElementById('legal-modal');
|
|
const legalModalClose = document.getElementById('legal-modal-close');
|
|
const legalTabs = document.querySelectorAll('.legal-tab');
|
|
const legalContents = document.querySelectorAll('.legal-content');
|
|
const btnLegal = document.getElementById('btn-legal');
|
|
|
|
// Imprint Modal
|
|
const imprintModal = document.getElementById('imprint-modal');
|
|
const imprintModalClose = document.getElementById('imprint-modal-close');
|
|
|
|
// Open legal modal from footer
|
|
function openLegalModal(tab = 'terms') {
|
|
legalModal.classList.add('active');
|
|
// Switch to specified tab
|
|
if (tab) {
|
|
legalTabs.forEach(t => t.classList.remove('active'));
|
|
legalContents.forEach(c => c.classList.remove('active'));
|
|
const targetTab = document.querySelector(`.legal-tab[data-tab="${tab}"]`);
|
|
if (targetTab) targetTab.classList.add('active');
|
|
document.getElementById(`legal-${tab}`)?.classList.add('active');
|
|
}
|
|
loadLegalDocuments();
|
|
}
|
|
|
|
// Open imprint modal from footer
|
|
function openImprintModal() {
|
|
imprintModal.classList.add('active');
|
|
loadImprintContent();
|
|
}
|
|
|
|
// Open legal modal
|
|
btnLegal?.addEventListener('click', async () => {
|
|
openLegalModal();
|
|
});
|
|
|
|
// Close legal modal
|
|
legalModalClose?.addEventListener('click', () => {
|
|
legalModal.classList.remove('active');
|
|
});
|
|
|
|
// Close imprint modal
|
|
imprintModalClose?.addEventListener('click', () => {
|
|
imprintModal.classList.remove('active');
|
|
});
|
|
|
|
// Close on background click
|
|
legalModal?.addEventListener('click', (e) => {
|
|
if (e.target === legalModal) {
|
|
legalModal.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
imprintModal?.addEventListener('click', (e) => {
|
|
if (e.target === imprintModal) {
|
|
imprintModal.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Tab switching
|
|
legalTabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const tabId = tab.dataset.tab;
|
|
legalTabs.forEach(t => t.classList.remove('active'));
|
|
legalContents.forEach(c => c.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
document.getElementById(`legal-${tabId}`)?.classList.add('active');
|
|
|
|
// Load cookie categories when switching to cookies tab
|
|
if (tabId === 'cookies') {
|
|
loadCookieCategories();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Load legal documents from consent service
|
|
async function loadLegalDocuments() {
|
|
const lang = document.getElementById('language-select')?.value || 'de';
|
|
|
|
// Load all documents in parallel
|
|
await Promise.all([
|
|
loadDocumentContent('terms', 'legal-terms-content', getDefaultTerms, lang),
|
|
loadDocumentContent('privacy', 'legal-privacy-content', getDefaultPrivacy, lang),
|
|
loadDocumentContent('community_guidelines', 'legal-community-content', getDefaultCommunityGuidelines, lang)
|
|
]);
|
|
}
|
|
|
|
// Load imprint content
|
|
async function loadImprintContent() {
|
|
const lang = document.getElementById('language-select')?.value || 'de';
|
|
await loadDocumentContent('imprint', 'imprint-content', getDefaultImprint, lang);
|
|
}
|
|
|
|
// Generic function to load document content
|
|
async function loadDocumentContent(docType, containerId, defaultFn, lang) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/consent/documents/${docType}/latest?language=${lang}`);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data.content) {
|
|
container.innerHTML = data.content;
|
|
return;
|
|
}
|
|
}
|
|
} catch(e) {
|
|
console.log(`Could not load ${docType}:`, e);
|
|
}
|
|
|
|
// Fallback to default
|
|
container.innerHTML = defaultFn(lang);
|
|
}
|
|
|
|
// Load cookie categories for the cookie settings tab
|
|
async function loadCookieCategories() {
|
|
const container = document.getElementById('cookie-categories-container');
|
|
if (!container) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/consent/cookies/categories');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const categories = data.categories || [];
|
|
|
|
if (categories.length === 0) {
|
|
container.innerHTML = getDefaultCookieCategories();
|
|
return;
|
|
}
|
|
|
|
// Get current preferences from localStorage
|
|
const savedPrefs = JSON.parse(localStorage.getItem('bp_cookie_consent') || '{}');
|
|
|
|
container.innerHTML = categories.map(cat => `
|
|
<label class="cookie-category">
|
|
<input type="checkbox" id="cookie-${cat.name}"
|
|
${cat.is_mandatory ? 'checked disabled' : (savedPrefs[cat.name] ? 'checked' : '')}>
|
|
<span>
|
|
<strong>${cat.display_name_de || cat.name}</strong>
|
|
${cat.is_mandatory ? ' (erforderlich)' : ''}
|
|
${cat.description_de ? ` - ${cat.description_de}` : ''}
|
|
</span>
|
|
</label>
|
|
`).join('');
|
|
} else {
|
|
container.innerHTML = getDefaultCookieCategories();
|
|
}
|
|
} catch(e) {
|
|
container.innerHTML = getDefaultCookieCategories();
|
|
}
|
|
}
|
|
|
|
function getDefaultCookieCategories() {
|
|
return `
|
|
<label class="cookie-category">
|
|
<input type="checkbox" checked disabled>
|
|
<span><strong>Notwendig</strong> (erforderlich) - Erforderlich für die Grundfunktionen</span>
|
|
</label>
|
|
<label class="cookie-category">
|
|
<input type="checkbox" id="cookie-functional">
|
|
<span><strong>Funktional</strong> - Erweiterte Funktionen und Personalisierung</span>
|
|
</label>
|
|
<label class="cookie-category">
|
|
<input type="checkbox" id="cookie-analytics">
|
|
<span><strong>Analyse</strong> - Hilft uns, die Nutzung zu verstehen</span>
|
|
</label>
|
|
`;
|
|
}
|
|
|
|
function getDefaultTerms(lang) {
|
|
const terms = {
|
|
de: '<h3>Allgemeine Geschäftsbedingungen</h3><p>Die BreakPilot-Plattform wird von der BreakPilot UG bereitgestellt.</p><p><strong>Nutzung:</strong> Die Plattform dient zur Erstellung und Verwaltung von Lernmaterialien für Bildungszwecke.</p><p><strong>Haftung:</strong> Die Nutzung erfolgt auf eigene Verantwortung.</p><p><strong>Änderungen:</strong> Wir behalten uns vor, diese AGB jederzeit zu ändern.</p>',
|
|
en: '<h3>Terms of Service</h3><p>The BreakPilot platform is provided by BreakPilot UG.</p><p><strong>Usage:</strong> The platform is designed for creating and managing learning materials for educational purposes.</p><p><strong>Liability:</strong> Use at your own risk.</p><p><strong>Changes:</strong> We reserve the right to modify these terms at any time.</p>'
|
|
};
|
|
return terms[lang] || terms.de;
|
|
}
|
|
|
|
function getDefaultPrivacy(lang) {
|
|
const privacy = {
|
|
de: '<h3>Datenschutzerklärung</h3><p><strong>Verantwortlicher:</strong> BreakPilot UG</p><p><strong>Erhobene Daten:</strong> Bei der Nutzung werden technische Daten (IP-Adresse, Browser-Typ) sowie von Ihnen eingegebene Inhalte verarbeitet.</p><p><strong>Zweck:</strong> Die Daten werden zur Bereitstellung der Plattform und zur Verbesserung unserer Dienste genutzt.</p><p><strong>Ihre Rechte (DSGVO):</strong></p><ul><li>Auskunftsrecht (Art. 15)</li><li>Recht auf Berichtigung (Art. 16)</li><li>Recht auf Löschung (Art. 17)</li><li>Recht auf Datenübertragbarkeit (Art. 20)</li></ul><p><strong>Kontakt:</strong> datenschutz@breakpilot.app</p>',
|
|
en: '<h3>Privacy Policy</h3><p><strong>Controller:</strong> BreakPilot UG</p><p><strong>Data Collected:</strong> Technical data (IP address, browser type) and content you provide are processed.</p><p><strong>Purpose:</strong> Data is used to provide the platform and improve our services.</p><p><strong>Your Rights (GDPR):</strong></p><ul><li>Right of access (Art. 15)</li><li>Right to rectification (Art. 16)</li><li>Right to erasure (Art. 17)</li><li>Right to data portability (Art. 20)</li></ul><p><strong>Contact:</strong> privacy@breakpilot.app</p>'
|
|
};
|
|
return privacy[lang] || privacy.de;
|
|
}
|
|
|
|
function getDefaultCommunityGuidelines(lang) {
|
|
const guidelines = {
|
|
de: '<h3>Community Guidelines</h3><p>Willkommen bei BreakPilot! Um eine positive und respektvolle Umgebung zu gewährleisten, bitten wir alle Nutzer, diese Richtlinien zu befolgen.</p><p><strong>Respektvoller Umgang:</strong> Behandeln Sie andere Nutzer mit Respekt und Höflichkeit.</p><p><strong>Keine illegalen Inhalte:</strong> Das Erstellen oder Teilen von illegalen Inhalten ist streng untersagt.</p><p><strong>Urheberrecht:</strong> Respektieren Sie das geistige Eigentum anderer. Verwenden Sie nur Inhalte, für die Sie die Rechte besitzen.</p><p><strong>Datenschutz:</strong> Teilen Sie keine persönlichen Daten anderer ohne deren ausdrückliche Zustimmung.</p><p><strong>Qualität:</strong> Bemühen Sie sich um qualitativ hochwertige Lerninhalte.</p><p>Verstöße gegen diese Richtlinien können zur Sperrung des Accounts führen.</p>',
|
|
en: '<h3>Community Guidelines</h3><p>Welcome to BreakPilot! To ensure a positive and respectful environment, we ask all users to follow these guidelines.</p><p><strong>Respectful Behavior:</strong> Treat other users with respect and courtesy.</p><p><strong>No Illegal Content:</strong> Creating or sharing illegal content is strictly prohibited.</p><p><strong>Copyright:</strong> Respect the intellectual property of others. Only use content you have rights to.</p><p><strong>Privacy:</strong> Do not share personal data of others without their explicit consent.</p><p><strong>Quality:</strong> Strive for high-quality learning content.</p><p>Violations of these guidelines may result in account suspension.</p>'
|
|
};
|
|
return guidelines[lang] || guidelines.de;
|
|
}
|
|
|
|
function getDefaultImprint(lang) {
|
|
const imprint = {
|
|
de: '<h3>Impressum</h3><p><strong>Angaben gemäß § 5 TMG:</strong></p><p>BreakPilot UG (haftungsbeschränkt)<br>Musterstraße 1<br>12345 Musterstadt<br>Deutschland</p><p><strong>Vertreten durch:</strong><br>Geschäftsführer: Max Mustermann</p><p><strong>Kontakt:</strong><br>Telefon: +49 (0) 123 456789<br>E-Mail: info@breakpilot.app</p><p><strong>Registereintrag:</strong><br>Eintragung im Handelsregister<br>Registergericht: Amtsgericht Musterstadt<br>Registernummer: HRB 12345</p><p><strong>Umsatzsteuer-ID:</strong><br>Umsatzsteuer-Identifikationsnummer gemäß § 27 a UStG: DE123456789</p><p><strong>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>',
|
|
en: '<h3>Legal Notice</h3><p><strong>Information according to § 5 TMG:</strong></p><p>BreakPilot UG (limited liability)<br>Musterstraße 1<br>12345 Musterstadt<br>Germany</p><p><strong>Represented by:</strong><br>Managing Director: Max Mustermann</p><p><strong>Contact:</strong><br>Phone: +49 (0) 123 456789<br>Email: info@breakpilot.app</p><p><strong>Register entry:</strong><br>Entry in the commercial register<br>Register court: Amtsgericht Musterstadt<br>Register number: HRB 12345</p><p><strong>VAT ID:</strong><br>VAT identification number according to § 27 a UStG: DE123456789</p><p><strong>Responsible for content according to § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>'
|
|
};
|
|
return imprint[lang] || imprint.de;
|
|
}
|
|
|
|
// Save cookie preferences
|
|
function saveCookiePreferences() {
|
|
const prefs = {};
|
|
const checkboxes = document.querySelectorAll('#cookie-categories-container input[type="checkbox"]');
|
|
checkboxes.forEach(cb => {
|
|
const name = cb.id.replace('cookie-', '');
|
|
if (name && !cb.disabled) {
|
|
prefs[name] = cb.checked;
|
|
}
|
|
});
|
|
localStorage.setItem('bp_cookie_consent', JSON.stringify(prefs));
|
|
localStorage.setItem('bp_cookie_consent_date', new Date().toISOString());
|
|
|
|
// TODO: Send to consent service if user is logged in
|
|
alert('Cookie-Einstellungen gespeichert!');
|
|
}
|
|
"""
|