""" Admin Panel Component - JavaScript Extracted from admin_panel.py for maintainability. Contains all JavaScript for the admin panel functionality including: - Document management - Version management - Cookie categories - DSB approval workflow - GPU infrastructure controls """ def get_admin_panel_js() -> str: """JavaScript fuer Admin Panel zurueckgeben""" return _get_modal_js() + _get_documents_js() + _get_versions_js() + \ _get_approval_workflow_js() + _get_compare_view_js() + \ _get_cookie_categories_js() + _get_gpu_controls_js() def _get_modal_js() -> str: """JavaScript fuer Modal-Handling""" return """ const adminModal = document.getElementById('admin-modal'); const adminModalClose = document.getElementById('admin-modal-close'); const adminTabs = document.querySelectorAll('.admin-tab'); const adminContents = document.querySelectorAll('.admin-content'); const btnAdmin = document.getElementById('btn-admin'); // Admin data cache let adminDocuments = []; let adminCookieCategories = []; // Open admin modal btnAdmin?.addEventListener('click', async () => { adminModal.classList.add('active'); await loadAdminDocuments(); await loadAdminCookieCategories(); populateDocumentSelect(); }); // Close admin modal adminModalClose?.addEventListener('click', () => { adminModal.classList.remove('active'); }); // Close on background click adminModal?.addEventListener('click', (e) => { if (e.target === adminModal) { adminModal.classList.remove('active'); } }); // Admin tab switching adminTabs.forEach(tab => { tab.addEventListener('click', () => { const tabId = tab.dataset.tab; adminTabs.forEach(t => t.classList.remove('active')); adminContents.forEach(c => c.classList.remove('active')); tab.classList.add('active'); document.getElementById(`admin-${tabId}`)?.classList.add('active'); // Load stats when stats tab is clicked if (tabId === 'stats') { loadAdminStats(); } }); }); """ def _get_documents_js() -> str: """JavaScript fuer Dokumentenverwaltung""" return """ // ========================================== // DOCUMENTS MANAGEMENT // ========================================== async function loadAdminDocuments() { const container = document.getElementById('admin-doc-table-container'); container.innerHTML = '
Lade Dokumente...
'; try { const res = await fetch('/api/consent/admin/documents'); if (!res.ok) throw new Error('Failed to load'); const data = await res.json(); adminDocuments = data.documents || []; renderDocumentsTable(); } catch(e) { container.innerHTML = '
Fehler beim Laden der Dokumente.
'; } } function renderDocumentsTable() { const container = document.getElementById('admin-doc-table-container'); // Alle Dokumente anzeigen const allDocs = adminDocuments; if (allDocs.length === 0) { container.innerHTML = '
Keine Dokumente vorhanden. Klicken Sie auf "+ Neues Dokument" um ein Dokument zu erstellen.
'; return; } const typeLabels = { 'terms': 'AGB', 'privacy': 'Datenschutz', 'cookies': 'Cookies', 'community': 'Community', 'imprint': 'Impressum' }; const html = ` ${allDocs.map(doc => ` `).join('')}
Typ Name Beschreibung Status Aktionen
${typeLabels[doc.type] || doc.type} ${doc.name} ${doc.description || '-'} ${doc.is_active ? 'Aktiv' : 'Inaktiv'} ${doc.is_mandatory ? 'Pflicht' : ''}
`; container.innerHTML = html; } function goToVersions(docId) { // Wechsle zum Versionen-Tab und wähle das Dokument aus const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]'); if (versionsTab) { versionsTab.click(); setTimeout(() => { const select = document.getElementById('admin-version-doc-select'); if (select) { select.value = docId; loadVersionsForDocument(); } }, 100); } } function showDocumentForm(doc = null) { const form = document.getElementById('admin-document-form'); const title = document.getElementById('admin-document-form-title'); if (doc) { title.textContent = 'Dokument bearbeiten'; document.getElementById('admin-document-id').value = doc.id; document.getElementById('admin-document-type').value = doc.type; document.getElementById('admin-document-name').value = doc.name; document.getElementById('admin-document-description').value = doc.description || ''; document.getElementById('admin-document-mandatory').checked = doc.is_mandatory; } else { title.textContent = 'Neues Dokument erstellen'; document.getElementById('admin-document-id').value = ''; document.getElementById('admin-document-type').value = ''; document.getElementById('admin-document-name').value = ''; document.getElementById('admin-document-description').value = ''; document.getElementById('admin-document-mandatory').checked = true; } form.style.display = 'block'; } function hideDocumentForm() { document.getElementById('admin-document-form').style.display = 'none'; } function editDocument(docId) { const doc = adminDocuments.find(d => d.id === docId); if (doc) showDocumentForm(doc); } async function saveDocument() { const docId = document.getElementById('admin-document-id').value; const docType = document.getElementById('admin-document-type').value; const docName = document.getElementById('admin-document-name').value; if (!docType || !docName) { alert('Bitte füllen Sie alle Pflichtfelder aus (Typ und Name).'); return; } const data = { type: docType, name: docName, description: document.getElementById('admin-document-description').value || null, is_mandatory: document.getElementById('admin-document-mandatory').checked }; try { const url = docId ? `/api/consent/admin/documents/${docId}` : '/api/consent/admin/documents'; const method = docId ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error('Failed to save'); hideDocumentForm(); await loadAdminDocuments(); populateDocumentSelect(); alert('Dokument gespeichert!'); } catch(e) { alert('Fehler beim Speichern: ' + e.message); } } async function deleteDocument(docId) { if (!confirm('Dokument wirklich deaktivieren?')) return; try { const res = await fetch(`/api/consent/admin/documents/${docId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); await loadAdminDocuments(); populateDocumentSelect(); alert('Dokument deaktiviert!'); } catch(e) { alert('Fehler: ' + e.message); } } """ def _get_versions_js() -> str: """JavaScript fuer Versionsverwaltung""" return """ // ========================================== // VERSIONS MANAGEMENT // ========================================== function populateDocumentSelect() { const select = document.getElementById('admin-version-doc-select'); const uniqueDocs = [...new Map(adminDocuments.map(d => [d.type, d])).values()]; select.innerHTML = '' + adminDocuments.filter(d => d.is_active).map(doc => `` ).join(''); } async function loadVersionsForDocument() { const docId = document.getElementById('admin-version-doc-select').value; const container = document.getElementById('admin-version-table-container'); const btnNew = document.getElementById('btn-new-version'); if (!docId) { container.innerHTML = '
Wählen Sie ein Dokument aus.
'; btnNew.disabled = true; return; } btnNew.disabled = false; container.innerHTML = '
Lade Versionen...
'; try { const res = await fetch(`/api/consent/admin/documents/${docId}/versions`); if (!res.ok) throw new Error('Failed to load'); const data = await res.json(); renderVersionsTable(data.versions || []); } catch(e) { container.innerHTML = '
Fehler beim Laden der Versionen.
'; } } function renderVersionsTable(versions) { const container = document.getElementById('admin-version-table-container'); if (versions.length === 0) { container.innerHTML = '
Keine Versionen vorhanden.
'; return; } const getStatusBadge = (status) => { const statusLabels = { 'draft': 'Entwurf', 'review': 'In Prüfung', 'approved': 'Genehmigt', 'rejected': 'Abgelehnt', 'scheduled': 'Geplant', 'published': 'Veröffentlicht', 'archived': 'Archiviert' }; return statusLabels[status] || status; }; const formatScheduledDate = (isoDate) => { if (!isoDate) return ''; const date = new Date(isoDate); return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const html = ` ${versions.map(v => ` `).join('')}
Version Sprache Titel Status Aktionen
${v.version} ${v.language.toUpperCase()} ${v.title} ${getStatusBadge(v.status)} ${v.scheduled_publish_at ? `
Geplant: ${formatScheduledDate(v.scheduled_publish_at)}` : ''}
${v.status === 'draft' ? ` ` : ''} ${v.status === 'review' ? ` ` : ''} ${v.status === 'rejected' ? ` ` : ''} ${v.status === 'scheduled' ? ` Wartet auf Veröffentlichung ` : ''} ${v.status === 'approved' ? ` ` : ''} ${v.status === 'published' ? ` ` : ''}
`; container.innerHTML = html; } function showVersionForm() { const form = document.getElementById('admin-version-form'); document.getElementById('admin-version-id').value = ''; document.getElementById('admin-version-number').value = ''; document.getElementById('admin-version-lang').value = 'de'; document.getElementById('admin-version-title').value = ''; document.getElementById('admin-version-summary').value = ''; document.getElementById('admin-version-content').value = ''; // Clear rich text editor const editor = document.getElementById('admin-version-editor'); if (editor) { editor.innerHTML = ''; document.getElementById('editor-char-count').textContent = '0 Zeichen'; } form.classList.add('active'); } function hideVersionForm() { document.getElementById('admin-version-form').classList.remove('active'); } async function editVersion(versionId) { // Lade die Version und fülle das Formular const docId = document.getElementById('admin-version-doc-select').value; try { const res = await fetch(`/api/consent/admin/documents/${docId}/versions`); if (!res.ok) throw new Error('Failed to load versions'); const data = await res.json(); const version = (data.versions || []).find(v => v.id === versionId); if (!version) { alert('Version nicht gefunden'); return; } // Formular öffnen und Daten einfügen const form = document.getElementById('admin-version-form'); document.getElementById('admin-version-id').value = version.id; document.getElementById('admin-version-number').value = version.version; document.getElementById('admin-version-lang').value = version.language; document.getElementById('admin-version-title').value = version.title; document.getElementById('admin-version-summary').value = version.summary || ''; // Rich-Text-Editor mit Inhalt füllen const editor = document.getElementById('admin-version-editor'); if (editor) { editor.innerHTML = version.content || ''; const charCount = editor.textContent.length; document.getElementById('editor-char-count').textContent = charCount + ' Zeichen'; } document.getElementById('admin-version-content').value = version.content || ''; form.classList.add('active'); } catch(e) { alert('Fehler beim Laden der Version: ' + e.message); } } async function saveVersion() { const docId = document.getElementById('admin-version-doc-select').value; const versionId = document.getElementById('admin-version-id').value; // Get content from rich text editor const editor = document.getElementById('admin-version-editor'); const content = editor ? editor.innerHTML : document.getElementById('admin-version-content').value; const data = { document_id: docId, version: document.getElementById('admin-version-number').value, language: document.getElementById('admin-version-lang').value, title: document.getElementById('admin-version-title').value, summary: document.getElementById('admin-version-summary').value, content: content }; try { const url = versionId ? `/api/consent/admin/versions/${versionId}` : '/api/consent/admin/versions'; const method = versionId ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error('Failed to save'); hideVersionForm(); await loadVersionsForDocument(); alert('Version gespeichert!'); } catch(e) { alert('Fehler beim Speichern: ' + e.message); } } async function publishVersion(versionId) { if (!confirm('Version wirklich veröffentlichen?')) return; try { const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to publish'); await loadVersionsForDocument(); alert('Version veröffentlicht!'); } catch(e) { alert('Fehler: ' + e.message); } } async function archiveVersion(versionId) { if (!confirm('Version wirklich archivieren?')) return; try { const res = await fetch(`/api/consent/admin/versions/${versionId}/archive`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to archive'); await loadVersionsForDocument(); alert('Version archiviert!'); } catch(e) { alert('Fehler: ' + e.message); } } async function deleteVersion(versionId) { if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei und kann erneut verwendet werden.\\n\\nDiese Aktion kann nicht rückgängig gemacht werden!')) return; try { const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen'); } await loadVersionsForDocument(); alert('Version wurde dauerhaft gelöscht.'); } catch(e) { alert('Fehler: ' + e.message); } } """ def _get_approval_workflow_js() -> str: """JavaScript fuer DSB Approval Workflow""" return """ // ========================================== // DSB APPROVAL WORKFLOW // ========================================== async function submitForReview(versionId) { if (!confirm('Version zur DSB-Prüfung einreichen?')) return; try { const res = await fetch(`/api/consent/admin/versions/${versionId}/submit-review`, { method: 'POST' }); if (!res.ok) { const data = await res.json(); throw new Error(data.detail?.error || 'Einreichung fehlgeschlagen'); } await loadVersionsForDocument(); alert('Version wurde zur Prüfung eingereicht!'); } catch(e) { alert('Fehler: ' + e.message); } } // Dialog für Genehmigung mit Veröffentlichungszeitpunkt let approvalVersionId = null; function showApprovalDialog(versionId) { approvalVersionId = versionId; const dialog = document.getElementById('approval-dialog'); // Setze Minimum-Datum auf morgen const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(0, 0, 0, 0); document.getElementById('approval-date').min = tomorrow.toISOString().split('T')[0]; document.getElementById('approval-date').value = ''; document.getElementById('approval-time').value = '00:00'; document.getElementById('approval-comment').value = ''; dialog.classList.add('active'); } function hideApprovalDialog() { document.getElementById('approval-dialog').classList.remove('active'); approvalVersionId = null; } async function submitApproval() { if (!approvalVersionId) return; const dateInput = document.getElementById('approval-date').value; const timeInput = document.getElementById('approval-time').value; const comment = document.getElementById('approval-comment').value; let scheduledPublishAt = null; if (dateInput) { // Kombiniere Datum und Zeit zu ISO 8601 const datetime = new Date(dateInput + 'T' + (timeInput || '00:00') + ':00'); scheduledPublishAt = datetime.toISOString(); } try { const body = { comment: comment || '' }; if (scheduledPublishAt) { body.scheduled_publish_at = scheduledPublishAt; } const res = await fetch(`/api/consent/admin/versions/${approvalVersionId}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!res.ok) { const data = await res.json(); throw new Error(data.detail?.error || data.detail || 'Genehmigung fehlgeschlagen'); } hideApprovalDialog(); await loadVersionsForDocument(); if (scheduledPublishAt) { const date = new Date(scheduledPublishAt); alert('Version genehmigt! Geplante Veröffentlichung: ' + date.toLocaleString('de-DE')); } else { alert('Version genehmigt! Sie kann jetzt manuell veröffentlicht werden.'); } } catch(e) { alert('Fehler: ' + e.message); } } // Alte Funktion für Rückwärtskompatibilität async function approveVersion(versionId) { showApprovalDialog(versionId); } async function rejectVersion(versionId) { const comment = prompt('Begründung für Ablehnung (erforderlich):'); if (!comment) { alert('Eine Begründung ist erforderlich.'); return; } try { const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ comment: comment }) }); if (!res.ok) { const data = await res.json(); throw new Error(data.detail?.error || data.detail || 'Ablehnung fehlgeschlagen'); } await loadVersionsForDocument(); alert('Version abgelehnt und zurück in Entwurf-Status versetzt.'); } catch(e) { alert('Fehler: ' + e.message); } } async function showApprovalHistory(versionId) { try { const res = await fetch(`/api/consent/admin/versions/${versionId}/approval-history`); if (!res.ok) throw new Error('Historie konnte nicht geladen werden'); const data = await res.json(); const history = data.approval_history || []; const content = history.length === 0 ? '

Keine Genehmigungshistorie vorhanden.

' : ` ${history.map(h => ` `).join('')}
Aktion Benutzer Kommentar Datum
${h.action} ${h.approver || h.name || '-'} ${h.comment || '-'} ${new Date(h.created_at).toLocaleString('de-DE')}
`; showCustomModal('Genehmigungsverlauf', content, [ { text: 'Schließen', onClick: () => hideCustomModal() } ]); } catch(e) { alert('Fehler: ' + e.message); } } // Custom Modal Functions function showCustomModal(title, content, buttons = []) { let modal = document.getElementById('custom-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'custom-modal'; modal.className = 'modal-overlay'; document.body.appendChild(modal); } modal.innerHTML = ` `; modal.classList.add('active'); } function hideCustomModal() { const modal = document.getElementById('custom-modal'); if (modal) modal.classList.remove('active'); } """ def _get_compare_view_js() -> str: """JavaScript fuer Version Compare View""" return """ // Store current compare version for actions let currentCompareVersionId = null; let currentCompareVersionStatus = null; let currentCompareDocId = null; async function showCompareView(versionId) { try { const res = await fetch(`/api/consent/admin/versions/${versionId}/compare`); if (!res.ok) throw new Error('Vergleich konnte nicht geladen werden'); const data = await res.json(); const currentVersion = data.current_version; const publishedVersion = data.published_version; const history = data.approval_history || []; // Store version info for actions currentCompareVersionId = versionId; currentCompareVersionStatus = currentVersion.status; currentCompareDocId = currentVersion.document_id; // Update header info document.getElementById('compare-published-info').textContent = publishedVersion ? `${publishedVersion.title} (v${publishedVersion.version})` : 'Keine Version'; document.getElementById('compare-draft-info').textContent = `${currentVersion.title} (v${currentVersion.version})`; document.getElementById('compare-published-version').textContent = publishedVersion ? `v${publishedVersion.version}` : ''; document.getElementById('compare-draft-version').textContent = `v${currentVersion.version} - ${currentVersion.status}`; // Populate content panels const leftPanel = document.getElementById('compare-content-left'); const rightPanel = document.getElementById('compare-content-right'); leftPanel.innerHTML = publishedVersion ? publishedVersion.content : '
Keine veröffentlichte Version vorhanden
'; rightPanel.innerHTML = currentVersion.content || '
Kein Inhalt
'; // Populate history const historyContainer = document.getElementById('compare-history-container'); if (history.length > 0) { historyContainer.innerHTML = `
Genehmigungsverlauf
${history.map(h => ` ${h.action} von ${h.approver || 'System'} (${new Date(h.created_at).toLocaleString('de-DE')}) ${h.comment ? ': ' + h.comment : ''} `).join(' | ')}
`; } else { historyContainer.innerHTML = ''; } // Render action buttons based on status renderCompareActions(currentVersion.status, versionId); // Setup synchronized scrolling setupSyncScroll(leftPanel, rightPanel); // Show the overlay document.getElementById('version-compare-view').classList.add('active'); document.body.style.overflow = 'hidden'; } catch(e) { alert('Fehler beim Laden des Vergleichs: ' + e.message); } } function renderCompareActions(status, versionId) { const actionsContainer = document.getElementById('compare-actions-container'); let buttons = ''; // Edit button - available for draft, review, and rejected if (status === 'draft' || status === 'review' || status === 'rejected') { buttons += ``; } // Status-specific actions if (status === 'draft') { buttons += ``; } if (status === 'review') { buttons += ``; buttons += ``; } if (status === 'approved') { buttons += ``; } // Delete button for draft/rejected if (status === 'draft' || status === 'rejected') { buttons += ``; } actionsContainer.innerHTML = buttons; } async function editVersionFromCompare(versionId) { // Store the doc ID before closing compare view const docId = currentCompareDocId; // Close compare view hideCompareView(); // Switch to versions tab const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]'); if (versionsTab) { versionsTab.click(); } // Wait a moment for the tab to become active await new Promise(resolve => setTimeout(resolve, 150)); // Ensure document select is populated populateDocumentSelect(); // Set the document select if we have the doc ID if (docId) { const select = document.getElementById('admin-version-doc-select'); if (select) { select.value = docId; // Load versions for this document await loadVersionsForDocument(); } } // Now load the version data directly and open the form try { const res = await fetch(`/api/consent/admin/documents/${docId}/versions`); if (!res.ok) throw new Error('Failed to load versions'); const data = await res.json(); const version = (data.versions || []).find(v => v.id === versionId); if (!version) { alert('Version nicht gefunden'); return; } // Open the form and fill with version data const form = document.getElementById('admin-version-form'); document.getElementById('admin-version-id').value = version.id; document.getElementById('admin-version-number').value = version.version; document.getElementById('admin-version-lang').value = version.language; document.getElementById('admin-version-title').value = version.title; document.getElementById('admin-version-summary').value = version.summary || ''; // Fill rich text editor with content const editor = document.getElementById('admin-version-editor'); if (editor) { editor.innerHTML = version.content || ''; const charCount = editor.textContent.length; document.getElementById('editor-char-count').textContent = charCount + ' Zeichen'; } document.getElementById('admin-version-content').value = version.content || ''; form.classList.add('active'); } catch(e) { alert('Fehler beim Laden der Version: ' + e.message); } } async function submitForReviewFromCompare(versionId) { await submitForReview(versionId); hideCompareView(); await loadVersionsForDocument(); } async function approveVersionFromCompare(versionId) { const comment = prompt('Kommentar zur Genehmigung (optional):'); try { const res = await fetch(`/api/consent/admin/versions/${versionId}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ comment: comment || '' }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail?.error || err.error || 'Genehmigung fehlgeschlagen'); } alert('Version genehmigt!'); hideCompareView(); await loadVersionsForDocument(); } catch(e) { alert('Fehler: ' + e.message); } } async function rejectVersionFromCompare(versionId) { const comment = prompt('Begründung für die Ablehnung (erforderlich):'); if (!comment) { alert('Eine Begründung ist erforderlich.'); return; } try { const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ comment: comment }) }); if (!res.ok) throw new Error('Ablehnung fehlgeschlagen'); alert('Version abgelehnt. Der Autor kann sie überarbeiten.'); hideCompareView(); await loadVersionsForDocument(); } catch(e) { alert('Fehler: ' + e.message); } } async function publishVersionFromCompare(versionId) { if (!confirm('Version wirklich veröffentlichen?')) return; try { const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' }); if (!res.ok) throw new Error('Veröffentlichung fehlgeschlagen'); alert('Version veröffentlicht!'); hideCompareView(); await loadVersionsForDocument(); } catch(e) { alert('Fehler: ' + e.message); } } async function deleteVersionFromCompare(versionId) { if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei.')) return; try { const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen'); } alert('Version gelöscht!'); hideCompareView(); await loadVersionsForDocument(); } catch(e) { alert('Fehler: ' + e.message); } } function hideCompareView() { document.getElementById('version-compare-view').classList.remove('active'); document.body.style.overflow = ''; // Remove scroll listeners const leftPanel = document.getElementById('compare-content-left'); const rightPanel = document.getElementById('compare-content-right'); if (leftPanel) leftPanel.onscroll = null; if (rightPanel) rightPanel.onscroll = null; } // Synchronized scrolling between two panels let syncScrollActive = false; function setupSyncScroll(leftPanel, rightPanel) { // Remove any existing listeners first leftPanel.onscroll = null; rightPanel.onscroll = null; // Flag to prevent infinite scroll loops let isScrolling = false; rightPanel.onscroll = function() { if (isScrolling) return; isScrolling = true; // Calculate scroll percentage const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight); // Apply same percentage to left panel const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight; leftPanel.scrollTop = rightScrollPercent * leftMaxScroll; setTimeout(() => { isScrolling = false; }, 10); }; leftPanel.onscroll = function() { if (isScrolling) return; isScrolling = true; // Calculate scroll percentage const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight); // Apply same percentage to right panel const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight; rightPanel.scrollTop = leftScrollPercent * rightMaxScroll; setTimeout(() => { isScrolling = false; }, 10); }; } """ def _get_cookie_categories_js() -> str: """JavaScript fuer Cookie-Kategorien Verwaltung""" return """ // ========================================== // COOKIE CATEGORIES MANAGEMENT // ========================================== async function loadAdminCookieCategories() { const container = document.getElementById('admin-cookie-table-container'); container.innerHTML = '
Lade Cookie-Kategorien...
'; try { const res = await fetch('/api/consent/admin/cookies/categories'); if (!res.ok) throw new Error('Failed to load'); const data = await res.json(); adminCookieCategories = data.categories || []; renderCookieCategoriesTable(); } catch(e) { container.innerHTML = '
Fehler beim Laden der Kategorien.
'; } } function renderCookieCategoriesTable() { const container = document.getElementById('admin-cookie-table-container'); if (adminCookieCategories.length === 0) { container.innerHTML = '
Keine Cookie-Kategorien vorhanden.
'; return; } const html = ` ${adminCookieCategories.map(cat => ` `).join('')}
Name Anzeigename (DE) Typ Aktionen
${cat.name} ${cat.display_name_de} ${cat.is_mandatory ? 'Notwendig' : 'Optional'} ${!cat.is_mandatory ? `` : ''}
`; container.innerHTML = html; } function showCookieForm(cat = null) { const form = document.getElementById('admin-cookie-form'); if (cat) { document.getElementById('admin-cookie-id').value = cat.id; document.getElementById('admin-cookie-name').value = cat.name; document.getElementById('admin-cookie-display-de').value = cat.display_name_de; document.getElementById('admin-cookie-display-en').value = cat.display_name_en || ''; document.getElementById('admin-cookie-desc-de').value = cat.description_de || ''; document.getElementById('admin-cookie-mandatory').checked = cat.is_mandatory; } else { document.getElementById('admin-cookie-id').value = ''; document.getElementById('admin-cookie-name').value = ''; document.getElementById('admin-cookie-display-de').value = ''; document.getElementById('admin-cookie-display-en').value = ''; document.getElementById('admin-cookie-desc-de').value = ''; document.getElementById('admin-cookie-mandatory').checked = false; } form.classList.add('active'); } function hideCookieForm() { document.getElementById('admin-cookie-form').classList.remove('active'); } function editCookieCategory(catId) { const cat = adminCookieCategories.find(c => c.id === catId); if (cat) showCookieForm(cat); } async function saveCookieCategory() { const catId = document.getElementById('admin-cookie-id').value; const data = { name: document.getElementById('admin-cookie-name').value, display_name_de: document.getElementById('admin-cookie-display-de').value, display_name_en: document.getElementById('admin-cookie-display-en').value, description_de: document.getElementById('admin-cookie-desc-de').value, is_mandatory: document.getElementById('admin-cookie-mandatory').checked }; try { const url = catId ? `/api/consent/admin/cookies/categories/${catId}` : '/api/consent/admin/cookies/categories'; const method = catId ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error('Failed to save'); hideCookieForm(); await loadAdminCookieCategories(); alert('Kategorie gespeichert!'); } catch(e) { alert('Fehler beim Speichern: ' + e.message); } } async function deleteCookieCategory(catId) { if (!confirm('Kategorie wirklich löschen?')) return; try { const res = await fetch(`/api/consent/admin/cookies/categories/${catId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); await loadAdminCookieCategories(); alert('Kategorie gelöscht!'); } catch(e) { alert('Fehler: ' + e.message); } } """ def _get_gpu_controls_js() -> str: """JavaScript fuer GPU Infrastructure Controls""" return """ // ========================================== // GPU INFRASTRUCTURE CONTROLS // ========================================== let gpuStatusInterval = null; async function gpuFetch(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers, }; try { const response = await fetch(`/infra/vast${endpoint}`, { ...options, headers, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(error.detail || 'Request failed'); } return await response.json(); } catch (error) { console.error('GPU API Error:', error); throw error; } } async function gpuRefreshStatus() { const btnRefresh = document.querySelector('.gpu-btn-refresh'); const btnStart = document.getElementById('gpu-btn-start'); const btnStop = document.getElementById('gpu-btn-stop'); try { if (btnRefresh) btnRefresh.disabled = true; const status = await gpuFetch('/status'); // Update status badge const badge = document.getElementById('gpu-status-badge'); const statusText = document.getElementById('gpu-status-text'); const statusDot = badge.querySelector('.gpu-status-dot'); badge.className = 'gpu-status-badge ' + status.status; statusText.textContent = formatGpuStatus(status.status); statusDot.className = 'gpu-status-dot ' + status.status; // Update info cards document.getElementById('gpu-name').textContent = status.gpu_name || '-'; document.getElementById('gpu-dph').textContent = status.dph_total ? `$${status.dph_total.toFixed(2)}/h` : '-'; // Update endpoint const endpointDiv = document.getElementById('gpu-endpoint'); const endpointUrl = document.getElementById('gpu-endpoint-url'); if (status.endpoint_base_url && status.status === 'running') { endpointDiv.style.display = 'block'; endpointDiv.className = 'gpu-endpoint-url active'; endpointUrl.textContent = status.endpoint_base_url; } else { endpointDiv.style.display = 'none'; } // Update shutdown warning const warningDiv = document.getElementById('gpu-shutdown-warning'); const shutdownMinutes = document.getElementById('gpu-shutdown-minutes'); if (status.auto_shutdown_in_minutes !== null && status.status === 'running') { warningDiv.style.display = 'flex'; shutdownMinutes.textContent = status.auto_shutdown_in_minutes; } else { warningDiv.style.display = 'none'; } // Update totals document.getElementById('gpu-total-runtime').textContent = formatGpuRuntime(status.total_runtime_hours || 0); document.getElementById('gpu-total-cost-all').textContent = `$${(status.total_cost_usd || 0).toFixed(2)}`; // Update buttons const isRunning = status.status === 'running'; const isLoading = ['loading', 'scheduling', 'creating'].includes(status.status); btnStart.disabled = isRunning || isLoading; btnStop.disabled = !isRunning; if (isLoading) { btnStart.innerHTML = ' Startet...'; } else { btnStart.innerHTML = ` Starten `; } // Load audit log loadGpuAuditLog(); } catch (error) { console.error('Failed to refresh GPU status:', error); document.getElementById('gpu-status-text').textContent = 'Fehler'; document.getElementById('gpu-status-badge').className = 'gpu-status-badge error'; } finally { if (btnRefresh) btnRefresh.disabled = false; } } async function gpuPowerOn() { const btnStart = document.getElementById('gpu-btn-start'); const btnStop = document.getElementById('gpu-btn-stop'); if (!confirm('GPU-Instanz starten? Es fallen Kosten an solange sie laeuft.')) { return; } try { btnStart.disabled = true; btnStop.disabled = true; btnStart.innerHTML = ' Startet...'; const result = await gpuFetch('/power/on', { method: 'POST', body: JSON.stringify({ wait_for_health: true, }), }); alert('GPU gestartet: ' + (result.message || result.status)); await gpuRefreshStatus(); startGpuStatusPolling(); } catch (error) { alert('GPU Start fehlgeschlagen: ' + error.message); await gpuRefreshStatus(); } } async function gpuPowerOff() { const btnStart = document.getElementById('gpu-btn-start'); const btnStop = document.getElementById('gpu-btn-stop'); if (!confirm('GPU-Instanz stoppen?')) { return; } try { btnStart.disabled = true; btnStop.disabled = true; btnStop.innerHTML = ' Stoppt...'; const result = await gpuFetch('/power/off', { method: 'POST', body: JSON.stringify({}), }); const msg = result.session_runtime_minutes ? `GPU gestoppt. Session: ${result.session_runtime_minutes.toFixed(1)} min, $${result.session_cost_usd.toFixed(3)}` : 'GPU gestoppt'; alert(msg); await gpuRefreshStatus(); stopGpuStatusPolling(); } catch (error) { alert('GPU Stop fehlgeschlagen: ' + error.message); await gpuRefreshStatus(); } finally { btnStop.innerHTML = ` Stoppen `; } } async function loadGpuAuditLog() { try { const entries = await gpuFetch('/audit?limit=20'); const container = document.getElementById('gpu-audit-log'); if (!entries || entries.length === 0) { container.innerHTML = '
Keine Eintraege
'; return; } container.innerHTML = entries.map(entry => { const time = new Date(entry.ts).toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', }); return `
${time} ${entry.event}
`; }).join(''); } catch (error) { console.error('Failed to load audit log:', error); } } function formatGpuStatus(status) { const statusMap = { 'running': 'Laeuft', 'stopped': 'Gestoppt', 'exited': 'Beendet', 'loading': 'Laedt...', 'scheduling': 'Plane...', 'creating': 'Erstelle...', 'unconfigured': 'Nicht konfiguriert', 'not_found': 'Nicht gefunden', 'unknown': 'Unbekannt', 'error': 'Fehler', }; return statusMap[status] || status; } function formatGpuRuntime(hours) { const h = Math.floor(hours); const m = Math.round((hours - h) * 60); return `${h}h ${m}m`; } function startGpuStatusPolling() { stopGpuStatusPolling(); gpuStatusInterval = setInterval(gpuRefreshStatus, 30000); } function stopGpuStatusPolling() { if (gpuStatusInterval) { clearInterval(gpuStatusInterval); gpuStatusInterval = null; } } // Initialize GPU tab when clicked document.addEventListener('DOMContentLoaded', () => { const gpuTab = document.querySelector('.admin-tab[data-tab="gpu"]'); if (gpuTab) { gpuTab.addEventListener('click', () => { gpuRefreshStatus(); startGpuStatusPolling(); }); } }); """