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>
285 lines
11 KiB
Python
285 lines
11 KiB
Python
"""
|
|
Meetings Module - Recordings Page
|
|
Recordings and transcripts management
|
|
"""
|
|
|
|
from ..templates import ICONS, render_base_page
|
|
|
|
|
|
def recordings_page() -> str:
|
|
"""Recordings and transcripts management"""
|
|
content = f'''
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Aufzeichnungen</h1>
|
|
<p class="page-subtitle">Aufzeichnungen und Protokolle verwalten</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="filterRecordings('all')">Alle</button>
|
|
<button class="tab" onclick="filterRecordings('trainings')">Schulungen</button>
|
|
<button class="tab" onclick="filterRecordings('meetings')">Meetings</button>
|
|
</div>
|
|
|
|
<!-- Recordings List -->
|
|
<div class="recording-list">
|
|
<div class="card" style="margin-bottom: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
<div class="card-icon primary">{ICONS['record']}</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-weight: 600; margin-bottom: 0.25rem;">Docker Grundlagen Schulung</div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
|
10.12.2025, 10:00 - 11:30 | 1:30:00 | 156 MB
|
|
</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-secondary" onclick="playRecording('docker-basics')">
|
|
{ICONS['play']} Abspielen
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="viewTranscript('docker-basics')">
|
|
{ICONS['file_text']} Protokoll
|
|
</button>
|
|
<button class="btn-icon" onclick="downloadRecording('docker-basics')">
|
|
{ICONS['download']}
|
|
</button>
|
|
<button class="btn-icon" onclick="deleteRecording('docker-basics')">
|
|
{ICONS['trash']}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
<div class="card-icon primary">{ICONS['record']}</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-weight: 600; margin-bottom: 0.25rem;">Team-Meeting KW 49</div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
|
06.12.2025, 14:00 - 15:00 | 1:00:00 | 98 MB
|
|
</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-secondary" onclick="playRecording('team-kw49')">
|
|
{ICONS['play']} Abspielen
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="viewTranscript('team-kw49')">
|
|
{ICONS['file_text']} Protokoll
|
|
</button>
|
|
<button class="btn-icon" onclick="downloadRecording('team-kw49')">
|
|
{ICONS['download']}
|
|
</button>
|
|
<button class="btn-icon" onclick="deleteRecording('team-kw49')">
|
|
{ICONS['trash']}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
<div class="card-icon primary">{ICONS['record']}</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-weight: 600; margin-bottom: 0.25rem;">Elterngespräch - Max Müller</div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
|
02.12.2025, 16:00 - 16:30 | 0:28:00 | 42 MB
|
|
</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-secondary" onclick="playRecording('parent-mueller')">
|
|
{ICONS['play']} Abspielen
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="viewTranscript('parent-mueller')">
|
|
{ICONS['file_text']} Protokoll
|
|
</button>
|
|
<button class="btn-icon" onclick="downloadRecording('parent-mueller')">
|
|
{ICONS['download']}
|
|
</button>
|
|
<button class="btn-icon" onclick="deleteRecording('parent-mueller')">
|
|
{ICONS['trash']}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Storage Info -->
|
|
<div class="card" style="margin-top: 2rem;">
|
|
<div class="card-header">
|
|
<span class="card-title">Speicherplatz</span>
|
|
</div>
|
|
<div style="margin-bottom: 1rem;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<span>296 MB von 10 GB verwendet</span>
|
|
<span>3%</span>
|
|
</div>
|
|
<div style="background: var(--bp-bg); border-radius: 4px; height: 8px; overflow: hidden;">
|
|
<div style="background: var(--bp-primary); width: 3%; height: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
|
|
3 Aufzeichnungen | Älteste Aufzeichnung: 02.12.2025
|
|
</p>
|
|
</div>
|
|
|
|
<script>
|
|
function filterRecordings(filter) {{
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
console.log('Filter:', filter);
|
|
}}
|
|
|
|
function playRecording(recordingId) {{
|
|
window.location.href = '/meetings/recordings/' + recordingId + '/play';
|
|
}}
|
|
|
|
function viewTranscript(recordingId) {{
|
|
window.location.href = '/meetings/recordings/' + recordingId + '/transcript';
|
|
}}
|
|
|
|
function downloadRecording(recordingId) {{
|
|
window.location.href = '/api/recordings/' + recordingId + '/download';
|
|
}}
|
|
|
|
function deleteRecording(recordingId) {{
|
|
if (confirm('Aufzeichnung wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {{
|
|
fetch('/api/recordings/' + recordingId, {{ method: 'DELETE' }})
|
|
.then(() => location.reload());
|
|
}}
|
|
}}
|
|
</script>
|
|
'''
|
|
|
|
return render_base_page("Aufzeichnungen", content, "recordings")
|
|
|
|
|
|
def play_recording(recording_id: str) -> str:
|
|
"""Play a recording"""
|
|
content = f'''
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Aufzeichnung abspielen</h1>
|
|
<p class="page-subtitle">{recording_id}</p>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-secondary" onclick="downloadRecording()">
|
|
{ICONS['download']} Herunterladen
|
|
</button>
|
|
<a href="/meetings/recordings" class="btn btn-secondary">Zurück</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="video-container">
|
|
<div class="video-placeholder">
|
|
{ICONS['play']}
|
|
<p>Aufzeichnung wird geladen...</p>
|
|
<p style="font-size: 0.875rem;">Recording ID: {recording_id}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recording Info -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-title">Details</span>
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Datum</div>
|
|
<div style="font-weight: 600;">10.12.2025, 10:00</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Dauer</div>
|
|
<div style="font-weight: 600;">1:30:00</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Größe</div>
|
|
<div style="font-weight: 600;">156 MB</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">Teilnehmer</div>
|
|
<div style="font-weight: 600;">15</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function downloadRecording() {{
|
|
window.location.href = '/api/recordings/{recording_id}/download';
|
|
}}
|
|
</script>
|
|
'''
|
|
|
|
return render_base_page("Aufzeichnung", content, "recordings")
|
|
|
|
|
|
def view_transcript(recording_id: str) -> str:
|
|
"""View recording transcript"""
|
|
content = f'''
|
|
<div class="page-header">
|
|
<div>
|
|
<h1 class="page-title">Protokoll</h1>
|
|
<p class="page-subtitle">{recording_id}</p>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-secondary" onclick="downloadTranscript()">
|
|
{ICONS['download']} Als PDF exportieren
|
|
</button>
|
|
<a href="/meetings/recordings" class="btn btn-secondary">Zurück</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-title">Transkript</span>
|
|
</div>
|
|
|
|
<div style="margin-top: 1rem;">
|
|
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<span style="font-weight: 600;">Max Trainer</span>
|
|
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:00:15</span>
|
|
</div>
|
|
<p style="font-size: 0.875rem;">Willkommen zur Docker Grundlagen Schulung. Heute werden wir die Basics von Containern und Images besprechen.</p>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<span style="font-weight: 600;">Max Trainer</span>
|
|
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:02:30</span>
|
|
</div>
|
|
<p style="font-size: 0.875rem;">Docker ist eine Open-Source-Plattform, die es ermöglicht, Anwendungen in Containern zu entwickeln, zu versenden und auszuführen.</p>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<span style="font-weight: 600;">Teilnehmer 1</span>
|
|
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:05:45</span>
|
|
</div>
|
|
<p style="font-size: 0.875rem;">Was ist der Unterschied zwischen einem Container und einer virtuellen Maschine?</p>
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1rem; padding: 1rem; background: var(--bp-bg); border-radius: 8px;">
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
|
<span style="font-weight: 600;">Max Trainer</span>
|
|
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">00:06:00</span>
|
|
</div>
|
|
<p style="font-size: 0.875rem;">Gute Frage! Container teilen sich den Kernel des Host-Systems, während VMs einen vollständigen Hypervisor und ein eigenes Betriebssystem benötigen...</p>
|
|
</div>
|
|
|
|
<div style="text-align: center; padding: 2rem; color: var(--bp-text-muted);">
|
|
<p>... Transkript wird fortgesetzt ...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function downloadTranscript() {{
|
|
alert('PDF-Export wird vorbereitet...');
|
|
// In production: window.location.href = '/api/recordings/{recording_id}/transcript/pdf';
|
|
}}
|
|
</script>
|
|
'''
|
|
|
|
return render_base_page("Protokoll", content, "recordings")
|