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:
284
backend/frontend/meetings/pages/recordings.py
Normal file
284
backend/frontend/meetings/pages/recordings.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user