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>
299 lines
11 KiB
Python
299 lines
11 KiB
Python
"""
|
|
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')">×</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")
|