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:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,27 @@
"""
Meetings Module - Pages
Route handlers for the Meetings frontend
"""
from .dashboard import meetings_dashboard
from .meeting_room import meeting_room
from .active import active_meetings
from .schedule import schedule_meetings
from .trainings import trainings_page
from .recordings import recordings_page, play_recording, view_transcript
from .breakout import breakout_rooms_page
from .quick_actions import quick_meeting, parent_teacher_meeting
__all__ = [
"meetings_dashboard",
"meeting_room",
"active_meetings",
"schedule_meetings",
"trainings_page",
"recordings_page",
"play_recording",
"view_transcript",
"breakout_rooms_page",
"quick_meeting",
"parent_teacher_meeting",
]

View File

@@ -0,0 +1,76 @@
"""
Meetings Module - Active Meetings Page
List of currently active meetings
"""
from ..templates import ICONS, render_base_page
def active_meetings() -> str:
"""Active meetings list"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Aktive Meetings</h1>
<p class="page-subtitle">Laufende Videokonferenzen und Schulungen</p>
</div>
<button class="btn btn-primary" onclick="window.location.href='/meetings/quick'">
{ICONS['plus']} Neues Meeting starten
</button>
</div>
<!-- Active Meetings List -->
<div class="meeting-list" id="activeMeetingsList">
<div style="text-align: center; padding: 3rem; color: var(--bp-text-muted);">
<p>Keine aktiven Meetings</p>
<p style="font-size: 0.875rem; margin-top: 0.5rem;">
Starten Sie ein neues Meeting oder warten Sie, bis ein geplantes Meeting beginnt.
</p>
</div>
</div>
<script>
async function loadActiveMeetings() {{
try {{
const response = await fetch('/api/meetings/active');
if (response.ok) {{
const meetings = await response.json();
renderMeetings(meetings);
}}
}} catch (error) {{
console.error('Error loading active meetings:', error);
}}
}}
function renderMeetings(meetings) {{
const container = document.getElementById('activeMeetingsList');
if (!meetings || meetings.length === 0) {{
return;
}}
container.innerHTML = meetings.map(meeting => `
<div class="meeting-item">
<div class="meeting-time">
<div class="meeting-time-value">${{meeting.participants || 0}}</div>
<div class="meeting-time-date">Teilnehmer</div>
</div>
<div class="meeting-info">
<div class="meeting-title">${{meeting.title}}</div>
<div class="meeting-meta">
<span>{ICONS['clock']} Seit ${{meeting.started_at}}</span>
</div>
</div>
<span class="meeting-badge badge-live">LIVE</span>
<div class="meeting-actions">
<button class="btn btn-primary" onclick="window.location.href='/meetings/room/${{meeting.room_name}}'">Beitreten</button>
</div>
</div>
`).join('');
}}
loadActiveMeetings();
</script>
'''
return render_base_page("Aktive Meetings", content, "active")

View File

@@ -0,0 +1,136 @@
"""
Meetings Module - Breakout Rooms Page
Breakout rooms management
"""
from ..templates import ICONS, render_base_page
def breakout_rooms_page() -> str:
"""Breakout rooms management"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Breakout-Rooms</h1>
<p class="page-subtitle">Gruppenräume für Workshops und Übungen verwalten</p>
</div>
</div>
<!-- Active Meeting Warning -->
<div class="card" style="background: var(--bp-primary-soft); border-color: var(--bp-primary); margin-bottom: 2rem;">
<div style="display: flex; align-items: center; gap: 1rem;">
<div class="card-icon primary">{ICONS['video']}</div>
<div style="flex: 1;">
<div style="font-weight: 600;">Kein aktives Meeting</div>
<div style="font-size: 0.875rem; color: var(--bp-text-muted);">
Breakout-Rooms können nur während eines aktiven Meetings erstellt werden.
</div>
</div>
<button class="btn btn-primary" onclick="window.location.href='/meetings/quick'">
Meeting starten
</button>
</div>
</div>
<!-- How it works -->
<div class="card">
<div class="card-header">
<span class="card-title">So funktionieren Breakout-Rooms</span>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.5rem; margin-top: 1rem;">
<div style="text-align: center;">
<div class="card-icon primary" style="margin: 0 auto 1rem;">{ICONS['grid']}</div>
<h4 style="margin-bottom: 0.5rem;">1. Räume erstellen</h4>
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
Erstellen Sie mehrere Breakout-Rooms für Gruppenarbeit.
</p>
</div>
<div style="text-align: center;">
<div class="card-icon info" style="margin: 0 auto 1rem;">{ICONS['users']}</div>
<h4 style="margin-bottom: 0.5rem;">2. Teilnehmer zuweisen</h4>
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
Weisen Sie Teilnehmer manuell oder automatisch zu.
</p>
</div>
<div style="text-align: center;">
<div class="card-icon accent" style="margin: 0 auto 1rem;">{ICONS['play']}</div>
<h4 style="margin-bottom: 0.5rem;">3. Sessions starten</h4>
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
Starten Sie alle Räume gleichzeitig oder einzeln.
</p>
</div>
<div style="text-align: center;">
<div class="card-icon warning" style="margin: 0 auto 1rem;">{ICONS['clock']}</div>
<h4 style="margin-bottom: 0.5rem;">4. Timer setzen</h4>
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
Setzen Sie einen Timer für automatisches Beenden.
</p>
</div>
</div>
</div>
<!-- Breakout Configuration Preview -->
<div class="card" style="margin-top: 2rem;">
<div class="card-header">
<span class="card-title">Breakout-Konfiguration (Vorschau)</span>
<button class="btn btn-secondary" disabled>
{ICONS['plus']} Raum hinzufügen
</button>
</div>
<div class="breakout-grid" style="margin-top: 1rem;">
<div class="breakout-room" style="opacity: 0.5;">
<div class="breakout-room-header">
<span class="breakout-room-title">Raum 1</span>
<span class="breakout-room-count">0 Teilnehmer</span>
</div>
<div class="breakout-participants">
<span style="color: var(--bp-text-muted); font-size: 0.875rem;">Keine Teilnehmer</span>
</div>
</div>
<div class="breakout-room" style="opacity: 0.5;">
<div class="breakout-room-header">
<span class="breakout-room-title">Raum 2</span>
<span class="breakout-room-count">0 Teilnehmer</span>
</div>
<div class="breakout-participants">
<span style="color: var(--bp-text-muted); font-size: 0.875rem;">Keine Teilnehmer</span>
</div>
</div>
<div class="breakout-room" style="opacity: 0.5;">
<div class="breakout-room-header">
<span class="breakout-room-title">Raum 3</span>
<span class="breakout-room-count">0 Teilnehmer</span>
</div>
<div class="breakout-participants">
<span style="color: var(--bp-text-muted); font-size: 0.875rem;">Keine Teilnehmer</span>
</div>
</div>
</div>
<div style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--bp-border);">
<div class="form-group">
<label class="form-label">Automatische Zuweisung</label>
<select class="form-select" disabled>
<option>Gleichmäßig verteilen</option>
<option>Zufällig zuweisen</option>
<option>Manuell zuweisen</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Timer (Minuten)</label>
<input type="number" class="form-input" value="15" disabled>
</div>
<div class="btn-group">
<button class="btn btn-primary" disabled>Breakout-Sessions starten</button>
<button class="btn btn-secondary" disabled>Alle zurückholen</button>
</div>
</div>
</div>
'''
return render_base_page("Breakout-Rooms", content, "breakout")

View File

@@ -0,0 +1,298 @@
"""
Meetings Module - Dashboard Page
Main meetings dashboard with statistics and quick actions
"""
from ..templates import ICONS, render_base_page
def meetings_dashboard() -> str:
"""Main meetings dashboard"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Meeting Dashboard</h1>
<p class="page-subtitle">Videokonferenzen, Schulungen und Elterngespräche verwalten</p>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="openModal('newMeeting')">
{ICONS['plus']} Neues Meeting
</button>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<a href="/meetings/quick" class="quick-action">
<div class="quick-action-icon card-icon primary">{ICONS['video']}</div>
<span class="quick-action-label">Sofort-Meeting starten</span>
</a>
<a href="/meetings/schedule/new" class="quick-action">
<div class="quick-action-icon card-icon info">{ICONS['calendar']}</div>
<span class="quick-action-label">Meeting planen</span>
</a>
<a href="/meetings/trainings/new" class="quick-action">
<div class="quick-action-icon card-icon accent">{ICONS['graduation']}</div>
<span class="quick-action-label">Schulung erstellen</span>
</a>
<a href="/meetings/parent-teacher" class="quick-action">
<div class="quick-action-icon card-icon warning">{ICONS['users']}</div>
<span class="quick-action-label">Elterngespräch</span>
</a>
</div>
<!-- Statistics Cards -->
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<span class="card-title">Aktive Meetings</span>
<div class="card-icon primary">{ICONS['video']}</div>
</div>
<div class="stat-value" id="activeMeetings">0</div>
<div class="stat-label">Jetzt live</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Geplante Termine</span>
<div class="card-icon info">{ICONS['calendar']}</div>
</div>
<div class="stat-value" id="scheduledMeetings">0</div>
<div class="stat-label">Diese Woche</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Aufzeichnungen</span>
<div class="card-icon accent">{ICONS['record']}</div>
</div>
<div class="stat-value" id="recordings">0</div>
<div class="stat-label">Verfügbar</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Teilnehmer</span>
<div class="card-icon warning">{ICONS['users']}</div>
</div>
<div class="stat-value" id="totalParticipants">0</div>
<div class="stat-label">Diese Woche</div>
</div>
</div>
<!-- Upcoming Meetings -->
<div class="card">
<div class="card-header">
<span class="card-title">Nächste Meetings</span>
<a href="/meetings/schedule" class="btn btn-secondary">Alle anzeigen</a>
</div>
<div class="meeting-list" id="upcomingMeetings">
<div class="meeting-item">
<div class="meeting-time">
<div class="meeting-time-value">14:00</div>
<div class="meeting-time-date">Heute</div>
</div>
<div class="meeting-info">
<div class="meeting-title">Elterngespräch - Max Müller</div>
<div class="meeting-meta">
<span>{ICONS['clock']} 30 Min</span>
<span>{ICONS['users']} 2 Teilnehmer</span>
</div>
</div>
<span class="meeting-badge badge-scheduled">Geplant</span>
<div class="meeting-actions">
<button class="btn btn-primary" onclick="joinMeeting('parent-123')">Beitreten</button>
<button class="btn-icon" onclick="copyMeetingLink('parent-123')">{ICONS['copy']}</button>
</div>
</div>
<div class="meeting-item">
<div class="meeting-time">
<div class="meeting-time-value">15:30</div>
<div class="meeting-time-date">Heute</div>
</div>
<div class="meeting-info">
<div class="meeting-title">Go Grundlagen Schulung</div>
<div class="meeting-meta">
<span>{ICONS['clock']} 120 Min</span>
<span>{ICONS['users']} 12 Teilnehmer</span>
<span>{ICONS['record']} Aufzeichnung</span>
</div>
</div>
<span class="meeting-badge badge-scheduled">Geplant</span>
<div class="meeting-actions">
<button class="btn btn-primary" onclick="joinMeeting('training-456')">Beitreten</button>
<button class="btn-icon" onclick="copyMeetingLink('training-456')">{ICONS['copy']}</button>
</div>
</div>
</div>
</div>
<!-- New Meeting Modal -->
<div class="modal-overlay" id="newMeetingModal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Neues Meeting erstellen</h2>
<button class="modal-close" onclick="closeModal('newMeeting')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Meeting-Typ</label>
<select class="form-select" id="meetingType" onchange="updateMeetingForm()">
<option value="quick">Sofort-Meeting</option>
<option value="scheduled">Geplantes Meeting</option>
<option value="training">Schulung</option>
<option value="parent">Elterngespräch</option>
<option value="class">Klassenkonferenz</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Titel</label>
<input type="text" class="form-input" id="meetingTitle" placeholder="Meeting-Titel eingeben">
</div>
<div class="form-group" id="dateTimeGroup" style="display: none;">
<label class="form-label">Datum & Uhrzeit</label>
<input type="datetime-local" class="form-input" id="meetingDateTime">
</div>
<div class="form-group" id="durationGroup">
<label class="form-label">Dauer (Minuten)</label>
<select class="form-select" id="meetingDuration">
<option value="30">30 Minuten</option>
<option value="60" selected>60 Minuten</option>
<option value="90">90 Minuten</option>
<option value="120">120 Minuten</option>
</select>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="enableLobby" checked> Warteraum aktivieren
</label>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="enableRecording"> Aufzeichnung erlauben
</label>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="muteOnStart" checked> Teilnehmer stummschalten bei Beitritt
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('newMeeting')">Abbrechen</button>
<button class="btn btn-primary" onclick="createMeeting()">Meeting erstellen</button>
</div>
</div>
</div>
<script>
// Modal Functions
function openModal(type) {{
document.getElementById(type + 'Modal').classList.add('active');
}}
function closeModal(type) {{
document.getElementById(type + 'Modal').classList.remove('active');
}}
function updateMeetingForm() {{
const type = document.getElementById('meetingType').value;
const dateTimeGroup = document.getElementById('dateTimeGroup');
if (type === 'quick') {{
dateTimeGroup.style.display = 'none';
}} else {{
dateTimeGroup.style.display = 'block';
}}
}}
// Meeting Functions
async function createMeeting() {{
const type = document.getElementById('meetingType').value;
const title = document.getElementById('meetingTitle').value || 'Neues Meeting';
const duration = document.getElementById('meetingDuration').value;
const enableLobby = document.getElementById('enableLobby').checked;
const enableRecording = document.getElementById('enableRecording').checked;
const muteOnStart = document.getElementById('muteOnStart').checked;
const payload = {{
type: type,
title: title,
duration: parseInt(duration),
config: {{
enable_lobby: enableLobby,
enable_recording: enableRecording,
start_with_audio_muted: muteOnStart
}}
}};
try {{
const response = await fetch('/api/meetings/create', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(payload)
}});
if (response.ok) {{
const data = await response.json();
closeModal('newMeeting');
if (type === 'quick') {{
window.location.href = '/meetings/room/' + data.room_name;
}} else {{
alert('Meeting erfolgreich erstellt!\\nLink: ' + data.join_url);
loadUpcomingMeetings();
}}
}} else {{
alert('Fehler beim Erstellen des Meetings');
}}
}} catch (error) {{
console.error('Error:', error);
alert('Fehler beim Erstellen des Meetings');
}}
}}
function joinMeeting(roomId) {{
window.location.href = '/meetings/room/' + roomId;
}}
function copyMeetingLink(roomId) {{
const link = window.location.origin + '/meetings/room/' + roomId;
navigator.clipboard.writeText(link).then(() => {{
alert('Link kopiert!');
}});
}}
// Load Data
async function loadDashboardData() {{
try {{
const response = await fetch('/api/meetings/stats');
if (response.ok) {{
const data = await response.json();
document.getElementById('activeMeetings').textContent = data.active || 0;
document.getElementById('scheduledMeetings').textContent = data.scheduled || 0;
document.getElementById('recordings').textContent = data.recordings || 0;
document.getElementById('totalParticipants').textContent = data.participants || 0;
}}
}} catch (error) {{
console.error('Error loading stats:', error);
}}
}}
async function loadUpcomingMeetings() {{
// In production, load from API
console.log('Loading upcoming meetings...');
}}
// Initialize
loadDashboardData();
</script>
'''
return render_base_page("Dashboard", content, "dashboard")

View File

@@ -0,0 +1,266 @@
"""
Meetings Module - Meeting Room Page
Meeting room with embedded Jitsi
"""
from ..templates import ICONS, render_base_page
def meeting_room(room_name: str) -> str:
"""Meeting room with embedded Jitsi"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Meeting: {room_name}</h1>
<p class="page-subtitle">Verbunden mit BreakPilot Meet</p>
</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="toggleFullscreen()">
Vollbild
</button>
<button class="btn btn-danger" onclick="leaveMeeting()">
{ICONS['phone_off']} Verlassen
</button>
</div>
</div>
<!-- Video Container -->
<div class="video-container" id="jitsiContainer">
<div class="video-placeholder">
{ICONS['video']}
<p>Meeting wird geladen...</p>
</div>
</div>
<!-- Meeting Controls -->
<div class="meeting-controls">
<button class="control-btn active" id="micBtn" onclick="toggleMic()">
{ICONS['mic']}
</button>
<button class="control-btn active" id="videoBtn" onclick="toggleVideo()">
{ICONS['video']}
</button>
<button class="control-btn inactive" id="screenBtn" onclick="toggleScreenShare()">
{ICONS['screen_share']}
</button>
<button class="control-btn inactive" id="chatBtn" onclick="toggleChat()">
{ICONS['chat']}
</button>
<button class="control-btn inactive" id="recordBtn" onclick="toggleRecording()">
{ICONS['record']}
</button>
<button class="control-btn danger" onclick="leaveMeeting()">
{ICONS['phone_off']}
</button>
</div>
<!-- Participants and Chat -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
<!-- Participants Panel -->
<div class="participants-panel">
<div style="font-weight: 600; margin-bottom: 1rem;">Teilnehmer (0)</div>
<div id="participantsList">
<div class="participant-item">
<div class="participant-avatar">Sie</div>
<div class="participant-info">
<div class="participant-name">Sie (Moderator)</div>
<div class="participant-role">Host</div>
</div>
<div class="participant-status">
<span class="status-indicator mic-on" title="Mikrofon an"></span>
<span class="status-indicator video-on" title="Kamera an"></span>
</div>
</div>
</div>
</div>
<!-- Chat Panel -->
<div class="chat-panel">
<div class="chat-header">Chat</div>
<div class="chat-messages" id="chatMessages">
<div class="chat-message">
<div class="chat-message-header">
<span class="chat-message-sender">System</span>
<span class="chat-message-time">Jetzt</span>
</div>
<div class="chat-message-content">Willkommen im Meeting!</div>
</div>
</div>
<div class="chat-input-area">
<input type="text" class="chat-input" id="chatInput" placeholder="Nachricht eingeben..." onkeypress="if(event.key==='Enter')sendMessage()">
<button class="btn btn-primary" onclick="sendMessage()">Senden</button>
</div>
</div>
</div>
<!-- Jitsi Integration Script -->
<script src="https://meet.jit.si/external_api.js"></script>
<script>
let api = null;
let isMuted = false;
let isVideoOff = false;
let isScreenSharing = false;
let isRecording = false;
// Initialize Jitsi
function initJitsi() {{
const domain = 'meet.jit.si';
const options = {{
roomName: 'BreakPilot-{room_name}',
width: '100%',
height: '100%',
parentNode: document.getElementById('jitsiContainer'),
configOverwrite: {{
startWithAudioMuted: false,
startWithVideoMuted: false,
enableWelcomePage: false,
prejoinPageEnabled: false,
}},
interfaceConfigOverwrite: {{
TOOLBAR_BUTTONS: [],
SHOW_JITSI_WATERMARK: false,
SHOW_BRAND_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false,
DEFAULT_BACKGROUND: '#1a1a1a',
DISABLE_VIDEO_BACKGROUND: true,
}},
userInfo: {{
displayName: 'Lehrer'
}}
}};
api = new JitsiMeetExternalAPI(domain, options);
// Event Listeners
api.addListener('participantJoined', (participant) => {{
console.log('Participant joined:', participant);
updateParticipantsList();
}});
api.addListener('participantLeft', (participant) => {{
console.log('Participant left:', participant);
updateParticipantsList();
}});
api.addListener('readyToClose', () => {{
window.location.href = '/meetings';
}});
}}
// Control Functions
function toggleMic() {{
if (api) {{
api.executeCommand('toggleAudio');
isMuted = !isMuted;
updateControlButton('micBtn', !isMuted);
}}
}}
function toggleVideo() {{
if (api) {{
api.executeCommand('toggleVideo');
isVideoOff = !isVideoOff;
updateControlButton('videoBtn', !isVideoOff);
}}
}}
function toggleScreenShare() {{
if (api) {{
api.executeCommand('toggleShareScreen');
isScreenSharing = !isScreenSharing;
updateControlButton('screenBtn', isScreenSharing);
}}
}}
function toggleChat() {{
if (api) {{
api.executeCommand('toggleChat');
}}
}}
function toggleRecording() {{
if (api) {{
if (!isRecording) {{
api.executeCommand('startRecording', {{
mode: 'file'
}});
}} else {{
api.executeCommand('stopRecording', 'file');
}}
isRecording = !isRecording;
updateControlButton('recordBtn', isRecording);
}}
}}
function updateControlButton(btnId, isActive) {{
const btn = document.getElementById(btnId);
if (isActive) {{
btn.classList.remove('inactive');
btn.classList.add('active');
}} else {{
btn.classList.remove('active');
btn.classList.add('inactive');
}}
}}
function leaveMeeting() {{
if (confirm('Meeting wirklich verlassen?')) {{
if (api) {{
api.executeCommand('hangup');
}}
window.location.href = '/meetings';
}}
}}
function toggleFullscreen() {{
const container = document.getElementById('jitsiContainer');
if (document.fullscreenElement) {{
document.exitFullscreen();
}} else {{
container.requestFullscreen();
}}
}}
// Chat Functions
function sendMessage() {{
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (message && api) {{
api.executeCommand('sendChatMessage', message);
addChatMessage('Sie', message);
input.value = '';
}}
}}
function addChatMessage(sender, text) {{
const container = document.getElementById('chatMessages');
const now = new Date().toLocaleTimeString('de-DE', {{ hour: '2-digit', minute: '2-digit' }});
container.innerHTML += `
<div class="chat-message">
<div class="chat-message-header">
<span class="chat-message-sender">${{sender}}</span>
<span class="chat-message-time">${{now}}</span>
</div>
<div class="chat-message-content">${{text}}</div>
</div>
`;
container.scrollTop = container.scrollHeight;
}}
function updateParticipantsList() {{
if (api) {{
const participants = api.getParticipantsInfo();
console.log('Participants:', participants);
// Update UI with participants
}}
}}
// Initialize on page load
document.addEventListener('DOMContentLoaded', initJitsi);
</script>
'''
return render_base_page(f"Meeting: {room_name}", content, "active")

View File

@@ -0,0 +1,138 @@
"""
Meetings Module - Quick Actions Pages
Quick meeting start and parent-teacher meeting creation
"""
import uuid
from ..templates import ICONS, render_base_page
def quick_meeting() -> str:
"""Start a quick meeting immediately"""
room_name = f"quick-{uuid.uuid4().hex[:8]}"
content = f'''
<div style="text-align: center; padding: 3rem;">
<h1 class="page-title">Sofort-Meeting wird gestartet...</h1>
<p class="page-subtitle">Sie werden in wenigen Sekunden weitergeleitet.</p>
<div style="margin-top: 2rem;">
<div class="card-icon primary" style="margin: 0 auto; width: 80px; height: 80px; font-size: 2rem;">
{ICONS['video']}
</div>
</div>
</div>
<script>
setTimeout(() => {{
window.location.href = '/meetings/room/{room_name}';
}}, 1000);
</script>
'''
return render_base_page("Sofort-Meeting", content, "active")
def parent_teacher_meeting() -> str:
"""Create a parent-teacher meeting"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Elterngespräch planen</h1>
<p class="page-subtitle">Sicheres Meeting mit Warteraum und Passwort</p>
</div>
</div>
<div class="card" style="max-width: 600px;">
<div class="form-group">
<label class="form-label">Schüler/in</label>
<input type="text" class="form-input" id="studentName" placeholder="Name des Schülers/der Schülerin">
</div>
<div class="form-group">
<label class="form-label">Elternteil</label>
<input type="text" class="form-input" id="parentName" placeholder="Name der Eltern">
</div>
<div class="form-group">
<label class="form-label">E-Mail (für Einladung)</label>
<input type="email" class="form-input" id="parentEmail" placeholder="eltern@example.com">
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label class="form-label">Datum</label>
<input type="date" class="form-input" id="meetingDate">
</div>
<div class="form-group">
<label class="form-label">Uhrzeit</label>
<input type="time" class="form-input" id="meetingTime">
</div>
</div>
<div class="form-group">
<label class="form-label">Anlass (optional)</label>
<textarea class="form-textarea" id="meetingReason" placeholder="Grund für das Gespräch..."></textarea>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="sendInvite" checked> Einladung per E-Mail senden
</label>
</div>
<div class="btn-group" style="margin-top: 1.5rem;">
<button class="btn btn-secondary" onclick="window.location.href='/meetings'">Abbrechen</button>
<button class="btn btn-primary" onclick="createParentTeacherMeeting()">Termin erstellen</button>
</div>
</div>
<script>
// Set minimum date to today
document.getElementById('meetingDate').min = new Date().toISOString().split('T')[0];
async function createParentTeacherMeeting() {{
const studentName = document.getElementById('studentName').value;
const parentName = document.getElementById('parentName').value;
const parentEmail = document.getElementById('parentEmail').value;
const date = document.getElementById('meetingDate').value;
const time = document.getElementById('meetingTime').value;
const reason = document.getElementById('meetingReason').value;
const sendInvite = document.getElementById('sendInvite').checked;
if (!studentName || !parentName || !date || !time) {{
alert('Bitte füllen Sie alle Pflichtfelder aus.');
return;
}}
const payload = {{
type: 'parent-teacher',
student_name: studentName,
parent_name: parentName,
parent_email: parentEmail,
scheduled_at: `${{date}}T${{time}}`,
reason: reason,
send_invite: sendInvite,
duration: 30
}};
try {{
const response = await fetch('/api/meetings/parent-teacher', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(payload)
}});
if (response.ok) {{
const data = await response.json();
alert('Elterngespräch erfolgreich geplant!\\n\\nLink: ' + data.join_url + '\\nPasswort: ' + data.password);
window.location.href = '/meetings/schedule';
}}
}} catch (error) {{
console.error('Error:', error);
alert('Fehler beim Erstellen des Termins');
}}
}}
</script>
'''
return render_base_page("Elterngespräch", content, "schedule")

View 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")

View File

@@ -0,0 +1,206 @@
"""
Meetings Module - Schedule Page
Schedule and manage upcoming meetings
"""
from ..templates import ICONS, render_base_page
def schedule_meetings() -> str:
"""Schedule and manage upcoming meetings"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Termine</h1>
<p class="page-subtitle">Geplante Meetings und Termine verwalten</p>
</div>
<button class="btn btn-primary" onclick="openScheduleModal()">
{ICONS['plus']} Meeting planen
</button>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" onclick="filterMeetings('all')">Alle</button>
<button class="tab" onclick="filterMeetings('today')">Heute</button>
<button class="tab" onclick="filterMeetings('week')">Diese Woche</button>
<button class="tab" onclick="filterMeetings('month')">Dieser Monat</button>
</div>
<div class="meeting-list" id="scheduledMeetings">
<div class="meeting-item">
<div class="meeting-time">
<div class="meeting-time-value">14:00</div>
<div class="meeting-time-date">Mo, 16.12.</div>
</div>
<div class="meeting-info">
<div class="meeting-title">Team-Besprechung</div>
<div class="meeting-meta">
<span>{ICONS['clock']} 60 Min</span>
<span>{ICONS['users']} 5 Teilnehmer</span>
</div>
</div>
<span class="meeting-badge badge-scheduled">Geplant</span>
<div class="meeting-actions">
<button class="btn btn-primary" onclick="joinMeeting('team-abc')">Beitreten</button>
<button class="btn-icon" onclick="editMeeting('team-abc')">{ICONS['settings']}</button>
<button class="btn-icon" onclick="copyLink('team-abc')">{ICONS['link']}</button>
<button class="btn-icon" onclick="deleteMeeting('team-abc')">{ICONS['trash']}</button>
</div>
</div>
<div class="meeting-item">
<div class="meeting-time">
<div class="meeting-time-value">09:30</div>
<div class="meeting-time-date">Di, 17.12.</div>
</div>
<div class="meeting-info">
<div class="meeting-title">Elterngespräch - Anna Schmidt</div>
<div class="meeting-meta">
<span>{ICONS['clock']} 30 Min</span>
<span>{ICONS['users']} 2 Teilnehmer</span>
</div>
</div>
<span class="meeting-badge badge-scheduled">Geplant</span>
<div class="meeting-actions">
<button class="btn btn-primary" onclick="joinMeeting('parent-xyz')">Beitreten</button>
<button class="btn-icon" onclick="editMeeting('parent-xyz')">{ICONS['settings']}</button>
<button class="btn-icon" onclick="copyLink('parent-xyz')">{ICONS['link']}</button>
<button class="btn-icon" onclick="deleteMeeting('parent-xyz')">{ICONS['trash']}</button>
</div>
</div>
</div>
<!-- Schedule Modal -->
<div class="modal-overlay" id="scheduleModal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Meeting planen</h2>
<button class="modal-close" onclick="closeScheduleModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Titel</label>
<input type="text" class="form-input" id="scheduleTitle" placeholder="Meeting-Titel">
</div>
<div class="form-group">
<label class="form-label">Datum</label>
<input type="date" class="form-input" id="scheduleDate">
</div>
<div class="form-group">
<label class="form-label">Uhrzeit</label>
<input type="time" class="form-input" id="scheduleTime">
</div>
<div class="form-group">
<label class="form-label">Dauer</label>
<select class="form-select" id="scheduleDuration">
<option value="30">30 Minuten</option>
<option value="60" selected>60 Minuten</option>
<option value="90">90 Minuten</option>
<option value="120">120 Minuten</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Beschreibung (optional)</label>
<textarea class="form-textarea" id="scheduleDescription" placeholder="Agenda oder Beschreibung..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Teilnehmer einladen</label>
<input type="email" class="form-input" id="scheduleInvites" placeholder="E-Mail-Adressen (kommagetrennt)">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeScheduleModal()">Abbrechen</button>
<button class="btn btn-primary" onclick="scheduleMeeting()">Meeting planen</button>
</div>
</div>
</div>
<script>
function openScheduleModal() {{
document.getElementById('scheduleModal').classList.add('active');
// Set default date to today
document.getElementById('scheduleDate').valueAsDate = new Date();
}}
function closeScheduleModal() {{
document.getElementById('scheduleModal').classList.remove('active');
}}
async function scheduleMeeting() {{
const title = document.getElementById('scheduleTitle').value;
const date = document.getElementById('scheduleDate').value;
const time = document.getElementById('scheduleTime').value;
const duration = document.getElementById('scheduleDuration').value;
const description = document.getElementById('scheduleDescription').value;
const invites = document.getElementById('scheduleInvites').value;
if (!title || !date || !time) {{
alert('Bitte füllen Sie alle Pflichtfelder aus.');
return;
}}
const payload = {{
title,
scheduled_at: `${{date}}T${{time}}`,
duration: parseInt(duration),
description,
invites: invites.split(',').map(e => e.trim()).filter(e => e)
}};
try {{
const response = await fetch('/api/meetings/schedule', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(payload)
}});
if (response.ok) {{
alert('Meeting erfolgreich geplant!');
closeScheduleModal();
location.reload();
}}
}} catch (error) {{
console.error('Error:', error);
}}
}}
function filterMeetings(filter) {{
// Update active tab
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
// Filter logic would go here
console.log('Filter:', filter);
}}
function joinMeeting(roomId) {{
window.location.href = '/meetings/room/' + roomId;
}}
function editMeeting(roomId) {{
// Open edit modal
console.log('Edit:', roomId);
}}
function copyLink(roomId) {{
const link = window.location.origin + '/meetings/room/' + roomId;
navigator.clipboard.writeText(link);
alert('Link kopiert!');
}}
function deleteMeeting(roomId) {{
if (confirm('Meeting wirklich löschen?')) {{
// Delete logic
console.log('Delete:', roomId);
}}
}}
</script>
'''
return render_base_page("Termine", content, "schedule")

View File

@@ -0,0 +1,267 @@
"""
Meetings Module - Trainings Page
Training sessions management
"""
from ..templates import ICONS, render_base_page
def trainings_page() -> str:
"""Training sessions management"""
content = f'''
<div class="page-header">
<div>
<h1 class="page-title">Schulungen</h1>
<p class="page-subtitle">Schulungen und Workshops verwalten</p>
</div>
<button class="btn btn-primary" onclick="openTrainingModal()">
{ICONS['plus']} Neue Schulung
</button>
</div>
<!-- Training Cards -->
<div class="dashboard-grid">
<div class="training-card">
<div class="training-card-header">
<div class="training-card-title">Go Grundlagen Workshop</div>
<div class="training-card-subtitle">Einführung in die Go-Programmierung</div>
</div>
<div class="training-card-body">
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
{ICONS['calendar']} 18.12.2025, 14:00
</span>
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
{ICONS['clock']} 120 Min
</span>
</div>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<span class="meeting-badge badge-scheduled">Geplant</span>
<span class="meeting-badge" style="background: var(--bp-accent-soft); color: var(--bp-accent);">Aufzeichnung</span>
</div>
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
12 von 20 Teilnehmern angemeldet
</p>
</div>
<div class="training-card-footer">
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">Trainer: Max Mustermann</span>
<div class="btn-group">
<button class="btn btn-secondary" onclick="editTraining('go-basics')">{ICONS['settings']}</button>
<button class="btn btn-primary" onclick="startTraining('go-basics')">Starten</button>
</div>
</div>
</div>
<div class="training-card">
<div class="training-card-header">
<div class="training-card-title">PWA Entwicklung</div>
<div class="training-card-subtitle">Progressive Web Apps mit JavaScript</div>
</div>
<div class="training-card-body">
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
{ICONS['calendar']} 20.12.2025, 10:00
</span>
<span style="display: flex; align-items: center; gap: 0.5rem; color: var(--bp-text-muted);">
{ICONS['clock']} 180 Min
</span>
</div>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<span class="meeting-badge badge-scheduled">Geplant</span>
<span class="meeting-badge" style="background: var(--bp-accent-soft); color: var(--bp-accent);">Aufzeichnung</span>
<span class="meeting-badge" style="background: var(--bp-info); color: white;">Breakout</span>
</div>
<p style="font-size: 0.875rem; color: var(--bp-text-muted);">
8 von 15 Teilnehmern angemeldet
</p>
</div>
<div class="training-card-footer">
<span style="font-size: 0.875rem; color: var(--bp-text-muted);">Trainer: Lisa Schmidt</span>
<div class="btn-group">
<button class="btn btn-secondary" onclick="editTraining('pwa-dev')">{ICONS['settings']}</button>
<button class="btn btn-primary" onclick="startTraining('pwa-dev')">Starten</button>
</div>
</div>
</div>
</div>
<!-- Past Trainings -->
<div class="card" style="margin-top: 2rem;">
<div class="card-header">
<span class="card-title">Vergangene Schulungen</span>
</div>
<div class="meeting-list">
<div class="meeting-item">
<div class="meeting-time">
<div class="meeting-time-value">10:00</div>
<div class="meeting-time-date">10.12.</div>
</div>
<div class="meeting-info">
<div class="meeting-title">Docker Grundlagen</div>
<div class="meeting-meta">
<span>{ICONS['clock']} 90 Min</span>
<span>{ICONS['users']} 15 Teilnehmer</span>
<span>{ICONS['record']} Aufzeichnung verfügbar</span>
</div>
</div>
<span class="meeting-badge badge-ended">Beendet</span>
<div class="meeting-actions">
<button class="btn btn-secondary" onclick="viewRecording('docker-basics')">{ICONS['play']} Ansehen</button>
<button class="btn-icon" onclick="downloadRecording('docker-basics')">{ICONS['download']}</button>
</div>
</div>
</div>
</div>
<!-- Training Modal -->
<div class="modal-overlay" id="trainingModal">
<div class="modal" style="max-width: 600px;">
<div class="modal-header">
<h2 class="modal-title">Neue Schulung erstellen</h2>
<button class="modal-close" onclick="closeTrainingModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Titel</label>
<input type="text" class="form-input" id="trainingTitle" placeholder="Schulungstitel">
</div>
<div class="form-group">
<label class="form-label">Beschreibung</label>
<textarea class="form-textarea" id="trainingDescription" placeholder="Beschreibung der Schulung..."></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label class="form-label">Datum</label>
<input type="date" class="form-input" id="trainingDate">
</div>
<div class="form-group">
<label class="form-label">Uhrzeit</label>
<input type="time" class="form-input" id="trainingTime">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label class="form-label">Dauer (Minuten)</label>
<select class="form-select" id="trainingDuration">
<option value="60">60 Minuten</option>
<option value="90">90 Minuten</option>
<option value="120" selected>120 Minuten</option>
<option value="180">180 Minuten</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Max. Teilnehmer</label>
<input type="number" class="form-input" id="trainingMaxParticipants" value="20">
</div>
</div>
<div class="form-group">
<label class="form-label">Trainer</label>
<input type="text" class="form-input" id="trainingTrainer" placeholder="Name des Trainers">
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="trainingRecording" checked> Aufzeichnung aktivieren
</label>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="trainingBreakout"> Breakout-Rooms aktivieren
</label>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="trainingLobby" checked> Warteraum aktivieren (Trainer lässt Teilnehmer ein)
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeTrainingModal()">Abbrechen</button>
<button class="btn btn-primary" onclick="createTraining()">Schulung erstellen</button>
</div>
</div>
</div>
<script>
function openTrainingModal() {{
document.getElementById('trainingModal').classList.add('active');
}}
function closeTrainingModal() {{
document.getElementById('trainingModal').classList.remove('active');
}}
async function createTraining() {{
const title = document.getElementById('trainingTitle').value;
const description = document.getElementById('trainingDescription').value;
const date = document.getElementById('trainingDate').value;
const time = document.getElementById('trainingTime').value;
const duration = document.getElementById('trainingDuration').value;
const maxParticipants = document.getElementById('trainingMaxParticipants').value;
const trainer = document.getElementById('trainingTrainer').value;
const enableRecording = document.getElementById('trainingRecording').checked;
const enableBreakout = document.getElementById('trainingBreakout').checked;
const enableLobby = document.getElementById('trainingLobby').checked;
if (!title || !date || !time || !trainer) {{
alert('Bitte füllen Sie alle Pflichtfelder aus.');
return;
}}
const payload = {{
title,
description,
scheduled_at: `${{date}}T${{time}}`,
duration: parseInt(duration),
max_participants: parseInt(maxParticipants),
trainer,
config: {{
enable_recording: enableRecording,
enable_breakout: enableBreakout,
enable_lobby: enableLobby,
start_with_audio_muted: true
}}
}};
try {{
const response = await fetch('/api/meetings/training', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(payload)
}});
if (response.ok) {{
alert('Schulung erfolgreich erstellt!');
closeTrainingModal();
location.reload();
}}
}} catch (error) {{
console.error('Error:', error);
}}
}}
function startTraining(trainingId) {{
window.location.href = '/meetings/room/training-' + trainingId;
}}
function editTraining(trainingId) {{
console.log('Edit training:', trainingId);
}}
function viewRecording(trainingId) {{
window.location.href = '/meetings/recordings/' + trainingId;
}}
function downloadRecording(trainingId) {{
console.log('Download recording:', trainingId);
}}
</script>
'''
return render_base_page("Schulungen", content, "trainings")