This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/modules/jitsi.py
Benjamin Admin bfdaf63ba9 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>
2026-02-09 09:51:32 +01:00

688 lines
18 KiB
Python

"""
BreakPilot Studio - Jitsi Videokonferenz Modul
Funktionen:
- Elterngespraeche (mit Lobby und Passwort)
- Klassenkonferenzen
- Schulungen (mit Aufzeichnung)
- Schnelle Meetings
Nutzt die Jitsi-API unter /api/meetings/*
"""
class JitsiModule:
"""Jitsi Videokonferenz Modul fuer BreakPilot Studio."""
@staticmethod
def get_css() -> str:
"""CSS fuer das Jitsi-Modul."""
return """
/* ==========================================
JITSI MODULE STYLES
========================================== */
/* Panel - hidden by default */
.panel-jitsi {
display: none;
flex-direction: column;
height: 100%;
background: var(--bp-bg);
overflow: hidden;
}
.panel-jitsi.active {
display: flex;
}
.jitsi-container {
padding: 24px;
flex: 1;
overflow-y: auto;
}
.jitsi-header {
margin-bottom: 24px;
}
.jitsi-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.jitsi-subtitle {
color: var(--bp-text-muted);
font-size: 14px;
}
/* Meeting Type Cards */
.meeting-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 32px;
}
.meeting-type-card {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: all 0.2s;
}
.meeting-type-card:hover {
border-color: var(--bp-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.meeting-type-icon {
font-size: 32px;
margin-bottom: 12px;
}
.meeting-type-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.meeting-type-desc {
font-size: 13px;
color: var(--bp-text-muted);
margin-bottom: 16px;
}
.meeting-type-features {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.meeting-feature {
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
background: var(--bp-bg);
color: var(--bp-text-muted);
}
.meeting-feature.highlight {
background: var(--bp-accent-soft);
color: var(--bp-accent);
}
/* Active Meetings */
.active-meetings {
margin-bottom: 32px;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section-badge {
font-size: 12px;
padding: 2px 8px;
border-radius: 999px;
background: var(--bp-accent);
color: white;
}
.meetings-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.meeting-item {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
padding: 16px;
}
.meeting-info {
display: flex;
align-items: center;
gap: 16px;
}
.meeting-status {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--bp-success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.meeting-details h4 {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
.meeting-meta {
font-size: 12px;
color: var(--bp-text-muted);
}
.meeting-actions {
display: flex;
gap: 8px;
}
/* Create Meeting Form */
.create-meeting-form {
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 12px;
padding: 24px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
/* Jitsi Embed */
.jitsi-embed-container {
display: none;
position: fixed;
top: 56px;
left: var(--sidebar-width);
right: 0;
bottom: 0;
background: #000;
z-index: 90;
}
.jitsi-embed-container.active {
display: block;
}
.jitsi-embed-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50px;
background: var(--bp-surface);
border-bottom: 1px solid var(--bp-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 10;
}
.jitsi-embed-title {
font-weight: 600;
}
.jitsi-iframe {
width: 100%;
height: calc(100% - 50px);
margin-top: 50px;
border: none;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 48px;
color: var(--bp-text-muted);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state-text {
font-size: 14px;
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer das Jitsi-Modul."""
return """
<!-- Jitsi Panel -->
<div id="panel-jitsi" class="panel-jitsi" style="display: none;">
<div class="jitsi-container" id="jitsi-module">
<div class="jitsi-header">
<h1 class="jitsi-title">&#127909; Videokonferenzen</h1>
<p class="jitsi-subtitle">Sichere Videokonferenzen fuer Elterngespraeche, Klassenkonferenzen und Schulungen</p>
</div>
<!-- Meeting Types -->
<div class="meeting-types">
<div class="meeting-type-card" onclick="showParentMeetingForm()">
<div class="meeting-type-icon">&#128106;</div>
<div class="meeting-type-title">Elterngespraech</div>
<div class="meeting-type-desc">Vertrauliche Gespraeche mit Eltern - mit Lobby und Passwortschutz</div>
<div class="meeting-type-features">
<span class="meeting-feature highlight">Lobby</span>
<span class="meeting-feature highlight">Passwort</span>
<span class="meeting-feature">30 Min</span>
</div>
</div>
<div class="meeting-type-card" onclick="showClassMeetingForm()">
<div class="meeting-type-icon">&#127979;</div>
<div class="meeting-type-title">Klassenkonferenz</div>
<div class="meeting-type-desc">Virtuelle Klassen-Meetings mit allen Schuelern</div>
<div class="meeting-type-features">
<span class="meeting-feature">Screen Sharing</span>
<span class="meeting-feature">Chat</span>
<span class="meeting-feature">45 Min</span>
</div>
</div>
<div class="meeting-type-card" onclick="showTrainingForm()">
<div class="meeting-type-icon">&#127891;</div>
<div class="meeting-type-title">Schulung</div>
<div class="meeting-type-desc">Fortbildungen und Workshops mit Aufzeichnung</div>
<div class="meeting-type-features">
<span class="meeting-feature highlight">Aufzeichnung</span>
<span class="meeting-feature">Praesentation</span>
<span class="meeting-feature">90 Min</span>
</div>
</div>
<div class="meeting-type-card" onclick="startQuickMeeting()">
<div class="meeting-type-icon">&#9889;</div>
<div class="meeting-type-title">Schnelles Meeting</div>
<div class="meeting-type-desc">Sofort starten ohne Konfiguration</div>
<div class="meeting-type-features">
<span class="meeting-feature">Sofort</span>
<span class="meeting-feature">Keine Anmeldung</span>
</div>
</div>
</div>
<!-- Active Meetings -->
<div class="active-meetings">
<h2 class="section-title">
Aktive Meetings
<span class="section-badge" id="active-meetings-count">0</span>
</h2>
<div class="meetings-list" id="meetings-list">
<div class="empty-state">
<div class="empty-state-icon">&#128247;</div>
<p class="empty-state-text">Keine aktiven Meetings. Starten Sie ein neues Meeting oben.</p>
</div>
</div>
</div>
<!-- Create Parent Meeting Form (hidden by default) -->
<div class="create-meeting-form hidden" id="parent-meeting-form">
<h3 style="margin-bottom: 16px;">Elterngespraech planen</h3>
<form onsubmit="createParentMeeting(event)">
<div class="form-row">
<div class="form-group">
<label class="form-label">Name des Schuelers / der Schuelerin</label>
<input type="text" class="form-input" id="pm-student-name" placeholder="Max Mustermann" required>
</div>
<div class="form-group">
<label class="form-label">Name der Eltern</label>
<input type="text" class="form-input" id="pm-parent-name" placeholder="Familie Mustermann">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Datum</label>
<input type="date" class="form-input" id="pm-date" required>
</div>
<div class="form-group">
<label class="form-label">Uhrzeit</label>
<input type="time" class="form-input" id="pm-time" value="14:00" required>
</div>
</div>
<div class="form-group">
<label class="form-label">Anlass / Thema</label>
<input type="text" class="form-input" id="pm-topic" placeholder="z.B. Halbjahresgespraech, Leistungsstand">
</div>
<div style="display: flex; gap: 12px; margin-top: 16px;">
<button type="submit" class="btn btn-primary">Meeting erstellen</button>
<button type="button" class="btn btn-ghost" onclick="hideParentMeetingForm()">Abbrechen</button>
</div>
</form>
</div>
</div>
<!-- Jitsi Embed Container -->
<div class="jitsi-embed-container" id="jitsi-embed">
<div class="jitsi-embed-header">
<span class="jitsi-embed-title" id="jitsi-embed-title">Meeting</span>
<button class="btn btn-sm btn-ghost" onclick="closeJitsiEmbed()">&#10005; Schliessen</button>
</div>
<iframe id="jitsi-iframe" class="jitsi-iframe" allow="camera; microphone; display-capture; autoplay; clipboard-write"></iframe>
</div>
</div><!-- /panel-jitsi -->
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer das Jitsi-Modul."""
return """
// ==========================================
// JITSI MODULE
// ==========================================
console.log('Jitsi Module loaded');
const JITSI_BASE_URL = 'https://meet.jit.si'; // oder eigener Server
// ==========================================
// MODULE LOADER
// ==========================================
function loadJitsiModule() {
console.log('Initializing Jitsi Module');
loadActiveMeetings();
// Set default date to today
const dateInput = document.getElementById('pm-date');
if (dateInput) {
dateInput.valueAsDate = new Date();
}
}
// ==========================================
// MEETING FORMS
// ==========================================
function showParentMeetingForm() {
document.getElementById('parent-meeting-form').classList.remove('hidden');
document.getElementById('pm-student-name').focus();
}
function hideParentMeetingForm() {
document.getElementById('parent-meeting-form').classList.add('hidden');
}
function showClassMeetingForm() {
const className = prompt('Klassenname (z.B. 7a):');
if (!className) return;
const roomName = 'klasse-' + className.toLowerCase().replace(/[^a-z0-9]/g, '') + '-' + Date.now();
createAndOpenMeeting(roomName, 'Klassenkonferenz ' + className, {
startWithAudioMuted: true,
startWithVideoMuted: false
});
}
function showTrainingForm() {
const topic = prompt('Thema der Schulung:');
if (!topic) return;
const roomName = 'schulung-' + topic.toLowerCase().replace(/[^a-z0-9]/g, '-').substring(0, 30) + '-' + Date.now();
createAndOpenMeeting(roomName, 'Schulung: ' + topic, {
startWithAudioMuted: false,
startWithVideoMuted: false,
enableRecording: true
});
}
function startQuickMeeting() {
const roomName = 'bp-quick-' + Date.now();
createAndOpenMeeting(roomName, 'Schnelles Meeting', {
startWithAudioMuted: false,
startWithVideoMuted: false
});
}
// ==========================================
// CREATE MEETINGS
// ==========================================
function createParentMeeting(event) {
event.preventDefault();
const studentName = document.getElementById('pm-student-name').value;
const parentName = document.getElementById('pm-parent-name').value;
const date = document.getElementById('pm-date').value;
const time = document.getElementById('pm-time').value;
const topic = document.getElementById('pm-topic').value;
// Generate room name
const sanitizedName = studentName.toLowerCase()
.replace(/ae/g, 'ae').replace(/oe/g, 'oe').replace(/ue/g, 'ue')
.replace(/[^a-z0-9]/g, '-');
const roomName = 'elterngespraech-' + sanitizedName + '-' + date.replace(/-/g, '');
// Generate password
const password = Math.random().toString(36).substring(2, 10);
console.log('Creating parent meeting:', { roomName, studentName, parentName, date, time, topic });
// Call API to create meeting
fetch('/api/meetings/parent-teacher', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (localStorage.getItem('bp-token') || '')
},
body: JSON.stringify({
student_name: studentName,
parent_name: parentName,
scheduled_date: date,
scheduled_time: time,
topic: topic,
password: password
})
})
.then(response => response.json())
.then(data => {
if (data.meeting_url || data.room_name) {
hideParentMeetingForm();
showMeetingCreatedDialog({
roomName: data.room_name || roomName,
meetingUrl: data.meeting_url || JITSI_BASE_URL + '/' + roomName,
password: data.password || password,
studentName: studentName,
date: date,
time: time
});
loadActiveMeetings();
} else {
alert('Fehler beim Erstellen: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(err => {
console.error('Error creating meeting:', err);
// Fallback: Open directly
createAndOpenMeeting(roomName, 'Elterngespraech: ' + studentName, {
startWithAudioMuted: false,
startWithVideoMuted: false,
password: password,
lobbyEnabled: true
});
});
}
function createAndOpenMeeting(roomName, title, options = {}) {
console.log('Creating meeting:', roomName, title, options);
const meetingUrl = JITSI_BASE_URL + '/' + roomName;
// Build config params
let configParams = [];
if (options.startWithAudioMuted) configParams.push('config.startWithAudioMuted=true');
if (options.startWithVideoMuted) configParams.push('config.startWithVideoMuted=true');
if (options.password) configParams.push('config.prejoinPageEnabled=true');
const fullUrl = meetingUrl + (configParams.length ? '#' + configParams.join('&') : '');
// Open in embed
openJitsiEmbed(fullUrl, title);
}
// ==========================================
// JITSI EMBED
// ==========================================
function openJitsiEmbed(url, title) {
const container = document.getElementById('jitsi-embed');
const iframe = document.getElementById('jitsi-iframe');
const titleEl = document.getElementById('jitsi-embed-title');
titleEl.textContent = title || 'Meeting';
iframe.src = url;
container.classList.add('active');
console.log('Opened Jitsi embed:', url);
}
function closeJitsiEmbed() {
const container = document.getElementById('jitsi-embed');
const iframe = document.getElementById('jitsi-iframe');
iframe.src = '';
container.classList.remove('active');
}
// ==========================================
// ACTIVE MEETINGS
// ==========================================
function loadActiveMeetings() {
fetch('/api/meetings/active', {
headers: {
'Authorization': 'Bearer ' + (localStorage.getItem('bp-token') || '')
}
})
.then(response => response.json())
.then(data => {
const list = document.getElementById('meetings-list');
const countBadge = document.getElementById('active-meetings-count');
if (data.meetings && data.meetings.length > 0) {
countBadge.textContent = data.meetings.length;
list.innerHTML = data.meetings.map(meeting => `
<div class="meeting-item">
<div class="meeting-info">
<div class="meeting-status"></div>
<div class="meeting-details">
<h4>${escapeHtml(meeting.title || meeting.room_name)}</h4>
<div class="meeting-meta">${meeting.participants || 0} Teilnehmer | Gestartet ${formatTime(meeting.started_at)}</div>
</div>
</div>
<div class="meeting-actions">
<button class="btn btn-sm btn-primary" onclick="joinMeeting('${meeting.room_name}', '${escapeHtml(meeting.title || '')}')">Beitreten</button>
<button class="btn btn-sm btn-ghost" onclick="copyMeetingLink('${meeting.room_name}')">Link kopieren</button>
</div>
</div>
`).join('');
} else {
countBadge.textContent = '0';
list.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">&#128247;</div>
<p class="empty-state-text">Keine aktiven Meetings. Starten Sie ein neues Meeting oben.</p>
</div>
`;
}
})
.catch(err => {
console.log('Could not load active meetings:', err);
});
}
function joinMeeting(roomName, title) {
const url = JITSI_BASE_URL + '/' + roomName;
openJitsiEmbed(url, title || roomName);
}
function copyMeetingLink(roomName) {
const url = JITSI_BASE_URL + '/' + roomName;
navigator.clipboard.writeText(url).then(() => {
alert('Link kopiert: ' + url);
});
}
// ==========================================
// DIALOGS
// ==========================================
function showMeetingCreatedDialog(info) {
const message = `
Elterngespraech erstellt!
Schueler: ${info.studentName}
Datum: ${info.date} um ${info.time}
Passwort: ${info.password}
Link fuer Eltern:
${info.meetingUrl}
Der Link wurde in die Zwischenablage kopiert.
`;
navigator.clipboard.writeText(info.meetingUrl + '\\nPasswort: ' + info.password);
alert(message);
// Ask to open now
if (confirm('Moechten Sie das Meeting jetzt oeffnen?')) {
openJitsiEmbed(info.meetingUrl, 'Elterngespraech: ' + info.studentName);
}
}
// ==========================================
// HELPERS
// ==========================================
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatTime(isoString) {
if (!isoString) return '';
const date = new Date(isoString);
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
// Auto-refresh active meetings every 30 seconds
setInterval(loadActiveMeetings, 30000);
"""
def get_jitsi_module() -> dict:
"""Gibt das komplette Jitsi-Modul als Dictionary zurueck."""
module = JitsiModule()
return {
'css': module.get_css(),
'html': module.get_html(),
'js': module.get_js(),
'init_function': 'loadJitsiModule'
}