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:
22
backend/frontend/components/admin_panel/__init__.py
Normal file
22
backend/frontend/components/admin_panel/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Admin Panel Module
|
||||
|
||||
Modular structure for the Admin Panel component.
|
||||
Contains CSS, HTML, and JavaScript for the consent admin panel.
|
||||
|
||||
Modular Refactoring (2026-02-03):
|
||||
- Split into sub-modules for maintainability
|
||||
- Original file: admin_panel.py (2,977 lines)
|
||||
- Now split into: styles.py, markup.py, scripts.py
|
||||
"""
|
||||
|
||||
# Re-export the main functions for backward compatibility
|
||||
from .styles import get_admin_panel_css
|
||||
from .markup import get_admin_panel_html
|
||||
from .scripts import get_admin_panel_js
|
||||
|
||||
__all__ = [
|
||||
"get_admin_panel_css",
|
||||
"get_admin_panel_html",
|
||||
"get_admin_panel_js",
|
||||
]
|
||||
831
backend/frontend/components/admin_panel/markup.py
Normal file
831
backend/frontend/components/admin_panel/markup.py
Normal file
@@ -0,0 +1,831 @@
|
||||
"""
|
||||
Admin Panel Component - HTML Markup
|
||||
|
||||
Extracted from admin_panel.py for maintainability.
|
||||
Contains all HTML templates for the admin panel modal, tabs, forms, and dialogs.
|
||||
"""
|
||||
|
||||
|
||||
def get_admin_panel_html() -> str:
|
||||
"""HTML fuer Admin Panel zurueckgeben"""
|
||||
return """
|
||||
<!-- Admin Panel Modal -->
|
||||
<div id="admin-modal" class="admin-modal">
|
||||
<div class="admin-modal-content">
|
||||
<div class="admin-modal-header">
|
||||
<h2><span>⚙️</span> Consent Admin Panel</h2>
|
||||
<button id="admin-modal-close" class="legal-modal-close">×</button>
|
||||
</div>
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab active" data-tab="documents">Dokumente</button>
|
||||
<button class="admin-tab" data-tab="versions">Versionen</button>
|
||||
<button class="admin-tab" data-tab="cookies">Cookie-Kategorien</button>
|
||||
<button class="admin-tab" data-tab="stats">Statistiken</button>
|
||||
<button class="admin-tab" data-tab="emails">E-Mail Vorlagen</button>
|
||||
<button class="admin-tab" data-tab="dsms">DSMS</button>
|
||||
<button class="admin-tab" data-tab="gpu">GPU Infra</button>
|
||||
</div>
|
||||
<div class="admin-body">
|
||||
<!-- Documents Tab -->
|
||||
<div id="admin-documents" class="admin-content active">
|
||||
<div class="admin-toolbar">
|
||||
<div class="admin-toolbar-left">
|
||||
<input type="text" class="admin-search" placeholder="Dokumente suchen..." id="admin-doc-search">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="showDocumentForm()">+ Neues Dokument</button>
|
||||
</div>
|
||||
|
||||
<!-- Document Creation Form -->
|
||||
<div id="admin-document-form" class="admin-form" style="display: none;">
|
||||
<h3 class="admin-form-title" id="admin-document-form-title">Neues Dokument erstellen</h3>
|
||||
<input type="hidden" id="admin-document-id">
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Dokumenttyp *</label>
|
||||
<select class="admin-form-select" id="admin-document-type">
|
||||
<option value="">-- Typ auswählen --</option>
|
||||
<option value="terms">AGB (Allgemeine Geschäftsbedingungen)</option>
|
||||
<option value="privacy">Datenschutzerklärung</option>
|
||||
<option value="cookies">Cookie-Richtlinie</option>
|
||||
<option value="community">Community Guidelines</option>
|
||||
<option value="imprint">Impressum</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Name *</label>
|
||||
<input type="text" class="admin-form-input" id="admin-document-name" placeholder="z.B. Allgemeine Geschäftsbedingungen">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Beschreibung</label>
|
||||
<input type="text" class="admin-form-input" id="admin-document-description" placeholder="Kurze Beschreibung des Dokuments">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">
|
||||
<input type="checkbox" id="admin-document-mandatory" style="margin-right: 8px;">
|
||||
Pflichtdokument (Nutzer müssen zustimmen)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideDocumentForm()">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="saveDocument()">Dokument erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-doc-table-container">
|
||||
<div class="admin-loading">Lade Dokumente...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Versions Tab -->
|
||||
<div id="admin-versions" class="admin-content">
|
||||
<div class="admin-toolbar">
|
||||
<div class="admin-toolbar-left">
|
||||
<select class="admin-form-select" id="admin-version-doc-select" onchange="loadVersionsForDocument()">
|
||||
<option value="">-- Dokument auswählen --</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="showVersionForm()" id="btn-new-version" disabled>+ Neue Version</button>
|
||||
</div>
|
||||
|
||||
<div id="admin-version-form" class="admin-form">
|
||||
<h3 class="admin-form-title" id="admin-version-form-title">Neue Version erstellen</h3>
|
||||
<input type="hidden" id="admin-version-id">
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Version *</label>
|
||||
<input type="text" class="admin-form-input" id="admin-version-number" placeholder="z.B. 1.0.0">
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Sprache *</label>
|
||||
<select class="admin-form-select" id="admin-version-lang">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Titel *</label>
|
||||
<input type="text" class="admin-form-input" id="admin-version-title" placeholder="Titel der Version">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Zusammenfassung</label>
|
||||
<input type="text" class="admin-form-input" id="admin-version-summary" placeholder="Kurze Zusammenfassung der Änderungen">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Inhalt *</label>
|
||||
<div class="editor-container">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('bold')" title="Fett (Strg+B)"><b>B</b></button>
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('italic')" title="Kursiv (Strg+I)"><i>I</i></button>
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('underline')" title="Unterstrichen (Strg+U)"><u>U</u></button>
|
||||
</div>
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="formatBlock('h1')" title="Überschrift 1">H1</button>
|
||||
<button type="button" class="editor-btn" onclick="formatBlock('h2')" title="Überschrift 2">H2</button>
|
||||
<button type="button" class="editor-btn" onclick="formatBlock('h3')" title="Überschrift 3">H3</button>
|
||||
<button type="button" class="editor-btn" onclick="formatBlock('p')" title="Absatz">P</button>
|
||||
</div>
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('insertUnorderedList')" title="Aufzählung">• Liste</button>
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('insertOrderedList')" title="Nummerierung">1. Liste</button>
|
||||
</div>
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="insertLink()" title="Link einfügen">🔗</button>
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('formatBlock', 'blockquote')" title="Zitat">❝</button>
|
||||
<button type="button" class="editor-btn" onclick="formatDoc('insertHorizontalRule')" title="Trennlinie">—</button>
|
||||
</div>
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn editor-btn-upload" onclick="document.getElementById('word-upload').click()" title="Word-Dokument importieren">📄 Word Import</button>
|
||||
<input type="file" id="word-upload" class="word-upload-input" accept=".docx,.doc" onchange="handleWordUpload(event)">
|
||||
</div>
|
||||
</div>
|
||||
<div id="admin-version-editor" class="editor-content" contenteditable="true" placeholder="Schreiben Sie hier den Inhalt..."></div>
|
||||
<div class="editor-status">
|
||||
<span id="editor-char-count">0 Zeichen</span> |
|
||||
<span style="color: var(--bp-text-muted);">Tipp: Sie können direkt aus Word kopieren und einfügen!</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="admin-version-content">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideVersionForm()">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="saveVersion()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-version-table-container">
|
||||
<div class="admin-empty">Wählen Sie ein Dokument aus, um dessen Versionen anzuzeigen.</div>
|
||||
</div>
|
||||
|
||||
<!-- Approval Dialog -->
|
||||
<div id="approval-dialog" class="admin-dialog">
|
||||
<div class="admin-dialog-content">
|
||||
<h3>Version genehmigen</h3>
|
||||
<p class="admin-dialog-info">
|
||||
Legen Sie einen Veröffentlichungszeitpunkt fest. Die Version wird automatisch zum gewählten Zeitpunkt veröffentlicht.
|
||||
</p>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Veröffentlichungsdatum *</label>
|
||||
<input type="date" class="admin-form-input" id="approval-date" required>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Uhrzeit</label>
|
||||
<input type="time" class="admin-form-input" id="approval-time" value="00:00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Kommentar (optional)</label>
|
||||
<input type="text" class="admin-form-input" id="approval-comment" placeholder="z.B. Genehmigt nach rechtlicher Prüfung">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-dialog-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideApprovalDialog()">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="submitApproval()">Genehmigen & Planen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Compare View (Full Screen Overlay) -->
|
||||
<div id="version-compare-view" class="version-compare-overlay">
|
||||
<div class="version-compare-header">
|
||||
<h2>Versionsvergleich</h2>
|
||||
<div class="version-compare-info">
|
||||
<span id="compare-published-info"></span>
|
||||
<span class="compare-vs">vs</span>
|
||||
<span id="compare-draft-info"></span>
|
||||
</div>
|
||||
<button class="btn btn-ghost" onclick="hideCompareView()">Schließen</button>
|
||||
</div>
|
||||
<div class="version-compare-container">
|
||||
<div class="version-compare-panel">
|
||||
<div class="version-compare-panel-header">
|
||||
<span class="compare-label compare-label-published">Veröffentlichte Version</span>
|
||||
<span id="compare-published-version"></span>
|
||||
</div>
|
||||
<div class="version-compare-content" id="compare-content-left"></div>
|
||||
</div>
|
||||
<div class="version-compare-panel">
|
||||
<div class="version-compare-panel-header">
|
||||
<span class="compare-label compare-label-draft">Neue Version</span>
|
||||
<span id="compare-draft-version"></span>
|
||||
</div>
|
||||
<div class="version-compare-content" id="compare-content-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-compare-footer">
|
||||
<div id="compare-history-container"></div>
|
||||
<div id="compare-actions-container" style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cookie Categories Tab -->
|
||||
<div id="admin-cookies" class="admin-content">
|
||||
<div class="admin-toolbar">
|
||||
<div class="admin-toolbar-left">
|
||||
<span style="color: var(--bp-text-muted);">Cookie-Kategorien verwalten</span>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="showCookieForm()">+ Neue Kategorie</button>
|
||||
</div>
|
||||
|
||||
<div id="admin-cookie-form" class="admin-form">
|
||||
<h3 class="admin-form-title">Neue Cookie-Kategorie</h3>
|
||||
<input type="hidden" id="admin-cookie-id">
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Technischer Name *</label>
|
||||
<input type="text" class="admin-form-input" id="admin-cookie-name" placeholder="z.B. analytics">
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Anzeigename (DE) *</label>
|
||||
<input type="text" class="admin-form-input" id="admin-cookie-display-de" placeholder="z.B. Analyse-Cookies">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Anzeigename (EN)</label>
|
||||
<input type="text" class="admin-form-input" id="admin-cookie-display-en" placeholder="z.B. Analytics Cookies">
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">
|
||||
<input type="checkbox" id="admin-cookie-mandatory"> Notwendig (kann nicht deaktiviert werden)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Beschreibung (DE)</label>
|
||||
<input type="text" class="admin-form-input" id="admin-cookie-desc-de" placeholder="Beschreibung auf Deutsch">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideCookieForm()">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="saveCookieCategory()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-cookie-table-container">
|
||||
<div class="admin-loading">Lade Cookie-Kategorien...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Tab -->
|
||||
<div id="admin-stats" class="admin-content">
|
||||
<div id="admin-stats-container">
|
||||
<div class="admin-loading">Lade Statistiken...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
""" + _get_email_templates_html() + """
|
||||
""" + _get_dsms_html() + """
|
||||
""" + _get_gpu_html() + """
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
""" + _get_dsms_webui_modal_html() + """
|
||||
"""
|
||||
|
||||
|
||||
def _get_email_templates_html() -> str:
|
||||
"""HTML fuer E-Mail Templates Tab"""
|
||||
return """
|
||||
<!-- E-Mail Templates Tab -->
|
||||
<div id="admin-emails" class="admin-content">
|
||||
<div class="admin-toolbar">
|
||||
<div class="admin-toolbar-left">
|
||||
<select class="admin-form-select" id="email-template-select" onchange="loadEmailTemplateVersions()">
|
||||
<option value="">-- E-Mail-Vorlage auswählen --</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="initializeEmailTemplates()">Templates initialisieren</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="showEmailVersionForm()" id="btn-new-email-version" disabled>+ Neue Version</button>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Template Info Card -->
|
||||
<div id="email-template-info" style="display: none; margin-bottom: 16px;">
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px; border: 1px solid var(--bp-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<div>
|
||||
<h3 style="margin: 0 0 8px 0; font-size: 16px;" id="email-template-name">-</h3>
|
||||
<p style="margin: 0; color: var(--bp-text-muted); font-size: 13px;" id="email-template-description">-</p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div class="admin-badge" id="email-template-type-badge">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--bp-border);">
|
||||
<span style="font-size: 12px; color: var(--bp-text-muted);">Variablen: </span>
|
||||
<span id="email-template-variables" style="font-size: 12px; font-family: monospace; color: var(--bp-primary);"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Version Form -->
|
||||
<div id="email-version-form" class="admin-form" style="display: none;">
|
||||
<h3 class="admin-form-title" id="email-version-form-title">Neue E-Mail-Version erstellen</h3>
|
||||
<input type="hidden" id="email-version-id">
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Version *</label>
|
||||
<input type="text" class="admin-form-input" id="email-version-number" placeholder="z.B. 1.0.0">
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Sprache *</label>
|
||||
<select class="admin-form-select" id="email-version-lang">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Betreff *</label>
|
||||
<input type="text" class="admin-form-input" id="email-version-subject" placeholder="E-Mail Betreff (kann Variablen enthalten)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">HTML-Inhalt *</label>
|
||||
<div class="editor-container">
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="formatEmailDoc('bold')" title="Fett"><b>B</b></button>
|
||||
<button type="button" class="editor-btn" onclick="formatEmailDoc('italic')" title="Kursiv"><i>I</i></button>
|
||||
<button type="button" class="editor-btn" onclick="formatEmailDoc('underline')" title="Unterstrichen"><u>U</u></button>
|
||||
</div>
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="formatEmailBlock('h1')" title="Überschrift 1">H1</button>
|
||||
<button type="button" class="editor-btn" onclick="formatEmailBlock('h2')" title="Überschrift 2">H2</button>
|
||||
<button type="button" class="editor-btn" onclick="formatEmailBlock('p')" title="Absatz">P</button>
|
||||
</div>
|
||||
<div class="editor-toolbar-group">
|
||||
<button type="button" class="editor-btn" onclick="insertEmailVariable()" title="Variable einfügen">{{var}}</button>
|
||||
<button type="button" class="editor-btn" onclick="insertEmailLink()" title="Link einfügen">🔗</button>
|
||||
<button type="button" class="editor-btn" onclick="insertEmailButton()" title="Button einfügen">🔘</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="email-version-editor" class="editor-content" contenteditable="true" placeholder="HTML-Inhalt der E-Mail..." style="min-height: 200px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Text-Version (Plain Text)</label>
|
||||
<textarea class="admin-form-input" id="email-version-text" rows="5" placeholder="Plain-Text-Version der E-Mail (optional, wird aus HTML generiert falls leer)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideEmailVersionForm()">Abbrechen</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="previewEmailVersion()">Vorschau</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="saveEmailVersion()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Versions Table -->
|
||||
<div id="email-version-table-container">
|
||||
<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Preview Dialog -->
|
||||
<div id="email-preview-dialog" class="admin-dialog" style="display: none;">
|
||||
<div class="admin-dialog-content" style="max-width: 700px; max-height: 80vh; overflow-y: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h3 style="margin: 0;">E-Mail Vorschau</h3>
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideEmailPreview()">Schließen</button>
|
||||
</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<strong>Betreff:</strong> <span id="email-preview-subject"></span>
|
||||
</div>
|
||||
<div style="border: 1px solid var(--bp-border); border-radius: 8px; padding: 16px; background: white; color: #333;">
|
||||
<div id="email-preview-content"></div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
||||
<input type="email" class="admin-form-input" id="email-test-address" placeholder="Test-E-Mail-Adresse" style="flex: 1;">
|
||||
<button class="btn btn-primary btn-sm" onclick="sendTestEmail()">Test-E-Mail senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Approval Dialog -->
|
||||
<div id="email-approval-dialog" class="admin-dialog" style="display: none;">
|
||||
<div class="admin-dialog-content">
|
||||
<h3>E-Mail-Version genehmigen</h3>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group full-width">
|
||||
<label class="admin-form-label">Kommentar (optional)</label>
|
||||
<input type="text" class="admin-form-input" id="email-approval-comment" placeholder="z.B. Genehmigt nach Marketing-Prüfung">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-dialog-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideEmailApprovalDialog()">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="submitEmailApproval()">Genehmigen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _get_dsms_html() -> str:
|
||||
"""HTML fuer DSMS Tab"""
|
||||
return """
|
||||
<!-- DSMS Tab -->
|
||||
<div id="admin-dsms" class="admin-content">
|
||||
<div class="admin-toolbar">
|
||||
<div class="admin-toolbar-left">
|
||||
<span style="font-weight: 600; color: var(--bp-primary);">Dezentrales Speichersystem (IPFS)</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="btn btn-ghost btn-sm" onclick="openDsmsWebUI()">DSMS WebUI</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="loadDsmsData()">Aktualisieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DSMS Status Cards -->
|
||||
<div id="dsms-status-cards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
|
||||
<div class="admin-loading">Lade DSMS Status...</div>
|
||||
</div>
|
||||
|
||||
<!-- DSMS Tabs -->
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 16px; border-bottom: 1px solid var(--bp-border); padding-bottom: 8px;">
|
||||
<button class="dsms-subtab active" data-dsms-tab="archives" onclick="switchDsmsTab('archives')">Archivierte Dokumente</button>
|
||||
<button class="dsms-subtab" data-dsms-tab="verify" onclick="switchDsmsTab('verify')">Verifizierung</button>
|
||||
<button class="dsms-subtab" data-dsms-tab="settings" onclick="switchDsmsTab('settings')">Einstellungen</button>
|
||||
</div>
|
||||
|
||||
<!-- Archives Sub-Tab -->
|
||||
<div id="dsms-archives" class="dsms-content active">
|
||||
<div class="admin-toolbar" style="margin-bottom: 16px;">
|
||||
<div class="admin-toolbar-left">
|
||||
<input type="text" class="admin-search" placeholder="CID suchen..." id="dsms-cid-search" style="width: 300px;">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="showArchiveForm()">+ Dokument archivieren</button>
|
||||
</div>
|
||||
|
||||
<!-- Archive Form -->
|
||||
<div id="dsms-archive-form" class="admin-form" style="display: none; margin-bottom: 16px;">
|
||||
<h3 class="admin-form-title">Dokument im DSMS archivieren</h3>
|
||||
<div class="admin-form-row">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Dokument auswählen *</label>
|
||||
<select class="admin-form-select" id="dsms-archive-doc-select">
|
||||
<option value="">-- Dokument wählen --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Version *</label>
|
||||
<select class="admin-form-select" id="dsms-archive-version-select" disabled>
|
||||
<option value="">-- Erst Dokument wählen --</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="hideArchiveForm()">Abbrechen</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="archiveDocumentToDsms()">Archivieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dsms-archives-table">
|
||||
<div class="admin-loading">Lade archivierte Dokumente...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verify Sub-Tab -->
|
||||
<div id="dsms-verify" class="dsms-content" style="display: none;">
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Dokumentenintegrität prüfen</h3>
|
||||
<p style="color: var(--bp-text-muted); margin-bottom: 16px; font-size: 14px;">
|
||||
Geben Sie einen CID (Content Identifier) ein, um die Integrität eines archivierten Dokuments zu verifizieren.
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
|
||||
<input type="text" class="admin-form-input" id="dsms-verify-cid" placeholder="Qm... oder bafy..." style="flex: 1;">
|
||||
<button class="btn btn-primary btn-sm" onclick="verifyDsmsDocument()">Verifizieren</button>
|
||||
</div>
|
||||
<div id="dsms-verify-result" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Sub-Tab -->
|
||||
<div id="dsms-settings" class="dsms-content" style="display: none;">
|
||||
<div style="display: grid; gap: 16px;">
|
||||
<!-- Node Info -->
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Node-Informationen</h3>
|
||||
<div id="dsms-node-info">
|
||||
<div class="admin-loading">Lade Node-Info...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Schnellzugriff</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||||
<button class="btn btn-primary btn-sm" onclick="openDsmsWebUI()">DSMS WebUI</button>
|
||||
<a href="http://localhost:8082/docs" target="_blank" class="btn btn-ghost btn-sm">DSMS API Docs</a>
|
||||
<a href="http://localhost:8085" target="_blank" class="btn btn-ghost btn-sm">IPFS Gateway</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lizenzhinweise -->
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Open Source Lizenzen</h3>
|
||||
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 12px;">
|
||||
DSMS verwendet folgende Open-Source-Komponenten:
|
||||
</p>
|
||||
<ul style="color: var(--bp-text-muted); font-size: 13px; margin: 0; padding-left: 20px;">
|
||||
<li><strong>IPFS Kubo</strong> - MIT + Apache 2.0 (Dual License) - Protocol Labs, Inc.</li>
|
||||
<li><strong>IPFS WebUI</strong> - MIT License - Protocol Labs, Inc.</li>
|
||||
<li><strong>FastAPI</strong> - MIT License</li>
|
||||
</ul>
|
||||
<p style="color: var(--bp-text-muted); font-size: 12px; margin-top: 12px; font-style: italic;">
|
||||
Alle Komponenten erlauben kommerzielle Nutzung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _get_gpu_html() -> str:
|
||||
"""HTML fuer GPU Infrastructure Tab"""
|
||||
return """
|
||||
<!-- GPU Infrastructure Tab -->
|
||||
<div id="admin-content-gpu" class="admin-content">
|
||||
<div class="gpu-control-panel">
|
||||
<div class="gpu-status-header">
|
||||
<h3 style="margin: 0; font-size: 16px;">vast.ai GPU Instance</h3>
|
||||
<div id="gpu-status-badge" class="gpu-status-badge stopped">
|
||||
<span class="gpu-status-dot"></span>
|
||||
<span id="gpu-status-text">Unbekannt</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gpu-info-grid">
|
||||
<div class="gpu-info-card">
|
||||
<div class="gpu-info-label">GPU</div>
|
||||
<div id="gpu-name" class="gpu-info-value">-</div>
|
||||
</div>
|
||||
<div class="gpu-info-card">
|
||||
<div class="gpu-info-label">Kosten/Stunde</div>
|
||||
<div id="gpu-dph" class="gpu-info-value cost">-</div>
|
||||
</div>
|
||||
<div class="gpu-info-card">
|
||||
<div class="gpu-info-label">Session</div>
|
||||
<div id="gpu-session-time" class="gpu-info-value time">-</div>
|
||||
</div>
|
||||
<div class="gpu-info-card">
|
||||
<div class="gpu-info-label">Gesamt</div>
|
||||
<div id="gpu-total-cost" class="gpu-info-value cost">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="gpu-endpoint" class="gpu-endpoint-url" style="display: none;">
|
||||
Endpoint: <span id="gpu-endpoint-url">-</span>
|
||||
</div>
|
||||
|
||||
<div id="gpu-shutdown-warning" class="gpu-shutdown-warning" style="display: none;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<span>Auto-Shutdown in <strong id="gpu-shutdown-minutes">0</strong> Minuten (bei Inaktivitaet)</span>
|
||||
</div>
|
||||
|
||||
<div class="gpu-controls">
|
||||
<button id="gpu-btn-start" class="gpu-btn gpu-btn-start" onclick="gpuPowerOn()">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3l14 9-14 9V3z"/>
|
||||
</svg>
|
||||
Starten
|
||||
</button>
|
||||
<button id="gpu-btn-stop" class="gpu-btn gpu-btn-stop" onclick="gpuPowerOff()" disabled>
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
|
||||
</svg>
|
||||
Stoppen
|
||||
</button>
|
||||
<button class="gpu-btn gpu-btn-refresh" onclick="gpuRefreshStatus()" title="Status aktualisieren">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gpu-cost-summary">
|
||||
<h4>Kosten-Zusammenfassung</h4>
|
||||
<div class="gpu-info-grid">
|
||||
<div class="gpu-info-card">
|
||||
<div class="gpu-info-label">Laufzeit Gesamt</div>
|
||||
<div id="gpu-total-runtime" class="gpu-info-value time">0h 0m</div>
|
||||
</div>
|
||||
<div class="gpu-info-card">
|
||||
<div class="gpu-info-label">Kosten Gesamt</div>
|
||||
<div id="gpu-total-cost-all" class="gpu-info-value cost">$0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details style="margin-top: 20px;">
|
||||
<summary style="cursor: pointer; color: var(--bp-text-muted); font-size: 13px;">
|
||||
Audit Log (letzte Aktionen)
|
||||
</summary>
|
||||
<div id="gpu-audit-log" class="gpu-audit-log">
|
||||
<div class="gpu-audit-entry">Keine Eintraege</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px; background: var(--bp-surface-elevated); border-radius: 8px; font-size: 12px; color: var(--bp-text-muted);">
|
||||
<strong>Hinweis:</strong> Die GPU-Instanz wird automatisch nach 30 Minuten Inaktivitaet gestoppt.
|
||||
Bei jedem LLM-Request wird die Aktivitaet aufgezeichnet und der Timer zurueckgesetzt.
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _get_dsms_webui_modal_html() -> str:
|
||||
"""HTML fuer DSMS WebUI Modal"""
|
||||
return """
|
||||
<!-- DSMS WebUI Modal -->
|
||||
<div id="dsms-webui-modal" class="legal-modal" style="display: none;">
|
||||
<div class="legal-modal-content" style="max-width: 1200px; width: 95%; height: 90vh;">
|
||||
<div class="legal-modal-header" style="border-bottom: 1px solid var(--bp-border);">
|
||||
<h2 style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 24px;">🌐</span> DSMS WebUI
|
||||
</h2>
|
||||
<button id="dsms-webui-modal-close" class="legal-modal-close" onclick="closeDsmsWebUI()">×</button>
|
||||
</div>
|
||||
<div style="display: flex; height: calc(100% - 60px);">
|
||||
<!-- Sidebar -->
|
||||
<div style="width: 200px; background: var(--bp-surface); border-right: 1px solid var(--bp-border); padding: 16px;">
|
||||
<nav style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<button class="dsms-webui-nav active" data-section="overview" onclick="switchDsmsWebUISection('overview')">
|
||||
<span>📈</span> Übersicht
|
||||
</button>
|
||||
<button class="dsms-webui-nav" data-section="files" onclick="switchDsmsWebUISection('files')">
|
||||
<span>📁</span> Dateien
|
||||
</button>
|
||||
<button class="dsms-webui-nav" data-section="explore" onclick="switchDsmsWebUISection('explore')">
|
||||
<span>🔍</span> Erkunden
|
||||
</button>
|
||||
<button class="dsms-webui-nav" data-section="peers" onclick="switchDsmsWebUISection('peers')">
|
||||
<span>🌐</span> Peers
|
||||
</button>
|
||||
<button class="dsms-webui-nav" data-section="config" onclick="switchDsmsWebUISection('config')">
|
||||
<span>⚙</span> Konfiguration
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- Main Content -->
|
||||
<div style="flex: 1; overflow-y: auto; padding: 24px;" id="dsms-webui-content">
|
||||
<!-- Overview Section (default) -->
|
||||
<div id="dsms-webui-overview" class="dsms-webui-section active">
|
||||
<h3 style="margin: 0 0 24px 0;">Node Übersicht</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Status</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-status">--</div>
|
||||
</div>
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Node ID</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-node-id" style="font-size: 11px; word-break: break-all;">--</div>
|
||||
</div>
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Protokoll</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-protocol">--</div>
|
||||
</div>
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Agent</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-agent">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px;">
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Repo Größe</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-repo-size">--</div>
|
||||
<div class="dsms-webui-stat-sub" id="webui-storage-info">--</div>
|
||||
</div>
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Objekte</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-num-objects">--</div>
|
||||
</div>
|
||||
<div class="dsms-webui-stat-card">
|
||||
<div class="dsms-webui-stat-label">Gepinnte Dokumente</div>
|
||||
<div class="dsms-webui-stat-value" id="webui-pinned-count">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 24px;">
|
||||
<h4 style="margin: 0 0 12px 0;">Adressen</h4>
|
||||
<div id="webui-addresses" style="background: var(--bp-input-bg); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto;">
|
||||
Lade...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files Section -->
|
||||
<div id="dsms-webui-files" class="dsms-webui-section" style="display: none;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
||||
<h3 style="margin: 0;">Dateien hochladen</h3>
|
||||
</div>
|
||||
<div class="dsms-webui-upload-zone" id="dsms-upload-zone" ondrop="handleDsmsFileDrop(event)" ondragover="handleDsmsDragOver(event)" ondragleave="handleDsmsDragLeave(event)">
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📥</div>
|
||||
<p style="color: var(--bp-text); margin-bottom: 8px;">Dateien hierher ziehen</p>
|
||||
<p style="color: var(--bp-text-muted); font-size: 13px;">oder</p>
|
||||
<input type="file" id="dsms-file-input" style="display: none;" onchange="handleDsmsFileSelect(event)" multiple>
|
||||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('dsms-file-input').click()">Dateien auswählen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dsms-upload-progress" style="display: none; margin-top: 16px;">
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||||
<div id="dsms-upload-status">Hochladen...</div>
|
||||
<div style="background: var(--bp-border); border-radius: 4px; height: 8px; margin-top: 8px; overflow: hidden;">
|
||||
<div id="dsms-upload-bar" style="background: var(--bp-primary); height: 100%; width: 0%; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dsms-upload-results" style="margin-top: 24px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Explore Section -->
|
||||
<div id="dsms-webui-explore" class="dsms-webui-section" style="display: none;">
|
||||
<h3 style="margin: 0 0 24px 0;">IPFS Explorer</h3>
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
|
||||
<input type="text" class="admin-search" placeholder="CID eingeben (z.B. QmXyz...)" id="webui-explore-cid" style="flex: 1;">
|
||||
<button class="btn btn-primary btn-sm" onclick="exploreDsmsCid()">Erkunden</button>
|
||||
</div>
|
||||
<div id="dsms-explore-result" style="display: none;">
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||||
<div id="dsms-explore-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peers Section -->
|
||||
<div id="dsms-webui-peers" class="dsms-webui-section" style="display: none;">
|
||||
<h3 style="margin: 0 0 24px 0;">Verbundene Peers</h3>
|
||||
<p style="color: var(--bp-text-muted); margin-bottom: 16px;">
|
||||
Hinweis: In einem privaten DSMS-Netzwerk sind normalerweise keine externen Peers verbunden.
|
||||
</p>
|
||||
<div id="webui-peers-list">
|
||||
<div class="admin-loading">Lade Peers...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Section -->
|
||||
<div id="dsms-webui-config" class="dsms-webui-section" style="display: none;">
|
||||
<h3 style="margin: 0 0 24px 0;">Konfiguration</h3>
|
||||
<div style="display: grid; gap: 16px;">
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||||
<h4 style="margin: 0 0 12px 0;">API Endpoints</h4>
|
||||
<table style="width: 100%; font-size: 13px;">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: var(--bp-text-muted);">IPFS API</td>
|
||||
<td style="padding: 8px 0; font-family: monospace;">http://localhost:5001</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: var(--bp-text-muted);">DSMS Gateway</td>
|
||||
<td style="padding: 8px 0; font-family: monospace;">http://localhost:8082</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: var(--bp-text-muted);">IPFS Gateway</td>
|
||||
<td style="padding: 8px 0; font-family: monospace;">http://localhost:8085</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Swarm P2P</td>
|
||||
<td style="padding: 8px 0; font-family: monospace;">:4001</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||||
<h4 style="margin: 0 0 12px 0;">Aktionen</h4>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||||
<button class="btn btn-ghost btn-sm" onclick="runDsmsGarbageCollection()">🗑 Garbage Collection</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="loadDsmsWebUIData()">↻ Daten aktualisieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
1428
backend/frontend/components/admin_panel/scripts.py
Normal file
1428
backend/frontend/components/admin_panel/scripts.py
Normal file
File diff suppressed because it is too large
Load Diff
803
backend/frontend/components/admin_panel/styles.py
Normal file
803
backend/frontend/components/admin_panel/styles.py
Normal file
@@ -0,0 +1,803 @@
|
||||
"""
|
||||
Admin Panel Component - CSS Styles
|
||||
|
||||
Extracted from admin_panel.py for maintainability.
|
||||
Contains all CSS styles for the admin panel modal, tabs, forms, and GPU controls.
|
||||
"""
|
||||
|
||||
|
||||
def get_admin_panel_css() -> str:
|
||||
"""CSS fuer Admin Panel zurueckgeben"""
|
||||
return """
|
||||
/* ==========================================
|
||||
ADMIN PANEL STYLES
|
||||
========================================== */
|
||||
.admin-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);
|
||||
}
|
||||
|
||||
.admin-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.admin-modal-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 16px;
|
||||
width: 95%;
|
||||
max-width: 1000px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.admin-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;
|
||||
}
|
||||
|
||||
.admin-tab:hover {
|
||||
background: var(--bp-border-subtle);
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.admin-tab.active {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-search {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
background: var(--bp-surface-elevated);
|
||||
color: var(--bp-text);
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
background: var(--bp-surface-elevated);
|
||||
font-weight: 600;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.admin-table tr:hover {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.admin-badge-published {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ADE80;
|
||||
}
|
||||
|
||||
.admin-badge-draft {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
color: #FBBF24;
|
||||
}
|
||||
|
||||
.admin-badge-archived {
|
||||
background: rgba(156, 163, 175, 0.2);
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.admin-badge-rejected {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.admin-badge-review {
|
||||
background: rgba(147, 51, 234, 0.2);
|
||||
color: #A855F7;
|
||||
}
|
||||
|
||||
.admin-badge-approved {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #22C55E;
|
||||
}
|
||||
|
||||
.admin-badge-submitted {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3B82F6;
|
||||
}
|
||||
|
||||
.admin-badge-mandatory {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.admin-badge-optional {
|
||||
background: rgba(96, 165, 250, 0.2);
|
||||
color: #60A5FA;
|
||||
}
|
||||
|
||||
.admin-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-btn {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-btn-edit {
|
||||
background: var(--bp-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-btn-edit:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.admin-btn-delete {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.admin-btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.admin-btn-publish {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ADE80;
|
||||
}
|
||||
|
||||
.admin-btn-publish:hover {
|
||||
background: rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
.admin-form {
|
||||
display: none;
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-form.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Info Text in Toolbar */
|
||||
.admin-info-text {
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Dialog Overlay */
|
||||
.admin-dialog {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.admin-dialog.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.admin-dialog-content {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.admin-dialog-content h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.admin-dialog-info {
|
||||
font-size: 13px;
|
||||
color: var(--bp-text-muted);
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.admin-dialog-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Scheduled Badge */
|
||||
.admin-badge-scheduled {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Version Compare Overlay */
|
||||
.version-compare-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--bp-bg);
|
||||
z-index: 2000;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.version-compare-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.version-compare-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: var(--bp-surface);
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.version-compare-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.version-compare-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compare-vs {
|
||||
color: var(--bp-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-compare-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.version-compare-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.version-compare-panel:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.version-compare-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
background: var(--bp-surface);
|
||||
border-bottom: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.compare-label {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.compare-label-published {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.compare-label-draft {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.version-compare-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.version-compare-content h1,
|
||||
.version-compare-content h2,
|
||||
.version-compare-content h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.version-compare-content p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.version-compare-content ul,
|
||||
.version-compare-content ol {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.version-compare-content .no-content {
|
||||
color: var(--bp-text-muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.version-compare-footer {
|
||||
padding: 12px 24px;
|
||||
background: var(--bp-surface);
|
||||
border-top: 1px solid var(--bp-border);
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.compare-history-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.compare-history-item {
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid var(--bp-border-subtle);
|
||||
}
|
||||
|
||||
.compare-history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.compare-history-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.admin-form-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.admin-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.admin-form-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.admin-form-input,
|
||||
.admin-form-select,
|
||||
.admin-form-textarea {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--bp-border);
|
||||
border-radius: 8px;
|
||||
background: var(--bp-surface);
|
||||
color: var(--bp-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-form-textarea {
|
||||
min-height: 150px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.admin-form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.admin-empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.admin-loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-modal-content {
|
||||
background: #FFFFFF;
|
||||
border-color: #E0E0E0;
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-tabs {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-table th {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
[data-theme="light"] .admin-form {
|
||||
background: #F8F8F8;
|
||||
border-color: #E0E0E0;
|
||||
}
|
||||
|
||||
/* GPU Infrastructure Styles */
|
||||
.gpu-control-panel {
|
||||
background: var(--bp-surface-elevated);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.gpu-status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.gpu-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gpu-status-badge.running {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.gpu-status-badge.stopped,
|
||||
.gpu-status-badge.exited {
|
||||
background: rgba(156, 163, 175, 0.15);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.gpu-status-badge.loading,
|
||||
.gpu-status-badge.scheduling,
|
||||
.gpu-status-badge.creating {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.gpu-status-badge.error,
|
||||
.gpu-status-badge.unconfigured {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.gpu-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.gpu-status-dot.running {
|
||||
animation: gpu-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes gpu-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.gpu-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.gpu-info-card {
|
||||
background: var(--bp-surface);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
}
|
||||
|
||||
.gpu-info-label {
|
||||
font-size: 11px;
|
||||
color: var(--bp-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.gpu-info-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--bp-text);
|
||||
}
|
||||
|
||||
.gpu-info-value.cost {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.gpu-info-value.time {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.gpu-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.gpu-btn {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.gpu-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.gpu-btn-start {
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gpu-btn-start:hover:not(:disabled) {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.gpu-btn-stop {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gpu-btn-stop:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.gpu-btn-refresh {
|
||||
background: var(--bp-surface);
|
||||
color: var(--bp-text);
|
||||
border: 1px solid var(--bp-border);
|
||||
flex: 0 0 auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.gpu-btn-refresh:hover:not(:disabled) {
|
||||
background: var(--bp-surface-elevated);
|
||||
}
|
||||
|
||||
.gpu-shutdown-warning {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fbbf24;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gpu-cost-summary {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--bp-border);
|
||||
}
|
||||
|
||||
.gpu-cost-summary h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: var(--bp-text-muted);
|
||||
}
|
||||
|
||||
.gpu-audit-log {
|
||||
margin-top: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: var(--bp-surface);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--bp-border-subtle);
|
||||
}
|
||||
|
||||
.gpu-audit-entry {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--bp-border-subtle);
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.gpu-audit-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gpu-audit-time {
|
||||
color: var(--bp-text-muted);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.gpu-endpoint-url {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bp-surface);
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--bp-text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.gpu-endpoint-url.active {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.gpu-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: gpu-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gpu-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
[data-theme="light"] .gpu-control-panel {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
[data-theme="light"] .gpu-info-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
"""
|
||||
Reference in New Issue
Block a user