diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index a59d0e02..1048f3b8 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -372,6 +372,7 @@ public class ClassroomHub : Hub { try { + // 1. Attendance kapat var attendances = await _attendanceRepository.GetListAsync( x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null ); @@ -381,42 +382,60 @@ public class ClassroomHub : Hub foreach (var attendance in attendances) { await CloseAttendanceAsync(attendance); - await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance); + await Clients.Group(sessionId.ToString()) + .SendAsync("AttendanceUpdated", attendance); } } + // 2. Participant bul var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == participantId ); - if (participant != null && !string.IsNullOrEmpty(participant.ConnectionId)) + if (participant == null) { - await Clients.Client(participant.ConnectionId) - .SendAsync("ForceDisconnect", "You have been removed from the class."); - - await Groups.RemoveFromGroupAsync(participant.ConnectionId, sessionId.ToString()); - - // ❌ pasif + ✅ kicked işaretle - participant.IsActive = false; - participant.IsKicked = true; - participant.ConnectionId = null; - await _participantRepository.UpdateAsync(participant, autoSave: true); - - await Clients.Group(sessionId.ToString()) - .SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId }); + _logger.LogWarning("KickParticipant: Session {SessionId} için participant {ParticipantId} bulunamadı", + sessionId, participantId); + return; } + // ConnectionId'yi kaydet (DB'ye null yazmadan önce) + var connectionId = participant.ConnectionId; - // 3. Diğerlerine duyur - _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId); + // 3. DB'de kick flag setle + participant.IsActive = false; + participant.IsKicked = true; + participant.ConnectionId = null; + + await _participantRepository.UpdateAsync(participant, autoSave: true); + + // 4. Hedef kullanıcıya bildir + if (!string.IsNullOrEmpty(connectionId)) + { + await Clients.Client(connectionId) + .SendAsync("ForceDisconnect", "You have been removed from the class."); + + await Groups.RemoveFromGroupAsync(connectionId, sessionId.ToString()); + } + + // 5. Diğer katılımcılara bildir + await Clients.Group(sessionId.ToString()) + .SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId }); + + // 6. Log + _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", + participantId, sessionId); } catch (Exception ex) { - _logger.LogError(ex, "❌ KickParticipant hata verdi"); + _logger.LogError(ex, "❌ KickParticipant hata verdi (Session={SessionId}, Participant={ParticipantId})", + sessionId, participantId); + await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu."); } } + [HubMethodName("ApproveHandRaise")] public async Task ApproveHandRaiseAsync(Guid sessionId, Guid studentId) { diff --git a/configs/deployment/scripts/10-migrator-true-dev.sh b/configs/deployment/scripts/10-dev-migrator-true.sh similarity index 100% rename from configs/deployment/scripts/10-migrator-true-dev.sh rename to configs/deployment/scripts/10-dev-migrator-true.sh diff --git a/configs/deployment/scripts/4-build-production.sh b/configs/deployment/scripts/4-production-build.sh similarity index 100% rename from configs/deployment/scripts/4-build-production.sh rename to configs/deployment/scripts/4-production-build.sh diff --git a/configs/deployment/scripts/5-deploy-production.sh b/configs/deployment/scripts/5-production-deploy.sh similarity index 100% rename from configs/deployment/scripts/5-deploy-production.sh rename to configs/deployment/scripts/5-production-deploy.sh diff --git a/configs/deployment/scripts/6-migrator-true-production.sh b/configs/deployment/scripts/6-production-migrator-true.sh similarity index 100% rename from configs/deployment/scripts/6-migrator-true-production.sh rename to configs/deployment/scripts/6-production-migrator-true.sh diff --git a/configs/deployment/scripts/8-build-dev.sh b/configs/deployment/scripts/8-dev-build.sh similarity index 100% rename from configs/deployment/scripts/8-build-dev.sh rename to configs/deployment/scripts/8-dev-build.sh diff --git a/configs/deployment/scripts/9-deploy-dev.sh b/configs/deployment/scripts/9-dev-deploy.sh similarity index 100% rename from configs/deployment/scripts/9-deploy-dev.sh rename to configs/deployment/scripts/9-dev-deploy.sh diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index d93f530f..df9d1c84 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -281,8 +281,7 @@ const RoomDetail: React.FC = () => { signalRServiceRef.current.setParticipantJoinHandler( async (userId: string, name: string, isTeacher: boolean, isActive: boolean) => { if (userId === user.id) return - if (!isActive) return // ❌ pasif kullanıcıyı ekleme - if (isTeacher) setTeacherDisconnected(false) + if (!isActive) return console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`) @@ -296,31 +295,19 @@ const RoomDetail: React.FC = () => { isTeacher, isAudioMuted: classSettings.defaultMicrophoneState === 'muted', isVideoMuted: classSettings.defaultCameraState === 'off', - isActive: isActive, // ✅ state’de tut + isActive: true, }) } - - // ✅ güncel listedeki öğretmen kontrolü - const teacherExists = updated.some((p) => p.isTeacher && p.isActive) - - if (teacherExists) { - ;(async () => { - if (!webRTCServiceRef.current?.getPeerConnection(userId)) { - await webRTCServiceRef.current?.createPeerConnection(userId) - } - - // ✅ öğretmen ise her zaman offer başlatır - if (isActive) { - if (user.role === 'teacher') { - const offer = await webRTCServiceRef.current!.createOffer(userId) - await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) - } - } - })() - } - return updated }) + + // 🔑 Mesh: Herkes herkese offer gönderir + if (!webRTCServiceRef.current?.getPeerConnection(userId)) { + await webRTCServiceRef.current?.createPeerConnection(userId) + } + + const offer = await webRTCServiceRef.current!.createOffer(userId) + await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) }, ) @@ -329,12 +316,12 @@ const RoomDetail: React.FC = () => { async ( existing: { userId: string; userName: string; isTeacher: boolean; isActive: boolean }[], ) => { - setParticipants((prev) => { - let updated = [...prev] + for (const participant of existing) { + if (!participant.isActive) continue + if (participant.userId === user.id) continue - for (const participant of existing) { - if (!participant.isActive) continue // ❌ pasif kullanıcıyı alma - if (participant.userId === user.id) continue + setParticipants((prev) => { + const updated = [...prev] if (!updated.find((p) => p.id === participant.userId)) { updated.push({ id: participant.userId, @@ -343,39 +330,20 @@ const RoomDetail: React.FC = () => { isTeacher: participant.isTeacher, isAudioMuted: classSettings.defaultMicrophoneState === 'muted', isVideoMuted: classSettings.defaultCameraState === 'off', - isActive: participant.isActive, // ✅ state’de tut + isActive: true, }) } + return updated + }) + + if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) { + await webRTCServiceRef.current?.createPeerConnection(participant.userId) } - // ✅ güncel listede öğretmen var mı? - const teacherExists = updated.some((p) => p.isTeacher && p.isActive) - - if (teacherExists) { - ;(async () => { - for (const participant of existing) { - if (!participant.isActive) continue - if (participant.userId === user.id) continue - - if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) { - await webRTCServiceRef.current?.createPeerConnection(participant.userId) - } - - // ✅ sadece öğretmen offer başlatır - if (user.role === 'teacher') { - const offer = await webRTCServiceRef.current!.createOffer(participant.userId) - await signalRServiceRef.current?.sendOffer( - classSession.id, - participant.userId, - offer, - ) - } - } - })() - } - - return updated - }) + // 🔑 Mesh: yeni gelen user da karşıya offer yollar + const offer = await webRTCServiceRef.current!.createOffer(participant.userId) + await signalRServiceRef.current?.sendOffer(classSession.id, participant.userId, offer) + } }, )