'use client' /** * Communication Admin Page * * Matrix & Jitsi Monitoring Dashboard * Provides system statistics, active calls, user metrics, and service health */ import AdminLayout from '@/components/admin/AdminLayout' import { useEffect, useState, useCallback } from 'react' interface MatrixStats { total_users: number active_users: number total_rooms: number active_rooms: number messages_today: number messages_this_week: number status: 'online' | 'offline' | 'degraded' } interface JitsiStats { active_meetings: number total_participants: number meetings_today: number average_duration_minutes: number peak_concurrent_users: number total_minutes_today: number status: 'online' | 'offline' | 'degraded' } interface TrafficStats { matrix: { bandwidth_in_mb: number bandwidth_out_mb: number messages_per_minute: number media_uploads_today: number media_size_mb: number } jitsi: { bandwidth_in_mb: number bandwidth_out_mb: number video_streams_active: number audio_streams_active: number estimated_hourly_gb: number } total: { bandwidth_in_mb: number bandwidth_out_mb: number estimated_monthly_gb: number } } interface CommunicationStats { matrix: MatrixStats jitsi: JitsiStats traffic?: TrafficStats last_updated: string } interface ActiveMeeting { room_name: string display_name: string participants: number started_at: string duration_minutes: number } interface RecentRoom { room_id: string name: string member_count: number last_activity: string room_type: 'class' | 'parent' | 'staff' | 'general' } export default function CommunicationPage() { const [stats, setStats] = useState(null) const [activeMeetings, setActiveMeetings] = useState([]) const [recentRooms, setRecentRooms] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const API_BASE = '/api/admin/communication' const fetchStats = useCallback(async () => { try { const response = await fetch(`${API_BASE}/stats`) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const data = await response.json() setStats(data) setActiveMeetings(data.active_meetings || []) setRecentRooms(data.recent_rooms || []) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'Verbindungsfehler') // Set mock data for display purposes when API unavailable setStats({ matrix: { total_users: 0, active_users: 0, total_rooms: 0, active_rooms: 0, messages_today: 0, messages_this_week: 0, status: 'offline' }, jitsi: { active_meetings: 0, total_participants: 0, meetings_today: 0, average_duration_minutes: 0, peak_concurrent_users: 0, total_minutes_today: 0, status: 'offline' }, last_updated: new Date().toISOString() }) } finally { setLoading(false) } }, []) useEffect(() => { fetchStats() }, [fetchStats]) // Auto-refresh every 15 seconds useEffect(() => { const interval = setInterval(fetchStats, 15000) return () => clearInterval(interval) }, [fetchStats]) const getStatusBadge = (status: string) => { const baseClasses = 'px-3 py-1 rounded-full text-xs font-semibold uppercase' switch (status) { case 'online': return `${baseClasses} bg-green-100 text-green-800` case 'degraded': return `${baseClasses} bg-yellow-100 text-yellow-800` case 'offline': return `${baseClasses} bg-red-100 text-red-800` default: return `${baseClasses} bg-slate-100 text-slate-600` } } const getRoomTypeBadge = (type: string) => { const baseClasses = 'px-2 py-0.5 rounded text-xs font-medium' switch (type) { case 'class': return `${baseClasses} bg-blue-100 text-blue-700` case 'parent': return `${baseClasses} bg-purple-100 text-purple-700` case 'staff': return `${baseClasses} bg-orange-100 text-orange-700` default: return `${baseClasses} bg-slate-100 text-slate-600` } } const formatDuration = (minutes: number) => { if (minutes < 60) return `${Math.round(minutes)} Min.` const hours = Math.floor(minutes / 60) const mins = Math.round(minutes % 60) return `${hours}h ${mins}m` } const formatTimeAgo = (dateStr: string) => { const date = new Date(dateStr) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) if (diffMins < 1) return 'gerade eben' if (diffMins < 60) return `vor ${diffMins} Min.` if (diffMins < 1440) return `vor ${Math.floor(diffMins / 60)} Std.` return `vor ${Math.floor(diffMins / 1440)} Tagen` } // Traffic estimation helpers for SysEleven planning const calculateEstimatedTraffic = (direction: 'in' | 'out'): number => { const messages = stats?.matrix?.messages_today || 0 const callMinutes = stats?.jitsi?.total_minutes_today || 0 const participants = stats?.jitsi?.total_participants || 0 // Estimates: ~2KB per message, ~1.5 Mbps per video participant const messageTrafficMB = messages * 0.002 const videoTrafficMB = callMinutes * participants * 0.011 // ~660 KB/min per participant if (direction === 'in') { return messageTrafficMB * 0.3 + videoTrafficMB * 0.4 // Incoming is less (mostly receiving) } return messageTrafficMB * 0.7 + videoTrafficMB * 0.6 // Outgoing is more } const calculateHourlyEstimate = (): number => { const activeParticipants = stats?.jitsi?.total_participants || 0 // ~1.5 Mbps per participant = ~0.675 GB/hour per participant return activeParticipants * 0.675 } const calculateMonthlyEstimate = (): number => { const dailyCallMinutes = stats?.jitsi?.total_minutes_today || 0 const avgParticipants = stats?.jitsi?.peak_concurrent_users || 1 // Extrapolate: assume 22 working days, similar usage pattern const monthlyMinutes = dailyCallMinutes * 22 // ~11 MB/min for video conference with participants return (monthlyMinutes * avgParticipants * 11) / 1024 } const getResourceRecommendation = (): string => { const peakUsers = stats?.jitsi?.peak_concurrent_users || 0 const monthlyGB = calculateMonthlyEstimate() if (monthlyGB < 10 || peakUsers < 5) { return 'Starter (1 vCPU, 2GB RAM, 100GB Traffic)' } else if (monthlyGB < 50 || peakUsers < 20) { return 'Standard (2 vCPU, 4GB RAM, 500GB Traffic)' } else if (monthlyGB < 200 || peakUsers < 50) { return 'Professional (4 vCPU, 8GB RAM, 2TB Traffic)' } else { return 'Enterprise (8+ vCPU, 16GB+ RAM, Unlimited Traffic)' } } return ( {/* Service Status Overview */}
{/* Matrix Status Card */}

Matrix (Synapse)

E2EE Messaging

{stats?.matrix.status || 'offline'}
{stats?.matrix.total_users || 0}
Benutzer
{stats?.matrix.active_users || 0}
Aktiv
{stats?.matrix.total_rooms || 0}
Räume
Nachrichten heute {stats?.matrix.messages_today || 0}
Diese Woche {stats?.matrix.messages_this_week || 0}
{/* Jitsi Status Card */}

Jitsi Meet

Videokonferenzen

{stats?.jitsi.status || 'offline'}
{stats?.jitsi.active_meetings || 0}
Live Calls
{stats?.jitsi.total_participants || 0}
Teilnehmer
{stats?.jitsi.meetings_today || 0}
Calls heute
Ø Dauer {formatDuration(stats?.jitsi.average_duration_minutes || 0)}
Peak gleichzeitig {stats?.jitsi.peak_concurrent_users || 0} Nutzer
{/* Traffic & Bandwidth Statistics for SysEleven Planning */}

Traffic & Bandbreite

SysEleven Ressourcenplanung

Live
Eingehend (heute)
{stats?.traffic?.total?.bandwidth_in_mb?.toFixed(1) || calculateEstimatedTraffic('in').toFixed(1)} MB
Ausgehend (heute)
{stats?.traffic?.total?.bandwidth_out_mb?.toFixed(1) || calculateEstimatedTraffic('out').toFixed(1)} MB
Geschätzt/Stunde
{stats?.traffic?.jitsi?.estimated_hourly_gb?.toFixed(2) || calculateHourlyEstimate().toFixed(2)} GB
Geschätzt/Monat
{stats?.traffic?.total?.estimated_monthly_gb?.toFixed(1) || calculateMonthlyEstimate().toFixed(1)} GB
{/* Matrix Traffic */}
Matrix Messaging
Nachrichten/Min {stats?.traffic?.matrix?.messages_per_minute || Math.round((stats?.matrix?.messages_today || 0) / (new Date().getHours() || 1) / 60)}
Media Uploads heute {stats?.traffic?.matrix?.media_uploads_today || 0}
Media Größe {stats?.traffic?.matrix?.media_size_mb?.toFixed(1) || '0.0'} MB
{/* Jitsi Traffic */}
Jitsi Video
Video Streams aktiv {stats?.traffic?.jitsi?.video_streams_active || (stats?.jitsi?.total_participants || 0)}
Audio Streams aktiv {stats?.traffic?.jitsi?.audio_streams_active || (stats?.jitsi?.total_participants || 0)}
Bitrate geschätzt {((stats?.jitsi?.total_participants || 0) * 1.5).toFixed(1)} Mbps
{/* SysEleven Resource Recommendations */}

SysEleven Empfehlung

Basierend auf aktuellem Traffic: {getResourceRecommendation()}

Peak Teilnehmer: {stats?.jitsi?.peak_concurrent_users || 0} | Ø Call-Dauer: {stats?.jitsi?.average_duration_minutes?.toFixed(0) || 0} Min. | Calls heute: {stats?.jitsi?.meetings_today || 0}

{/* Active Meetings */}

Aktive Meetings

{activeMeetings.length === 0 ? (

Keine aktiven Meetings

) : (
{activeMeetings.map((meeting, idx) => ( ))}
Meeting Teilnehmer Gestartet Dauer
{meeting.display_name}
{meeting.room_name}
{meeting.participants} {formatTimeAgo(meeting.started_at)} {formatDuration(meeting.duration_minutes)}
)}
{/* Recent Chat Rooms */}

Aktive Chat-Räume

{recentRooms.length === 0 ? (

Keine aktiven Räume

) : (
{recentRooms.slice(0, 5).map((room, idx) => (
{room.name}
{room.member_count} Mitglieder
{room.room_type} {formatTimeAgo(room.last_activity)}
))}
)}
{/* Usage Statistics */}

Nutzungsstatistiken

Call-Minuten heute {stats?.jitsi.total_minutes_today || 0} Min.
Aktive Chat-Räume {stats?.matrix.active_rooms || 0} / {stats?.matrix.total_rooms || 0}
Aktive Nutzer {stats?.matrix.active_users || 0} / {stats?.matrix.total_users || 0}
{/* Quick Actions */}
{/* Connection Info */}

Service Konfiguration

Matrix Homeserver: http://localhost:8448 (Synapse)
Jitsi Meet: http://localhost:8443
Auto-Refresh: Alle 15 Sekunden

{error && (

Fehler: {error} - API-Proxy nicht verfügbar

)} {stats?.last_updated && (

Letzte Aktualisierung: {new Date(stats.last_updated).toLocaleString('de-DE')}

)}
) }