Merge pull request 'subhajit_work_22_07' (#12) from subhajit_work_22_07 into main

Reviewed-on: #12
This commit is contained in:
subhajit.pal 2026-07-31 12:39:30 +00:00
commit ce4fa85c69
4 changed files with 310 additions and 234 deletions

View File

@ -155,69 +155,71 @@ public function executeCode(Request $request)
$code = $request->code;
$output = '';
// 1. Instant Local CLI Process Execution (0.02s response for Node.js, Python, PHP)
try {
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', '', $code);
eval($cleanCode);
$out = ob_get_clean();
$output = trim($out) ?: 'PHP code executed successfully.';
} catch (\Throwable $e) {
ob_end_clean();
$output = 'PHP Evaluation Error: ' . $e->getMessage();
// 1. Ultra-Fast Piston Code Execution API (sub-second for C, C++, Java, JS, Python, PHP, Go, Rust)
$pistonLangMap = [
'python' => 'python',
'py' => 'python',
'javascript' => 'javascript',
'js' => 'javascript',
'c' => 'c',
'cpp' => 'c++',
'c++' => 'c++',
'java' => 'java',
'php' => 'php',
'laravel' => 'php',
'go' => 'go',
'rust' => 'rust',
];
if (isset($pistonLangMap[$langKey])) {
try {
$pistonLang = $pistonLangMap[$langKey];
$response = Http::timeout(4)->post('https://emkc.org/api/v2/piston/execute', [
'language' => $pistonLang,
'version' => '*',
'files' => [
['content' => $code]
]
]);
if ($response->successful()) {
$resData = $response->json();
$output = $resData['run']['output'] ?? ($resData['run']['stdout'] ?? '');
if (empty($output) && !empty($resData['run']['stderr'])) {
$output = $resData['run']['stderr'];
}
}
}
} catch (\Exception $e) {
// Local execution exception; fall through to Judge0 API
} catch (\Exception $e) {}
}
// 2. Remote Compiler API (Judge0 CE) for C, C++, Java or non-local runtimes
// 2. Instant Local Process Runner Fallback
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));
try {
if (in_array($langKey, ['javascript', 'js'])) {
$process = new \Symfony\Component\Process\Process(['node', '-e', $code]);
$process->setTimeout(3);
$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(3);
$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', '', $code);
eval($cleanCode);
$out = ob_get_clean();
$output = trim($out) ?: 'PHP code executed successfully.';
} catch (\Throwable $e) {
ob_end_clean();
$output = 'PHP Evaluation Error: ' . $e->getMessage();
}
} catch (\Exception $e) {}
}
}
} catch (\Exception $e) {}
}
if (empty($output)) {
@ -533,6 +535,14 @@ public function getActiveCalls(Request $request)
$activeCalls = [];
foreach ($accessible as $interview) {
// Auto-terminate call session if interview timeline has expired
if ($interview->isExpired()) {
if ($interview->call_status === 'active') {
$interview->update(['call_status' => 'ended']);
}
continue;
}
if ($interview->call_status === 'active') {
$starterName = 'Panelist';
if ($interview->call_started_by) {
@ -570,6 +580,14 @@ public function getPollData($id)
->where('id', $id)
->orWhere('submission_unique_id', $id)
->firstOrFail();
// Enforce expiration: ended call status if timeline is expired
if ($interview->isExpired()) {
if ($interview->call_status === 'active') {
$interview->update(['call_status' => 'ended']);
}
}
$formattedRecordings = $this->getFormattedRecordings($interview);
$starterName = $interview->callStarter ? $interview->callStarter->name : null;
@ -580,7 +598,7 @@ public function getPollData($id)
return response()->json([
'status' => $interview->status,
'call_status' => $interview->call_status ?? 'idle',
'call_status' => $interview->isExpired() ? 'ended' : ($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,

View File

@ -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 @@
</div>
@endif
<!-- Global Admin Settings & Proctoring Control Panel -->
<section class="admin-card" style="margin-bottom: 35px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); border: 1.5px solid rgba(99, 102, 241, 0.35); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 12px;">
<div>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.3rem; font-weight: 800; color: #ffffff; margin: 0; display: flex; align-items: center; gap: 8px;">
⚙️ Global Admin Proctoring & System Settings
</h3>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
</div>
<span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); font-weight: 700; padding: 6px 12px; font-size: 0.75rem;">
🛡️ ADMIN CONTROL PANEL
</span>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
<div style="padding: 16px; background: rgba(16, 185, 129, 0.08); border: 1.5px solid rgba(16, 185, 129, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📸 Candidate System Screenshot Capture
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
</span>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }}
</span>
</label>
</div>
</form>
</div>
<!-- 2. Onboarding & Offboarding Module Master Toggle -->
<div style="padding: 16px; background: rgba(99, 102, 241, 0.08); border: 1.5px solid rgba(99, 102, 241, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
🚀 Candidate Provisioning & Onboarding Module
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
</span>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
</span>
</label>
</div>
</form>
</div>
</div>
</section>
<!-- Users Table Card -->
<section class="users-card" style="margin-bottom: 50px;">
@ -1241,41 +1175,7 @@
</select>
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; font-size:0.75rem; border-radius:8px; padding:8px 12px; border:none; cursor:pointer; font-weight:600; white-space: nowrap;">Save Default</button>
</div>
</div>
</form>
<!-- Onboarding & Offboarding Module Master Toggle -->
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST" style="margin-bottom: 12px; padding: 12px 14px; background: rgba(99, 102, 241, 0.08); border: 1px solid rgba(99, 102, 241, 0.25); border-radius: 12px;">
@csrf
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.8rem; color: #ffffff; font-weight: 600;">🚀 Onboarding & Offboarding Module</div>
<div style="font-size: 0.7rem; color: var(--text-muted);">Enable/disable candidate provisioning & 1-click revocation</div>
</div>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 18px; height: 18px;">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'ENABLED' : 'DISABLED' }}
</span>
</label>
</div>
</form>
<!-- Candidate System Screenshot Master Toggle (Admin Control) -->
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST" style="margin-bottom: 20px; padding: 12px 14px; background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.25); border-radius: 12px;">
@csrf
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.8rem; color: #ffffff; font-weight: 600;">📸 Candidate System Screenshot Capture</div>
<div style="font-size: 0.7rem; color: var(--text-muted);">Master Admin control to stop/start candidate system screen capturing (call start & tab switch)</div>
</div>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 18px; height: 18px;">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'ENABLED' : 'STOPPED / DISABLED' }}
</span>
</label>
</div>
</div>
</form>
<!-- Registered Roles List -->

View File

@ -368,7 +368,7 @@
<div style="color: #94a3b8; font-style: italic; text-align: center; padding-top: 40px;">No messages yet. Send a message below.</div>
</div>
<div style="display: flex; gap: 6px;">
<input type="text" id="candidate-chat-input" placeholder="Type message to panelist..." onkeydown="if(event.key==='Enter') sendCandidateMessage()" class="form-input" style="font-size: 0.75rem; background: #0b0f19; border: 1px solid var(--card-border); padding: 6px 10px; flex: 1;">
<input type="text" id="candidate-chat-input" placeholder="Type message to panelist..." onkeydown="if(event.key==='Enter') sendCandidateMessage()" class="form-input" style="font-size: 0.75rem; background: #0b0f19; color: #ffffff !important; border: 1px solid var(--card-border); padding: 6px 10px; flex: 1;">
<button onclick="sendCandidateMessage()" class="btn-ide" style="padding: 6px 10px; font-size: 0.75rem; background: #6366f1; border: none; font-weight: 600;">
Send
</button>
@ -458,13 +458,25 @@
}
});
// Real-Time Code Sync to Interviewer (Before Submission)
// Real-Time Instant BroadcastChannel + Backend Code Sync
const liveSyncChannel = new BroadcastChannel('live_sync_' + '{{ $interview->id }}');
liveSyncChannel.onmessage = function(event) {
if (event.data && event.data.type === 'chat_message' && typeof pollCandidateChat === 'function') {
pollCandidateChat();
}
};
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 +485,7 @@
},
body: JSON.stringify({ code: code, language: lang })
});
}, 600);
}, 200);
});
});
@ -1176,6 +1188,10 @@ function showCandidateJoinOption() {
if (joinBtn) joinBtn.style.display = 'none';
if (!localMediaStream) {
await startCandidateCameraStream();
}
isCandidateCallConnected = true;
const badge = document.getElementById('call-status-badge');
@ -1380,6 +1396,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 +1410,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';
}
}
});
@ -1805,6 +1829,10 @@ function sendCandidateMessage() {
if (!msg) return;
input.value = '';
if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) {
liveSyncChannel.postMessage({ type: 'chat_message', message: msg, sender: 'candidate' });
}
fetch(`{{ route('interview.candidate.send-message', $interview->id) }}`, {
method: 'POST',
headers: {
@ -1815,9 +1843,7 @@ function sendCandidateMessage() {
})
.then(r => r.json())
.then(res => {
if (res.success) {
pollCandidateChat();
}
pollCandidateChat();
});
}
@ -1830,9 +1856,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 +1871,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 +1920,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 +1960,12 @@ function saveCanvasDrawing() {
},
body: JSON.stringify({ drawing: drawingData })
});
}, 300);
} else {
clearTimeout(drawingSaveTimeout);
drawingSaveTimeout = setTimeout(() => {
saveCanvasDrawing(true);
}, 250);
}
}
setInterval(() => {
@ -1944,18 +1989,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 +2018,7 @@ function saveCanvasDrawing() {
}
})
.catch(() => {});
}, 2200);
}, 1000);
</script>
</body>
</html>

View File

@ -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') }}
</div>
@endif
<!-- Admin Proctoring & System Screenshot Control Banner -->
<div style="margin-bottom: 20px; padding: 14px 20px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.9) 100%); border: 1.5px solid rgba(16, 185, 129, 0.35); border-radius: 14px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="font-size: 1.5rem;">📸</div>
<!-- Global Admin Settings & Proctoring Control Panel -->
<section class="admin-card" style="margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); border: 1.5px solid rgba(99, 102, 241, 0.35); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 12px;">
<div>
<div style="font-size: 0.9rem; font-weight: 700; color: white;">Global Candidate System Screenshot Control</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">
Captures candidate desktop screen on call start & tab switches.
Admin can stop/start screen capturing globally anytime.
</div>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 800; color: #ffffff; margin: 0; display: flex; align-items: center; gap: 8px;">
⚙️ Global Admin Proctoring & System Settings
</h3>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 12px;">
<span id="global-screenshot-status-text" style="font-size: 0.8rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Screen Capturing Active)' : '🔴 STOPPED / DISABLED' }}
<span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); font-weight: 700; padding: 6px 12px; font-size: 0.75rem;">
🛡️ ADMIN CONTROL PANEL
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST" style="margin: 0;">
@csrf
<button type="submit" class="btn-nav" style="padding: 6px 14px; font-size: 0.75rem; font-weight: 700; background: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)' }}; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#fca5a5' : '#34d399' }}; border: 1px solid {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#ef4444' : '#10b981' }}; border-radius: 8px; cursor: pointer;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🛑 Stop Screenshot Capture' : '🟢 Start Screenshot Capture' }}
</button>
</form>
@endif
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
<div style="padding: 16px; background: rgba(16, 185, 129, 0.08); border: 1.5px solid rgba(16, 185, 129, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📸 Candidate System Screenshot Capture
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span id="global-screenshot-status-text" style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }}
</span>
</label>
@endif
</div>
</form>
</div>
<!-- 2. Onboarding & Offboarding Module Master Toggle -->
<div style="padding: 16px; background: rgba(99, 102, 241, 0.08); border: 1.5px solid rgba(99, 102, 241, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
🚀 Candidate Provisioning & Onboarding Module
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
</span>
</label>
@endif
</div>
</form>
</div>
</div>
</section>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<div>
@ -697,7 +749,7 @@
<div style="color: #94a3b8; font-style: italic; text-align: center; padding-top: 40px;">No chat history. Send a message below.</div>
</div>
<div style="display: flex; gap: 6px;">
<input type="text" id="warning-input" placeholder="Type message to candidate..." onkeydown="if(event.key==='Enter') sendSilentWarning()" class="form-input" style="font-size: 0.75rem; background: #0b0f19; padding: 6px 10px; flex: 1;">
<input type="text" id="warning-input" placeholder="Type message to candidate..." onkeydown="if(event.key==='Enter') sendSilentWarning()" class="form-input" style="font-size: 0.75rem; background: #0b0f19; color: #ffffff !important; border: 1px solid var(--card-border); padding: 6px 10px; flex: 1;">
<button onclick="sendSilentWarning()" class="btn-nav" style="background: #6366f1; color: white; border: none; font-size: 0.75rem; font-weight: 600; padding: 6px 12px; cursor: pointer;">
Send
</button>
@ -1029,17 +1081,24 @@ class CallRingtoneEngine {
const endedCallIds = new Set();
const currentUserId = {{ Auth::id() }};
const globalCallChannel = new BroadcastChannel('global_call_alerts');
const adminPageLoadTimestamp = Date.now();
function triggerIncomingCallRing(callData) {
function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) {
if (endedCallIds.has(callData.id) || dismissedCallIds.has(callData.id)) return;
if (iAmTheCaller && currentInterviewId == callData.id) return;
if (interviewerLocalStream && currentInterviewId == callData.id) return;
// Check ring 10-second threshold based on call_started_at
// Only play ringtone for real-time fresh calls initiated within the last 8 seconds
if (callData.call_started_at) {
const startedTime = new Date(callData.call_started_at).getTime();
const elapsedMs = Date.now() - startedTime;
if (elapsedMs > 10000) return;
if (elapsedMs > 8000 && !isRealtimeBroadcast) {
dismissedCallIds.add(callData.id);
return;
}
} else if (!isRealtimeBroadcast) {
dismissedCallIds.add(callData.id);
return;
}
activeIncomingCall = callData;
@ -1048,13 +1107,13 @@ function triggerIncomingCallRing(callData) {
document.getElementById('incoming-call-modal').classList.add('active');
callRingtone.start();
// Set 10-second auto-stop ring timeout
// Set 8-second auto-stop ring timeout
if (ringTimeoutTimer) clearTimeout(ringTimeoutTimer);
let remainingMs = 10000;
let remainingMs = 8000;
if (callData.call_started_at) {
const startedTime = new Date(callData.call_started_at).getTime();
const elapsedMs = Date.now() - startedTime;
remainingMs = Math.max(1000, 10000 - elapsedMs);
remainingMs = Math.max(1000, 8000 - elapsedMs);
}
ringTimeoutTimer = setTimeout(() => {
@ -1073,7 +1132,7 @@ function triggerIncomingCallRing(callData) {
submission_unique_id: data.submission_unique_id,
starter_name: data.starter_name,
call_started_at: data.call_started_at || new Date().toISOString()
});
}, true);
} else if (data.type === 'call_ended') {
endedCallIds.add(data.interview_id);
dismissedCallIds.add(data.interview_id);
@ -1111,8 +1170,16 @@ function pollGlobalActiveCalls() {
});
if (incoming) {
// Check if call was started before page refresh
const startedTime = incoming.call_started_at ? new Date(incoming.call_started_at).getTime() : 0;
if (startedTime > 0 && startedTime < (adminPageLoadTimestamp - 4000)) {
// Ongoing call from before page reload: silent status update without ringtone
dismissedCallIds.add(incoming.id);
return;
}
if (!activeIncomingCall || activeIncomingCall.id !== incoming.id) {
triggerIncomingCallRing(incoming);
triggerIncomingCallRing(incoming, false);
}
} else if (activeIncomingCall) {
declineIncomingCall(true);
@ -1121,8 +1188,8 @@ function pollGlobalActiveCalls() {
.catch(() => {});
}
// Poll for active calls every 1 second for instant ring notification
setInterval(pollGlobalActiveCalls, 1000);
// Poll for active calls every 1.5 seconds
setInterval(pollGlobalActiveCalls, 1500);
function acceptIncomingCall() {
callRingtone.stop();
@ -1165,6 +1232,50 @@ 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';
}
} else if (data.type === 'chat_message') {
pollReviewData();
}
};
}
function openReviewModal(id, name, uid, autoJoinCall = false) {
currentInterviewId = id;
acknowledgedViolationCount = 0;
@ -1184,9 +1295,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(() => {
@ -2120,6 +2232,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');
@ -2131,6 +2247,10 @@ function sendSilentWarning() {
if (!msg || !currentInterviewId) return;
input.value = '';
if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) {
liveSyncChannel.postMessage({ type: 'chat_message', message: msg, sender: 'interviewer' });
}
fetch(`{{ route('interview.warning', ':id') }}`.replace(':id', currentInterviewId), {
method: 'POST',
headers: {
@ -2141,9 +2261,7 @@ function sendSilentWarning() {
})
.then(r => r.json())
.then(res => {
if (res.success) {
pollReviewData();
}
pollReviewData();
});
}