From 3d7d3042edcb876cc6a9de1bd5572136d218390e Mon Sep 17 00:00:00 2001 From: subhajit Date: Thu, 30 Jul 2026 17:31:03 +0530 Subject: [PATCH] implement candidate feature --- app/Http/Controllers/AdminController.php | 12 + app/Http/Controllers/InterviewController.php | 192 +++++++------- .../screenshot_tab_switch_1785412177_446.jpg | Bin 0 -> 27692 bytes resources/views/admin/dashboard.blade.php | 90 ++++++- .../views/interview/candidate_room.blade.php | 163 ++++++++---- resources/views/interview/index.blade.php | 246 +++++++++++------- routes/web.php | 1 + 7 files changed, 469 insertions(+), 235 deletions(-) create mode 100644 public/uploads/candidate_screenshots/anwesha_9163855564_1785411698/screenshot_tab_switch_1785412177_446.jpg diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index dd5d439..2937cc8 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -306,6 +306,18 @@ public function updateOnboardingToggle(Request $request) return redirect()->route('admin.dashboard')->with('success', $msg); } + /** + * Toggle candidate system screenshot capture globally. + */ + public function updateScreenshotToggle(Request $request) + { + $enabled = $request->boolean('enable_candidate_screenshots') ? '1' : '0'; + \App\Models\Setting::set('enable_candidate_screenshots', $enabled); + + $msg = $enabled === '1' ? 'Candidate System Screenshot Capture ENABLED globally.' : 'Candidate System Screenshot Capture DISABLED globally by Admin.'; + return redirect()->route('admin.dashboard')->with('success', $msg); + } + /** * Toggle the admin privilege of a user. */ diff --git a/app/Http/Controllers/InterviewController.php b/app/Http/Controllers/InterviewController.php index db5415d..a5cd4ef 100644 --- a/app/Http/Controllers/InterviewController.php +++ b/app/Http/Controllers/InterviewController.php @@ -148,96 +148,96 @@ public function executeCode(Request $request) $request->validate([ 'language' => 'required|string', 'code' => 'required|string', + 'interview_id' => 'nullable|string', ]); - $langMap = [ - 'python' => ['language' => 'python', 'version' => '*'], - 'c' => ['language' => 'c', 'version' => '*'], - 'cpp' => ['language' => 'c++', 'version' => '*'], - 'java' => ['language' => 'java', 'version' => '*'], - 'php' => ['language' => 'php', 'version' => '*'], - 'laravel' => ['language' => 'php', 'version' => '*'], - 'javascript' => ['language' => 'javascript', 'version' => '*'], - ]; - - $langKey = strtolower($request->language); - $targetLang = $langMap[$langKey] ?? ['language' => 'python', 'version' => '*']; + $langKey = strtolower(trim($request->language)); + $code = $request->code; + $output = ''; + // 1. Instant Local CLI Process Execution (0.02s response for Node.js, Python, PHP) try { - $response = Http::withHeaders([ - 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept' => 'application/json', - ])->timeout(10)->post('https://emkc.org/api/v2/piston/execute', [ - 'language' => $targetLang['language'], - 'version' => $targetLang['version'], - 'files' => [ - [ - 'name' => 'solution', - 'content' => $request->code, - ] - ], - ]); - - if ($response->successful()) { - $data = $response->json(); - $output = $data['run']['output'] ?? ($data['compile']['output'] ?? 'Code executed successfully. No stdout output.'); - return response()->json(['success' => true, 'output' => $output]); - } - - // Retry with explicit fallback version if wildcard returned non-200 - $fallbackVersions = [ - 'python' => '3.10.0', - 'c' => '10.2.0', - 'c++' => '10.2.0', - 'java' => '15.0.2', - 'php' => '8.2.3', - 'javascript' => '18.15.0', - ]; - - $retryResponse = Http::withHeaders([ - 'User-Agent' => 'SingleLogin-ProctoredIDE/1.0', - 'Accept' => 'application/json', - ])->timeout(10)->post('https://emkc.org/api/v2/piston/execute', [ - 'language' => $targetLang['language'], - 'version' => $fallbackVersions[$targetLang['language']] ?? '3.10.0', - 'files' => [ - [ - 'name' => 'solution', - 'content' => $request->code, - ] - ], - ]); - - if ($retryResponse->successful()) { - $data = $retryResponse->json(); - $output = $data['run']['output'] ?? ($data['compile']['output'] ?? 'Code executed successfully.'); - return response()->json(['success' => true, 'output' => $output]); - } - - // Fallback for local PHP execution if PHP/Laravel language requested - if (in_array($langKey, ['php', 'laravel'])) { + if (in_array($langKey, ['javascript', 'js'])) { + $process = new \Symfony\Component\Process\Process(['node', '-e', $code]); + $process->setTimeout(4); + $process->run(); + $out = $process->getOutput() ?: $process->getErrorOutput(); + $output = trim($out) ?: 'JavaScript executed successfully (no stdout).'; + } elseif ($langKey === 'python') { + $process = new \Symfony\Component\Process\Process(['python', '-c', $code]); + $process->setTimeout(4); + $process->run(); + $out = $process->getOutput() ?: $process->getErrorOutput(); + $output = trim($out) ?: 'Python executed successfully (no stdout).'; + } elseif (in_array($langKey, ['php', 'laravel'])) { ob_start(); try { - $cleanCode = preg_replace('/^\s*<\?php\s*/i', '', $request->code); + $cleanCode = preg_replace('/^\s*<\?php\s*/i', '', $code); eval($cleanCode); - $localOutput = ob_get_clean(); - return response()->json(['success' => true, 'output' => $localOutput ?: '[Local PHP Execution Succeeded]']); + $out = ob_get_clean(); + $output = trim($out) ?: 'PHP code executed successfully.'; } catch (\Throwable $e) { ob_end_clean(); - return response()->json(['success' => true, 'output' => 'PHP Evaluation Error: ' . $e->getMessage()]); + $output = 'PHP Evaluation Error: ' . $e->getMessage(); } } - - return response()->json([ - 'success' => true, - 'output' => "=== Code Output ===\n" . "Code submitted successfully for evaluation.\n(Remote execution engine status: " . $response->status() . ")\nSolution saved to proctored record." - ]); } catch (\Exception $e) { - return response()->json([ - 'success' => true, - 'output' => "=== Code Output ===\nCode session active.\nSolution saved to proctored record." - ]); + // Local execution exception; fall through to Judge0 API } + + // 2. Remote Compiler API (Judge0 CE) for C, C++, Java or non-local runtimes + if (empty($output)) { + $judge0Map = [ + 'python' => 71, + 'c' => 50, + 'cpp' => 54, + 'c++' => 54, + 'java' => 62, + 'php' => 68, + 'laravel' => 68, + 'javascript' => 63, + 'js' => 63, + ]; + + if (isset($judge0Map[$langKey])) { + try { + $response = Http::timeout(5)->post('https://ce.judge0.com/submissions?wait=true', [ + 'source_code' => $code, + 'language_id' => $judge0Map[$langKey], + ]); + + if ($response->successful()) { + $data = $response->json(); + $stdout = $data['stdout'] ?? ''; + $stderr = $data['stderr'] ?? ''; + $compile = $data['compile_output'] ?? ''; + $msg = $data['message'] ?? ''; + + $outputParts = array_filter([$stdout, $stderr, $compile, $msg]); + $output = trim(implode("\n", $outputParts)); + } + } catch (\Exception $e) {} + } + } + + if (empty($output)) { + $output = "=== Code Execution Status ===\nSolution executed.\nLanguage: " . strtoupper($langKey); + } + + // Auto-persist candidate code + execution output directly to interview model + $interviewId = $request->input('interview_id'); + if ($interviewId) { + $interview = Interview::where('id', $interviewId)->orWhere('submission_unique_id', $interviewId)->first(); + if ($interview) { + $interview->update([ + 'submitted_code' => $code, + 'submitted_language' => $langKey, + 'code_output' => $output, + ]); + } + } + + return response()->json(['success' => true, 'output' => $output]); } /** @@ -340,10 +340,14 @@ public function candidateSendMessage(Request $request, $id) public function syncCandidateCode(Request $request, $id) { $interview = Interview::findOrFail($id); - $interview->update([ + $updateData = [ 'submitted_code' => $request->input('code'), 'submitted_language' => $request->input('language', $interview->language), - ]); + ]; + if ($request->has('output')) { + $updateData['code_output'] = $request->input('output'); + } + $interview->update($updateData); return response()->json(['success' => true]); } @@ -570,13 +574,18 @@ public function getPollData($id) $starterName = $interview->callStarter ? $interview->callStarter->name : null; + $globalScreenshotsEnabled = \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1'; + $interviewScreenshotsEnabled = (bool) ($interview->enable_tab_switch_screenshot ?? true); + $effectiveScreenshotEnabled = $globalScreenshotsEnabled && $interviewScreenshotsEnabled; + return response()->json([ 'status' => $interview->status, 'call_status' => $interview->call_status ?? 'idle', 'call_started_by' => $interview->call_started_by, 'starter_name' => $starterName, 'call_started_at' => $interview->call_started_at ? $interview->call_started_at->toIso8601String() : null, - 'enable_tab_switch_screenshot' => (bool) ($interview->enable_tab_switch_screenshot ?? true), + 'enable_tab_switch_screenshot' => $effectiveScreenshotEnabled, + 'global_screenshot_enabled' => $globalScreenshotsEnabled, 'tab_switch_screenshots' => $interview->tab_switch_screenshots ?? [], 'submitted_code' => $interview->submitted_code, 'submitted_language' => $interview->submitted_language, @@ -633,7 +642,7 @@ public function updateCallStatus(Request $request, $id) } /** - * Upload & Save Candidate Tab-Switch Screenshot under public/uploads/candidate_screenshots/{unique_id}/. + * Upload & Save Candidate System Screen Screenshot under public/uploads/candidate_screenshots/{unique_id}/. */ public function uploadTabScreenshot(Request $request, $id) { @@ -641,15 +650,18 @@ public function uploadTabScreenshot(Request $request, $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); - if (isset($interview->enable_tab_switch_screenshot) && !$interview->enable_tab_switch_screenshot) { + $globalEnabled = \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1'; + $interviewEnabled = !isset($interview->enable_tab_switch_screenshot) || $interview->enable_tab_switch_screenshot; + + if (!$globalEnabled || !$interviewEnabled) { return response()->json([ 'success' => false, - 'message' => 'Tab-switch screenshot capture is disabled by admin setting.' + 'message' => 'Candidate system screenshot capture is disabled by admin setting.' ]); } - $url = null; - $filename = 'screenshot_tab_switch_' . time() . '_' . rand(100, 999) . '.jpg'; + $reason = $request->input('reason', 'tab_switch'); // 'call_start' or 'tab_switch' + $filename = 'screenshot_' . $reason . '_' . time() . '_' . rand(100, 999) . '.jpg'; $subDir = 'uploads/candidate_screenshots/' . $interview->submission_unique_id; $destinationPath = public_path($subDir); @@ -657,6 +669,7 @@ public function uploadTabScreenshot(Request $request, $id) mkdir($destinationPath, 0777, true); } + $url = null; if ($request->hasFile('screenshot')) { $file = $request->file('screenshot'); $file->move($destinationPath, $filename); @@ -685,7 +698,7 @@ public function uploadTabScreenshot(Request $request, $id) 'filename' => $filename, 'ai_verdict' => $aiVerdict, 'timestamp' => now()->toIso8601String(), - 'reason' => 'tab_switch', + 'reason' => $reason, ]; $screenshots = $interview->tab_switch_screenshots ?? []; @@ -693,10 +706,13 @@ public function uploadTabScreenshot(Request $request, $id) $aiDetails = $aiVerdict['summary'] . ' (' . round(($aiVerdict['confidence'] ?? 0.85) * 100) . '% Confidence - ' . ($aiVerdict['engine'] ?? 'AI Engine') . ')'; + $logType = ($reason === 'call_start') ? 'call_start_screenshot' : (($aiVerdict['is_cheating'] && str_contains(strtolower($aiVerdict['ai_tool_detected'] ?? ''), 'parakeet')) ? 'external_ai_detected' : 'tab_switch'); + $logMsg = ($reason === 'call_start') ? '📸 CALL STARTED SYSTEM SNAPSHOT: System screen snapshot captured automatically when call started.' : ('🤖 AI INSPECTOR VERDICT: ' . $aiDetails); + $logs = $interview->proctor_logs ?? []; $logs[] = [ - 'type' => ($aiVerdict['is_cheating'] && str_contains(strtolower($aiVerdict['ai_tool_detected'] ?? ''), 'parakeet')) ? 'external_ai_detected' : 'tab_switch', - 'details' => '🤖 AI INSPECTOR VERDICT: ' . $aiDetails, + 'type' => $logType, + 'details' => $logMsg, 'ai_verdict' => $aiVerdict, 'screenshot_url' => $relativePath, 'timestamp' => now()->toIso8601String(), diff --git a/public/uploads/candidate_screenshots/anwesha_9163855564_1785411698/screenshot_tab_switch_1785412177_446.jpg b/public/uploads/candidate_screenshots/anwesha_9163855564_1785411698/screenshot_tab_switch_1785412177_446.jpg new file mode 100644 index 0000000000000000000000000000000000000000..770621936191c1fd7600f5bb7e803e9b7dff329c GIT binary patch literal 27692 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 literal 0 HcmV?d00001 diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index c457ecf..22b76af 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -910,6 +910,77 @@ @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' }} + + +
+
+
+ +
+
+
@@ -1174,7 +1245,7 @@ -
+ @csrf
@@ -1190,6 +1261,23 @@
+ +
+ @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 7c8e067..d173812 100644 --- a/resources/views/interview/candidate_room.blade.php +++ b/resources/views/interview/candidate_room.blade.php @@ -487,7 +487,7 @@ } }); - // Execute Code live via Piston API + // Execute Code live via Piston/Judge0 Engine function runCode() { document.getElementById('run-status').innerText = 'Running...'; document.getElementById('terminal-output').innerText = 'Executing solution on code engine...'; @@ -501,16 +501,23 @@ function runCode() { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, - body: JSON.stringify({ language: lang, code: code }) + body: JSON.stringify({ language: lang, code: code, interview_id: '{{ $interview->id }}' }) }) .then(r => r.json()) .then(data => { document.getElementById('run-status').innerText = 'Ready'; - if (data.success) { - document.getElementById('terminal-output').innerText = data.output; - } else { - document.getElementById('terminal-output').innerText = 'Error: ' + data.output; - } + const outStr = data.success ? data.output : ('Error: ' + data.output); + document.getElementById('terminal-output').innerText = outStr; + + // Sync live code & execution output to backend immediately + fetch(`{{ route("interview.candidate.sync-code", $interview->id) }}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ code: code, language: lang, output: outStr }) + }); }) .catch(err => { document.getElementById('run-status').innerText = 'Error'; @@ -561,14 +568,19 @@ function getScreenShareVideoElement() { } // REAL CANDIDATE SYSTEM / COMPUTER SCREENSHOT CAPTURE ENGINE - function captureAndUploadTabScreenshot() { + function captureAndUploadTabScreenshot(reason = 'tab_switch') { if (!isTabSwitchScreenshotEnabled) return; try { const tempCanvas = document.createElement('canvas'); let drewScreenStream = false; - // PRIORITY 1: Capture Candidate's ACTUAL Computer System Desktop Screen (Captures switched tab, external desktop apps like Parakeet AI, etc.) + const isCallStart = (reason === 'call_start'); + const bannerLabel = isCallStart + ? '📸 CALL STARTED • CANDIDATE SYSTEM SCREEN SNAPSHOT • ' + new Date().toLocaleString() + : '🚨 CANDIDATE SYSTEM COMPUTER DESKTOP SNAPSHOT • ' + new Date().toLocaleString(); + + // PRIORITY 1: Capture Candidate's ACTUAL Computer System Desktop Screen (Screen share stream) if (typeof screenShareStream !== 'undefined' && screenShareStream && screenShareStream.getVideoTracks().length > 0) { const screenTrack = screenShareStream.getVideoTracks()[0]; if (screenTrack.readyState === 'live') { @@ -579,24 +591,31 @@ function captureAndUploadTabScreenshot() { vidElem.play().catch(e => {}); } - const settings = screenTrack.getSettings ? screenTrack.getSettings() : {}; - const w = settings.width || vidElem.videoWidth || 1280; - const h = settings.height || vidElem.videoHeight || 720; + let rawW = vidElem.videoWidth || 1280; + let rawH = vidElem.videoHeight || 720; - if (w > 0 && h > 0) { - tempCanvas.width = w; - tempCanvas.height = h; + if (rawW > 0 && rawH > 0) { + const maxW = 1280; + let targetW = rawW; + let targetH = rawH; + if (targetW > maxW) { + targetH = Math.round(targetH * (maxW / targetW)); + targetW = maxW; + } + + tempCanvas.width = targetW; + tempCanvas.height = targetH; const tempCtx = tempCanvas.getContext('2d'); - tempCtx.drawImage(vidElem, 0, 0, w, h); + tempCtx.drawImage(vidElem, 0, 0, targetW, targetH); - // Add red violation banner + // Add system screenshot banner tempCtx.fillStyle = 'rgba(15, 23, 42, 0.90)'; tempCtx.fillRect(0, tempCanvas.height - 42, tempCanvas.width, 42); - tempCtx.fillStyle = '#ef4444'; + tempCtx.fillStyle = isCallStart ? '#10b981' : '#ef4444'; tempCtx.font = 'bold 14px sans-serif'; - tempCtx.fillText('🚨 CANDIDATE SYSTEM COMPUTER DESKTOP SNAPSHOT (TAB / APP SWITCH DETECTED) • ' + new Date().toLocaleString(), 16, tempCanvas.height - 15); + tempCtx.fillText(bannerLabel, 16, tempCanvas.height - 15); - uploadTabScreenshotBlob(tempCanvas.toDataURL('image/jpeg', 0.85)); + uploadTabScreenshotBlob(tempCanvas.toDataURL('image/jpeg', 0.75), reason); drewScreenStream = true; } } catch(e) { @@ -607,32 +626,31 @@ function captureAndUploadTabScreenshot() { if (drewScreenStream) return; - // PRIORITY 2: Full Rendered DOM Workspace Capture via html2canvas (Captures exact active tab DOM, code editor, layout) + // PRIORITY 2: Full Rendered DOM Workspace Capture via html2canvas if (typeof html2canvas !== 'undefined') { - html2canvas(document.body, { logging: false, useCORS: true, allowTaint: true }).then(canvas => { + html2canvas(document.body, { logging: false, useCORS: true, allowTaint: true, scale: 0.85 }).then(canvas => { const ctx = canvas.getContext('2d'); ctx.fillStyle = 'rgba(15, 23, 42, 0.90)'; ctx.fillRect(0, canvas.height - 42, canvas.width, 42); - ctx.fillStyle = '#ef4444'; + ctx.fillStyle = isCallStart ? '#10b981' : '#ef4444'; ctx.font = 'bold 14px sans-serif'; - ctx.fillText('🚨 CANDIDATE WORKSPACE TAB SNAPSHOT • ' + new Date().toLocaleString(), 16, canvas.height - 15); - uploadTabScreenshotBlob(canvas.toDataURL('image/jpeg', 0.85)); + ctx.fillText(bannerLabel, 16, canvas.height - 15); + uploadTabScreenshotBlob(canvas.toDataURL('image/jpeg', 0.75), reason); }).catch(err => { - fallbackCompositeSnapshot(); + fallbackCompositeSnapshot(reason); }); } else { - fallbackCompositeSnapshot(); + fallbackCompositeSnapshot(reason); } } catch(e) { - console.log('Tab switch screenshot capture exception:', e); + console.log('Candidate system screenshot capture exception:', e); } } - function fallbackCompositeSnapshot() { + function fallbackCompositeSnapshot(reason = 'tab_switch') { const tempCanvas = document.createElement('canvas'); - const video = document.getElementById('webcam'); - const w = (video && video.videoWidth > 0) ? video.videoWidth : 1280; - const h = (video && video.videoHeight > 0) ? video.videoHeight : 720; + const w = 1280; + const h = 720; tempCanvas.width = w; tempCanvas.height = h; const tempCtx = tempCanvas.getContext('2d'); @@ -643,51 +661,47 @@ function fallbackCompositeSnapshot() { tempCtx.fillRect(0, 0, w, 50); tempCtx.fillStyle = '#38bdf8'; tempCtx.font = 'bold 16px sans-serif'; - tempCtx.fillText('💻 Candidate Assessment Workspace • ID: ' + (typeof interviewId !== 'undefined' ? interviewId : ''), 20, 32); + tempCtx.fillText('💻 Candidate System Workspace • ID: ' + (typeof interviewId !== 'undefined' ? interviewId : ''), 20, 32); const codeDisplay = document.querySelector('.CodeMirror') || document.getElementById('code-editor'); tempCtx.fillStyle = '#020617'; - tempCtx.fillRect(20, 70, w - 240, h - 130); + tempCtx.fillRect(20, 70, w - 40, h - 130); tempCtx.fillStyle = '#94a3b8'; tempCtx.font = '13px monospace'; - const codeText = codeDisplay ? (codeDisplay.innerText || codeDisplay.value || '') : ''; + const codeText = codeDisplay ? (codeDisplay.innerText || codeDisplay.value || '') : (typeof editor !== 'undefined' && editor ? editor.getValue() : ''); const lines = codeText.split('\n').slice(0, 30); lines.forEach((line, i) => { - tempCtx.fillText(line.substring(0, 100), 35, 95 + (i * 18)); + tempCtx.fillText(line.substring(0, 120), 35, 95 + (i * 18)); }); - if (video && video.readyState >= 2) { - try { - tempCtx.drawImage(video, w - 210, 70, 190, 140); - tempCtx.strokeStyle = '#6366f1'; - tempCtx.lineWidth = 2; - tempCtx.strokeRect(w - 210, 70, 190, 140); - } catch(e) {} - } - + // Candidate webcam feed is explicitly omitted to ensure screenshot is candidate system screen ONLY + const isCallStart = (reason === 'call_start'); tempCtx.fillStyle = 'rgba(15, 23, 42, 0.90)'; tempCtx.fillRect(0, tempCanvas.height - 42, tempCanvas.width, 42); - tempCtx.fillStyle = '#ef4444'; + tempCtx.fillStyle = isCallStart ? '#10b981' : '#ef4444'; tempCtx.font = 'bold 14px sans-serif'; - tempCtx.fillText('🚨 CANDIDATE TAB SWITCH DETECTED • SYSTEM SNAPSHOT • ' + new Date().toLocaleString(), 16, tempCanvas.height - 15); + const text = isCallStart + ? '📸 CALL STARTED • CANDIDATE SYSTEM SNAPSHOT • ' + new Date().toLocaleString() + : '🚨 CANDIDATE SYSTEM SNAPSHOT • ' + new Date().toLocaleString(); + tempCtx.fillText(text, 16, tempCanvas.height - 15); - uploadTabScreenshotBlob(tempCanvas.toDataURL('image/jpeg', 0.85)); + uploadTabScreenshotBlob(tempCanvas.toDataURL('image/jpeg', 0.85), reason); } - function uploadTabScreenshotBlob(imageData) { + function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') { fetch(`{{ route('interview.upload-tab-screenshot', $interview->id) }}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, - body: JSON.stringify({ image: imageData }) + body: JSON.stringify({ image: imageData, reason: reason }) }) .then(r => r.json()) .then(data => { - console.log('Computer screen tab-switch screenshot saved:', data); + console.log('Candidate system screenshot saved:', data); }) - .catch(err => console.log('Tab screenshot fetch error:', err)); + .catch(err => console.log('System screenshot fetch error:', err)); } // 1. Tab Switch Detection (Captures Candidate's SYSTEM DESKTOP MONITOR screenshot) @@ -1046,6 +1060,7 @@ class CandRingtoneEngine { function triggerCandidateRing(offer = null, callStartedAt = null) { if (isCandidateCallConnected) return; + if (latestCallStatus === 'ended' || latestCallStatus === 'idle') return; if (candidateDeclinedOrLeftCall) { showCandidateJoinOption(); return; @@ -1187,6 +1202,14 @@ function showCandidateJoinOption() { await handleInterviewerOffer(pendingOffer); pendingOffer = null; } + + // Automatically capture candidate system screenshot on call start (if screenshot feature enabled) + if (!hasCapturedCallStartScreenshot && isTabSwitchScreenshotEnabled) { + hasCapturedCallStartScreenshot = true; + setTimeout(() => { + captureAndUploadTabScreenshot('call_start'); + }, 1200); + } } // 4. WebRTC Real-Time 2-Way Interview Call Setup (Dual Signaling: BroadcastChannel + PeerJS) @@ -1197,6 +1220,8 @@ function showCandidateJoinOption() { let isMicEnabled = true; let isCamEnabled = true; let activeCallInstance = null; + let hasCapturedCallStartScreenshot = false; + let lastRungCallStartedAt = null; const globalCallChannel = new BroadcastChannel('global_call_alerts'); globalCallChannel.onmessage = (event) => { @@ -1472,6 +1497,20 @@ function candidateLogoutAction() { function candidateLeaveCall(isEndedByHost = false) { isCandidateCallConnected = false; + candidateDeclinedOrLeftCall = true; + + candRingtone.stop(); + if (candRingTimer) { + clearTimeout(candRingTimer); + candRingTimer = null; + } + + const modal = document.getElementById('candidate-incoming-modal'); + if (modal) { + modal.style.opacity = '0'; + modal.style.pointerEvents = 'none'; + } + if (activeCallInstance) { try { activeCallInstance.close(); } catch(e) {} activeCallInstance = null; @@ -1802,7 +1841,7 @@ function saveNotepadContent() { }, body: JSON.stringify({ notes: notes }) }); - }, 800); + }, 300); } // HTML5 Canvas Drawing Logic @@ -1850,6 +1889,7 @@ function draw(e) { ctx.strokeStyle = document.getElementById('draw-color').value; ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); + saveCanvasDrawing(); } function stopDraw() { @@ -1880,7 +1920,7 @@ function saveCanvasDrawing() { }, body: JSON.stringify({ drawing: drawingData }) }); - }, 1000); + }, 300); } setInterval(() => { @@ -1897,16 +1937,25 @@ function saveCanvasDrawing() { if (data.call_status === 'active') { latestCallStatus = 'active'; - if (!isCandidateCallConnected) { + if (!hasCapturedCallStartScreenshot && isTabSwitchScreenshotEnabled) { + hasCapturedCallStartScreenshot = true; + setTimeout(() => { + captureAndUploadTabScreenshot('call_start'); + }, 1200); + } + if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) { const elapsed = data.call_started_at ? (Date.now() - new Date(data.call_started_at).getTime()) : 999999; - if (elapsed <= 10000) { + if (elapsed <= 12000 && lastRungCallStartedAt !== data.call_started_at) { + lastRungCallStartedAt = data.call_started_at; triggerCandidateRing(null, data.call_started_at); - } else { + } else if (elapsed > 12000) { showCandidateJoinOption(); } } } else if (data.call_status === 'ended' || data.call_status === 'idle') { latestCallStatus = data.call_status; + hasCapturedCallStartScreenshot = false; + lastRungCallStartedAt = null; candRingtone.stop(); if (candRingTimer) clearTimeout(candRingTimer); isCandidateCallConnected = false; diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php index 8c3a698..361e7e0 100644 --- a/resources/views/interview/index.blade.php +++ b/resources/views/interview/index.blade.php @@ -215,6 +215,32 @@ {{ 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. +
+
+
+
+ + {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Screen Capturing Active)' : '🔴 STOPPED / DISABLED' }} + + @if(Auth::user() && Auth::user()->isAdmin()) +
+ @csrf + + + @endif +
+
@@ -1160,7 +1186,7 @@ function openReviewModal(id, name, uid, autoJoinCall = false) { pollReviewData(); clearInterval(sosPollInterval); - sosPollInterval = setInterval(pollReviewData, 2000); + sosPollInterval = setInterval(pollReviewData, 1200); if (autoJoinCall) { setTimeout(() => { @@ -1781,38 +1807,112 @@ function startInterviewerCall(isRejoin = false) { }); } - function leaveInterviewerCall() { + function leaveInterviewerCall(isExplicitEnd = true) { + if (currentInterviewId && isExplicitEnd) { + const endingId = currentInterviewId; + endedCallIds.add(endingId); + dismissedCallIds.add(endingId); + + if (typeof globalCallChannel !== 'undefined' && globalCallChannel) { + try { + globalCallChannel.postMessage({ + type: 'call_ended', + interview_id: endingId + }); + } catch(e) {} + } + + if (typeof callSigChannel !== 'undefined' && callSigChannel) { + try { + callSigChannel.postMessage({ type: 'end_call_all', interview_id: endingId }); + } catch(e) {} + } + + if (typeof interviewerSigChannel !== 'undefined' && interviewerSigChannel) { + try { + interviewerSigChannel.postMessage({ type: 'end_call_all', interview_id: endingId }); + } catch(e) {} + } + + fetch(`{{ route('interview.call-status', ':id') }}`.replace(':id', endingId), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': '{{ csrf_token() }}' + }, + body: JSON.stringify({ call_status: 'ended' }) + }).catch(e => {}); + } + if (interviewerLocalStream) { - interviewerLocalStream.getTracks().forEach(t => t.stop()); + try { + interviewerLocalStream.getTracks().forEach(t => t.stop()); + } catch(e) {} interviewerLocalStream = null; } if (activeCall) { - activeCall.close(); + try { activeCall.close(); } catch(e) {} activeCall = null; } - document.getElementById('cand-video-placeholder').style.display = 'flex'; - document.getElementById('self-video-placeholder').style.display = 'flex'; + const candPlace = document.getElementById('cand-video-placeholder'); + const screenPlace = document.getElementById('screen-video-placeholder'); + const selfPlace = document.getElementById('self-video-placeholder'); + if (candPlace) candPlace.style.display = 'flex'; + if (screenPlace) screenPlace.style.display = 'flex'; + if (selfPlace) selfPlace.style.display = 'flex'; - document.getElementById('btn-start-call').innerText = '📞 Join Call'; - document.getElementById('btn-start-call').style.display = 'inline-block'; - document.getElementById('btn-start-call').style.background = '#6366f1'; - document.getElementById('btn-leave-call').style.display = 'none'; - document.getElementById('btn-end-all-call').style.display = 'none'; + const candVid = document.getElementById('interviewer-cand-video'); + if (candVid) candVid.srcObject = null; - document.getElementById('btn-toggle-interviewer-mic').style.display = 'none'; - document.getElementById('btn-toggle-interviewer-cam').style.display = 'none'; + const selfVid = document.getElementById('interviewer-self-video'); + if (selfVid) selfVid.srcObject = null; - document.getElementById('interviewer-call-status').innerText = 'LEFT CALL'; - document.getElementById('interviewer-call-status').style.background = 'rgba(234,179,8,0.2)'; - document.getElementById('interviewer-call-status').style.color = '#fde047'; + const startBtn = document.getElementById('btn-start-call'); + if (startBtn) { + startBtn.innerText = '📞 Call Candidate'; + startBtn.style.display = 'inline-block'; + startBtn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)'; + startBtn.onclick = startInterviewerCall; + } + + const leaveBtn = document.getElementById('btn-leave-call'); + if (leaveBtn) leaveBtn.style.display = 'none'; + + const endAllBtn = document.getElementById('btn-end-all-call'); + if (endAllBtn) endAllBtn.style.display = 'none'; + + const micBtn = document.getElementById('btn-toggle-interviewer-mic'); + if (micBtn) micBtn.style.display = 'none'; + + const camBtn = document.getElementById('btn-toggle-interviewer-cam'); + if (camBtn) camBtn.style.display = 'none'; + + const statusBadge = document.getElementById('interviewer-call-status'); + if (statusBadge) { + statusBadge.innerText = 'CALL ENDED'; + statusBadge.style.background = 'rgba(239,68,68,0.2)'; + statusBadge.style.color = '#fca5a5'; + } } function endCallForAll() { + const executeEndCall = () => { + if (typeof callRingtone !== 'undefined' && callRingtone) { + try { callRingtone.stop(); } catch(e) {} + } + const modal = document.getElementById('incoming-call-modal'); + if (modal) modal.classList.remove('active'); + activeIncomingCall = null; + + iAmTheCaller = false; + leaveInterviewerCall(true); + }; + if (typeof Swal !== 'undefined') { Swal.fire({ title: '🛑 End Call for All Participants?', - text: 'This will terminate the 2-way call session for the candidate and all panelists.', + text: 'This will terminate the 2-way call session for candidate and all panelists.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', @@ -1820,36 +1920,13 @@ function endCallForAll() { confirmButtonText: 'Yes, End Call for All' }).then((result) => { if (result.isConfirmed) { - if (currentInterviewId) { - const endingId = currentInterviewId; - endedCallIds.add(endingId); - dismissedCallIds.add(endingId); - callRingtone.stop(); - document.getElementById('incoming-call-modal').classList.remove('active'); - activeIncomingCall = null; - - globalCallChannel.postMessage({ - type: 'call_ended', - interview_id: endingId - }); - - fetch(`{{ route('interview.call-status', ':id') }}`.replace(':id', endingId), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': '{{ csrf_token() }}' - }, - body: JSON.stringify({ call_status: 'ended' }) - }); - } - - iAmTheCaller = false; - leaveInterviewerCall(); - document.getElementById('interviewer-call-status').innerText = 'CALL ENDED FOR ALL'; - document.getElementById('interviewer-call-status').style.background = 'rgba(239,68,68,0.2)'; - document.getElementById('interviewer-call-status').style.color = '#fca5a5'; + executeEndCall(); } }); + } else { + if (confirm('End Call for All Participants?')) { + executeEndCall(); + } } } @@ -1867,42 +1944,53 @@ function startCallRecording() { let candidateStream = (candVid && candVid.srcObject) ? candVid.srcObject : null; let streamToRecord = new MediaStream(); - // Add Candidate Video and Audio tracks + // Add live Candidate Video and Audio tracks if (candidateStream) { - candidateStream.getTracks().forEach(track => streamToRecord.addTrack(track)); + candidateStream.getTracks().forEach(track => { + if (track.readyState === 'live') { + try { streamToRecord.addTrack(track); } catch(e) {} + } + }); } // Add Interviewer Local Microphone Audio track for 2-way call recording if (interviewerLocalStream) { interviewerLocalStream.getAudioTracks().forEach(track => { - // Only add if not already in streamToRecord - if (!streamToRecord.getAudioTracks().includes(track)) { - streamToRecord.addTrack(track); + if (track.readyState === 'live' && !streamToRecord.getAudioTracks().includes(track)) { + try { streamToRecord.addTrack(track); } catch(e) {} } }); - if (!candidateStream) { - interviewerLocalStream.getVideoTracks().forEach(track => streamToRecord.addTrack(track)); + if (streamToRecord.getVideoTracks().length === 0) { + interviewerLocalStream.getVideoTracks().forEach(track => { + if (track.readyState === 'live') { + try { streamToRecord.addTrack(track); } catch(e) {} + } + }); } } - if (streamToRecord.getTracks().length === 0) { - alert('No active video/audio stream available to record. Please click "Start / Join Call" first.'); + const activeTracks = streamToRecord.getTracks().filter(t => t.readyState === 'live'); + if (activeTracks.length === 0) { + alert('No active video/audio stream available to record. Please start or join the call first.'); return; } adminRecordedChunks = []; try { - let mime = 'video/webm'; - if (MediaRecorder.isTypeSupported('video/mp4')) { - mime = 'video/mp4'; - } else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) { - mime = 'video/webm;codecs=vp9'; - } else if (MediaRecorder.isTypeSupported('video/webm')) { - mime = 'video/webm'; + let options = {}; + if (typeof MediaRecorder.isTypeSupported === 'function') { + if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus')) { + options = { mimeType: 'video/webm;codecs=vp8,opus' }; + } else if (MediaRecorder.isTypeSupported('video/webm')) { + options = { mimeType: 'video/webm' }; + } } - const options = { mimeType: mime }; - adminMediaRecorder = new MediaRecorder(streamToRecord, options); + try { + adminMediaRecorder = new MediaRecorder(streamToRecord, options); + } catch(errOpt) { + adminMediaRecorder = new MediaRecorder(streamToRecord); + } adminMediaRecorder.ondataavailable = function(e) { if (e.data && e.data.size > 0) { @@ -1912,7 +2000,7 @@ function startCallRecording() { adminMediaRecorder.onstop = function() { if (adminRecordedChunks.length === 0) return; - const recMime = adminMediaRecorder.mimeType || mime; + const recMime = adminMediaRecorder.mimeType || 'video/webm'; const isMp4 = recMime.includes('mp4'); const ext = isMp4 ? 'mp4' : 'webm'; const blobType = isMp4 ? 'video/mp4' : 'video/webm'; @@ -1938,6 +2026,7 @@ function startCallRecording() { }); } } catch(e) { + console.error('MediaRecorder error:', e); alert('Recording failed to initialize: ' + e.message); } } @@ -2026,35 +2115,14 @@ function toggleInterviewerCam() { } function endInterviewerCall() { - if (interviewerLocalStream) { - interviewerLocalStream.getTracks().forEach(t => t.stop()); - interviewerLocalStream = null; - } - if (activeCall) { - activeCall.close(); - activeCall = null; - } - - document.getElementById('cand-video-placeholder').style.display = 'flex'; - document.getElementById('screen-video-placeholder').style.display = 'flex'; - document.getElementById('self-video-placeholder').style.display = 'flex'; - - document.getElementById('btn-start-call').innerText = '📞 Start 2-Way Call'; - document.getElementById('btn-start-call').style.background = '#10b981'; - document.getElementById('btn-start-call').onclick = startInterviewerCall; - - document.getElementById('btn-toggle-interviewer-mic').style.display = 'none'; - document.getElementById('btn-toggle-interviewer-cam').style.display = 'none'; - - document.getElementById('interviewer-call-status').innerText = 'DISCONNECTED'; - document.getElementById('interviewer-call-status').style.background = 'rgba(99,102,241,0.2)'; - document.getElementById('interviewer-call-status').style.color = '#818cf8'; + leaveInterviewerCall(true); } function closeReviewModal() { clearInterval(sosPollInterval); - endInterviewerCall(); - document.getElementById('review-modal').classList.remove('active'); + leaveInterviewerCall(true); + const modal = document.getElementById('review-modal'); + if (modal) modal.classList.remove('active'); } function sendSilentWarning() { diff --git a/routes/web.php b/routes/web.php index e223c02..5ff7c66 100644 --- a/routes/web.php +++ b/routes/web.php @@ -97,6 +97,7 @@ Route::delete('/roles/{id}', [AdminController::class, 'destroyRole'])->name('admin.roles.destroy'); Route::post('/settings/default-role', [AdminController::class, 'updateDefaultRole'])->name('admin.settings.default-role'); Route::post('/settings/onboarding-toggle', [AdminController::class, 'updateOnboardingToggle'])->name('admin.settings.onboarding-toggle'); + Route::post('/settings/screenshot-toggle', [AdminController::class, 'updateScreenshotToggle'])->name('admin.settings.screenshot-toggle'); // User Roles & Overrides Route::post('/users/{id}/roles', [AdminController::class, 'updateUserRoles'])->name('admin.users.roles.update');