""" 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 """
""" @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 => `Keine aktiven Meetings. Starten Sie ein neues Meeting oben.