diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 22b76af..c0fdc82 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -577,12 +577,15 @@ .badge { display: inline-flex; align-items: center; + justify-content: center; + gap: 4px; padding: 4px 8px; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; + white-space: nowrap; } .badge-admin { @@ -910,76 +913,7 @@ @endif - -
-
-
-

- ⚙️ Global Admin Proctoring & System Settings -

-
Master control panel for candidate assessment rules, system screen capturing, and module toggles
-
- - 🛡️ ADMIN CONTROL PANEL - -
-
- - -
-
- @csrf -
-
- 📸 Candidate System Screenshot Capture -
-
- Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates. -
-
-
- - {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }} - - -
-
-
- - -
-
- @csrf -
-
- 🚀 Candidate Provisioning & Onboarding Module -
-
- Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation. -
-
-
- - {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }} - - -
-
-
- -
-
@@ -1241,41 +1175,7 @@ - - - - -
- @csrf -
-
-
🚀 Onboarding & Offboarding Module
-
Enable/disable candidate provisioning & 1-click revocation
-
- -
-
- - -
- @csrf -
-
-
📸 Candidate System Screenshot Capture
-
Master Admin control to stop/start candidate system screen capturing (call start & tab switch)
-
- -
+
diff --git a/resources/views/interview/candidate_room.blade.php b/resources/views/interview/candidate_room.blade.php index d173812..edb8b51 100644 --- a/resources/views/interview/candidate_room.blade.php +++ b/resources/views/interview/candidate_room.blade.php @@ -458,13 +458,20 @@ } }); - // Real-Time Code Sync to Interviewer (Before Submission) + // Real-Time Instant BroadcastChannel + Backend Code Sync + const liveSyncChannel = new BroadcastChannel('live_sync_' + '{{ $interview->id }}'); let codeSyncTimeout = null; editor.onDidChangeModelContent(function () { + const code = editor.getValue(); + const lang = document.getElementById('language-select').value; + + // Broadcast instant local event (0ms latency) + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'code_sync', code: code, language: lang }); + } + clearTimeout(codeSyncTimeout); codeSyncTimeout = setTimeout(function () { - const code = editor.getValue(); - const lang = document.getElementById('language-select').value; fetch(`{{ route("interview.candidate.sync-code", $interview->id) }}`, { method: 'POST', headers: { @@ -473,7 +480,7 @@ }, body: JSON.stringify({ code: code, language: lang }) }); - }, 600); + }, 200); }); }); @@ -1176,6 +1183,10 @@ function showCandidateJoinOption() { if (joinBtn) joinBtn.style.display = 'none'; + if (!localMediaStream) { + await startCandidateCameraStream(); + } + isCandidateCallConnected = true; const badge = document.getElementById('call-status-badge'); @@ -1380,6 +1391,12 @@ function initCandidatePeer() { peerInstance.on('call', call => { activeCallInstance = call; + latestCallStatus = 'active'; + + if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) { + triggerCandidateRing(); + } + const sendStream = localMediaStream || new MediaStream(); call.answer(sendStream); @@ -1388,15 +1405,17 @@ function initCandidatePeer() { const placeholder = document.getElementById('interviewer-video-placeholder'); if (remoteVideo) { remoteVideo.srcObject = remoteStream; - remoteVideo.play().catch(e => console.log(e)); + remoteVideo.play().catch(e => console.log('Candidate remote video play error:', e)); } if (placeholder) placeholder.style.display = 'none'; - const badge = document.getElementById('call-status-badge'); - if (badge) { - badge.innerText = '🟢 CALL CONNECTED (LIVE)'; - badge.style.background = 'rgba(16,185,129,0.3)'; - badge.style.color = '#34d399'; + if (isCandidateCallConnected) { + const badge = document.getElementById('call-status-badge'); + if (badge) { + badge.innerText = '🟢 CALL CONNECTED (LIVE)'; + badge.style.background = 'rgba(16,185,129,0.3)'; + badge.style.color = '#34d399'; + } } }); @@ -1830,9 +1849,13 @@ function toggleNotepadModal() { } function saveNotepadContent() { + const notes = document.getElementById('notepad-textarea').value; + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'notes_sync', notes: notes }); + } + clearTimeout(notesTimeout); notesTimeout = setTimeout(() => { - const notes = document.getElementById('notepad-textarea').value; fetch(`{{ route('interview.candidate.notes', $interview->id) }}`, { method: 'POST', headers: { @@ -1841,13 +1864,14 @@ function saveNotepadContent() { }, body: JSON.stringify({ notes: notes }) }); - }, 300); + }, 150); } // HTML5 Canvas Drawing Logic let isDrawing = false; let canvas, ctx; let drawingSaveTimeout = null; + let lastDrawingSaveTime = 0; function toggleDrawingModal() { const modal = document.getElementById('drawing-modal'); @@ -1889,29 +1913,38 @@ function draw(e) { ctx.strokeStyle = document.getElementById('draw-color').value; ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); - saveCanvasDrawing(); + saveCanvasDrawing(false); } function stopDraw() { if (isDrawing) { isDrawing = false; ctx.closePath(); - saveCanvasDrawing(); + saveCanvasDrawing(true); } } function clearCanvasDraw() { if (ctx && canvas) { ctx.clearRect(0, 0, canvas.width, canvas.height); - saveCanvasDrawing(); + saveCanvasDrawing(true); } } - function saveCanvasDrawing() { - clearTimeout(drawingSaveTimeout); - drawingSaveTimeout = setTimeout(() => { - if (!canvas) return; - const drawingData = canvas.toDataURL(); + function saveCanvasDrawing(force = false) { + if (!canvas) return; + const now = Date.now(); + const drawingData = canvas.toDataURL(); + + // Broadcast instant local event (0ms latency) + if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) { + liveSyncChannel.postMessage({ type: 'drawing_sync', drawing: drawingData }); + } + + // Throttled HTTP POST save to backend (fires every 250ms while drawing & on mouseup) + if (force || (now - lastDrawingSaveTime >= 250)) { + clearTimeout(drawingSaveTimeout); + lastDrawingSaveTime = now; fetch(`{{ route('interview.candidate.drawing', $interview->id) }}`, { method: 'POST', headers: { @@ -1920,7 +1953,12 @@ function saveCanvasDrawing() { }, body: JSON.stringify({ drawing: drawingData }) }); - }, 300); + } else { + clearTimeout(drawingSaveTimeout); + drawingSaveTimeout = setTimeout(() => { + saveCanvasDrawing(true); + }, 250); + } } setInterval(() => { @@ -1944,18 +1982,13 @@ function saveCanvasDrawing() { }, 1200); } if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) { - const elapsed = data.call_started_at ? (Date.now() - new Date(data.call_started_at).getTime()) : 999999; - if (elapsed <= 12000 && lastRungCallStartedAt !== data.call_started_at) { - lastRungCallStartedAt = data.call_started_at; - triggerCandidateRing(null, data.call_started_at); - } else if (elapsed > 12000) { - showCandidateJoinOption(); - } + triggerCandidateRing(null, data.call_started_at); } } else if (data.call_status === 'ended' || data.call_status === 'idle') { latestCallStatus = data.call_status; hasCapturedCallStartScreenshot = false; lastRungCallStartedAt = null; + candidateDeclinedOrLeftCall = false; candRingtone.stop(); if (candRingTimer) clearTimeout(candRingTimer); isCandidateCallConnected = false; @@ -1978,7 +2011,7 @@ function saveCanvasDrawing() { } }) .catch(() => {}); - }, 2200); + }, 1000); diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index 80d5350..c8830fe 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -128,10 +128,14 @@ .badge { display: inline-flex; align-items: center; - padding: 4px 8px; + justify-content: center; + gap: 5px; + padding: 4px 10px; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; + white-space: nowrap; + line-height: 1.2; } .admin-modal { @@ -215,32 +219,80 @@ {{ session('success') }} @endif - -
-
-
📸
+ +
+
-
Global Candidate System Screenshot Control
-
- Captures candidate desktop screen on call start & tab switches. - Admin can stop/start screen capturing globally anytime. -
+

+ ⚙️ Global Admin Proctoring & System Settings +

+
Master control panel for candidate assessment rules, system screen capturing, and module toggles
-
-
- - {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Screen Capturing Active)' : '🔴 STOPPED / DISABLED' }} + + 🛡️ ADMIN CONTROL PANEL - @if(Auth::user() && Auth::user()->isAdmin()) -
- @csrf - -
- @endif
-
+ +
+ + +
+
+ @csrf +
+
+ 📸 Candidate System Screenshot Capture +
+
+ Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates. +
+
+
+ + {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }} + + @if(Auth::user() && Auth::user()->isAdmin()) + + @endif +
+
+
+ + +
+
+ @csrf +
+
+ 🚀 Candidate Provisioning & Onboarding Module +
+
+ Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation. +
+
+
+ + {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }} + + @if(Auth::user() && Auth::user()->isAdmin()) + + @endif +
+
+
+ +
+
@@ -1180,6 +1232,48 @@ function declineIncomingCall(silent = false) { } } + let liveSyncChannel = null; + + function subscribeLiveSync(interviewId) { + if (liveSyncChannel) { + try { liveSyncChannel.close(); } catch(e) {} + } + liveSyncChannel = new BroadcastChannel('live_sync_' + interviewId); + liveSyncChannel.onmessage = function(event) { + const data = event.data; + if (!data) return; + + if (data.type === 'code_sync') { + if (data.language) { + const langEl = document.getElementById('modal-code-lang'); + if (langEl) langEl.innerText = data.language.toUpperCase(); + } + if (data.code !== undefined) { + const codeEl = document.getElementById('modal-code-display'); + if (codeEl) codeEl.innerText = data.code || '// Candidate has not typed any code yet.'; + } + } else if (data.type === 'notes_sync') { + if (data.notes !== undefined) { + const notesEl = document.getElementById('modal-notes-display'); + if (notesEl) notesEl.innerText = data.notes || 'No candidate notes written yet.'; + } + } else if (data.type === 'drawing_sync') { + const drawingImg = document.getElementById('modal-drawing-img'); + const noDrawing = document.getElementById('no-drawing-placeholder'); + if (data.drawing) { + if (drawingImg) { + drawingImg.src = data.drawing; + drawingImg.style.display = 'block'; + } + if (noDrawing) noDrawing.style.display = 'none'; + } else { + if (drawingImg) drawingImg.style.display = 'none'; + if (noDrawing) noDrawing.style.display = 'block'; + } + } + }; + } + function openReviewModal(id, name, uid, autoJoinCall = false) { currentInterviewId = id; acknowledgedViolationCount = 0; @@ -1199,9 +1293,10 @@ function openReviewModal(id, name, uid, autoJoinCall = false) { initInterviewerPeer(); switchIdeTab('code'); + subscribeLiveSync(id); pollReviewData(); clearInterval(sosPollInterval); - sosPollInterval = setInterval(pollReviewData, 1200); + sosPollInterval = setInterval(pollReviewData, 500); if (autoJoinCall) { setTimeout(() => { @@ -2135,6 +2230,10 @@ function endInterviewerCall() { function closeReviewModal() { clearInterval(sosPollInterval); + if (liveSyncChannel) { + try { liveSyncChannel.close(); } catch(e) {} + liveSyncChannel = null; + } leaveInterviewerCall(true); const modal = document.getElementById('review-modal'); if (modal) modal.classList.remove('active');