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' }} - - - - - {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }} - - - - - - - - - - @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' }} - - - - - {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }} - - - - - - - - @@ -1241,41 +1175,7 @@ Save Default - - - - - - @csrf - - - 🚀 Onboarding & Offboarding Module - Enable/disable candidate provisioning & 1-click revocation - - - - - {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'ENABLED' : 'DISABLED' }} - - - - - - - - @csrf - - - 📸 Candidate System Screenshot Capture - Master Admin control to stop/start candidate system screen capturing (call start & tab switch) - - - - - {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'ENABLED' : 'STOPPED / DISABLED' }} - - - + 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);