fix: handle joining expired active calls
- Mark the call as ended when it has expired but was not gracefully terminated. - Prevent users from joining calls that are already expired due to peers closing the tab or an ungraceful call termination.
This commit is contained in:
parent
9fdd101bfc
commit
68e2d62888
@ -32,6 +32,15 @@ public function index()
|
||||
}
|
||||
|
||||
$interviews = $user->getAccessibleInterviews();
|
||||
|
||||
// Auto-terminate call session if interview timeline has expired
|
||||
foreach ($interviews as $interview) {
|
||||
if ($interview->isExpired() && $interview->call_status === 'active') {
|
||||
$interview->update(['call_status' => 'ended', 'active_peers' => []]);
|
||||
$interview->call_status = 'ended';
|
||||
}
|
||||
}
|
||||
|
||||
$interviewers = User::orderBy('name', 'asc')->get();
|
||||
|
||||
return view('interview.index', compact('interviews', 'interviewers'));
|
||||
@ -49,6 +58,13 @@ public function show($id)
|
||||
}
|
||||
|
||||
$interview = Interview::findOrFail($id);
|
||||
|
||||
// Auto-terminate call session if interview timeline has expired
|
||||
if ($interview->isExpired() && $interview->call_status === 'active') {
|
||||
$interview->update(['call_status' => 'ended', 'active_peers' => []]);
|
||||
$interview->call_status = 'ended';
|
||||
}
|
||||
|
||||
$interviewers = User::orderBy('name', 'asc')->get();
|
||||
|
||||
return view('interview.show', compact('interview', 'interviewers'));
|
||||
@ -625,7 +641,8 @@ public function getActiveCalls(Request $request)
|
||||
// Auto-terminate call session if interview timeline has expired
|
||||
if ($interview->isExpired()) {
|
||||
if ($interview->call_status === 'active') {
|
||||
$interview->update(['call_status' => 'ended']);
|
||||
$interview->update(['call_status' => 'ended', 'active_peers' => []]);
|
||||
$interview->call_status = 'ended';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -70,6 +70,18 @@ public function isExpired(): bool
|
||||
return now()->greaterThan($this->expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the interview call is active and unexpired.
|
||||
*/
|
||||
public function isCallActive(): bool
|
||||
{
|
||||
if ($this->isExpired() || in_array($this->status, ['completed', 'expired'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->call_status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current time is within the interview timeline.
|
||||
*/
|
||||
|
||||
@ -10,11 +10,11 @@
|
||||
<x-button id="btn-toggle-interviewer-cam" onclick="toggleInterviewerCam()" variant="ghost" icon="fa-solid fa-video" class="hidden" title="Cam On / Off"></x-button>
|
||||
|
||||
<!-- 3. Start / Join / Restart Call -->
|
||||
@if($interview->call_status === 'ended')
|
||||
@if($interview->isExpired() || $interview->call_status === 'ended')
|
||||
<x-button id="btn-start-call" disabled variant="primary" icon="fa-solid fa-phone-slash" class="opacity-50 cursor-not-allowed pointer-events-none">
|
||||
Call Ended
|
||||
</x-button>
|
||||
@elseif($interview->call_status === 'active')
|
||||
@elseif(!$interview->isExpired() && $interview->call_status === 'active')
|
||||
<x-button id="btn-start-call" onclick="startInterviewerCall()" variant="primary" icon="fa-solid fa-phone">
|
||||
Join Call
|
||||
</x-button>
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
<x-pill id="timeline-status-pill" variant="danger" size="sm" icon="fa-solid fa-circle">Expired</x-pill>
|
||||
@elseif($interview->status === 'completed')
|
||||
<x-pill id="timeline-status-pill" variant="success" size="sm" icon="fa-solid fa-circle">Completed</x-pill>
|
||||
@elseif($interview->call_status === 'active')
|
||||
@elseif(!$interview->isExpired() && $interview->call_status === 'active')
|
||||
<x-pill id="timeline-status-pill" variant="info" size="sm" class="animate-pulse" icon="fa-solid fa-circle">Call in progress</x-pill>
|
||||
@elseif($interview->isActiveTimeline())
|
||||
<x-pill id="timeline-status-pill" variant="info" size="sm" icon="fa-solid fa-bolt">Timeline live</x-pill>
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
|
||||
@php
|
||||
$metrics = $interview->proctor_metrics;
|
||||
if ($interview->isExpired() && $interview->call_status === 'active') {
|
||||
$interview->update(['call_status' => 'ended', 'active_peers' => []]);
|
||||
$interview->call_status = 'ended';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<tr class="hover:bg-white/[0.02] transition-colors">
|
||||
@ -36,7 +40,7 @@
|
||||
<x-pill variant="danger" size="sm">EXPIRED</x-pill>
|
||||
@elseif($interview->status === 'completed')
|
||||
<x-pill variant="success" size="sm">COMPLETED</x-pill>
|
||||
@elseif($interview->call_status === 'active')
|
||||
@elseif(!$interview->isExpired() && $interview->call_status === 'active')
|
||||
<x-pill variant="success" size="sm" icon="fa-solid fa-phone" class="animate-pulse font-extrabold">CALL IN PROGRESS</x-pill>
|
||||
@elseif($interview->isActiveTimeline())
|
||||
<x-pill variant="info" size="sm" icon="fa-solid fa-bolt">TIMELINE LIVE</x-pill>
|
||||
@ -71,7 +75,7 @@
|
||||
</a>
|
||||
|
||||
<!-- Button 2: Join Call -->
|
||||
@if($interview->call_status === 'active')
|
||||
@if(!$interview->isExpired() && $interview->call_status === 'active')
|
||||
<a href="{{ route('interview.show', $interview->id) }}?join_call=1">
|
||||
<x-button variant="success" size="sm" icon="fa-solid fa-phone-volume" class="animate-pulse font-extrabold shadow-md shadow-emerald-500/50">
|
||||
Join Active Call
|
||||
|
||||
@ -416,4 +416,49 @@ public function test_tab_switch_logging_and_external_ai_detection_behavior(): vo
|
||||
->assertJsonFragment(['type' => 'talking_on_phone'])
|
||||
->assertJsonFragment(['type' => 'reading_external_device']);
|
||||
}
|
||||
|
||||
public function test_expired_interview_automatically_ends_call_status_when_rendering_view(): void
|
||||
{
|
||||
$admin = User::create([
|
||||
'name' => 'Admin Test',
|
||||
'email' => 'admintest@example.com',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
|
||||
// Create an expired interview that had active call_status in DB
|
||||
$expiredInterview = Interview::create([
|
||||
'candidate_name' => 'Expired Candidate',
|
||||
'candidate_email' => 'expired@example.com',
|
||||
'candidate_phone' => '1231231234',
|
||||
'temp_password' => 'Pass-111222',
|
||||
'expires_at' => now()->subHour(),
|
||||
'language' => 'python',
|
||||
'status' => 'scheduled',
|
||||
'call_status' => 'active',
|
||||
'active_peers' => [['peer_id' => 'peer_1', 'last_seen' => now()->subHour()->timestamp]],
|
||||
'submission_unique_id' => 'expired-cand_1231231234_1752000100',
|
||||
]);
|
||||
|
||||
$this->assertTrue($expiredInterview->isExpired());
|
||||
$this->assertFalse($expiredInterview->isCallActive());
|
||||
|
||||
// 1. Loading index page view should automatically end the call status
|
||||
$response = $this->actingAs($admin)->get(route('interview.index'));
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('EXPIRED');
|
||||
$response->assertDontSee('Join Active Call');
|
||||
|
||||
$expiredInterview->refresh();
|
||||
$this->assertEquals('ended', $expiredInterview->call_status);
|
||||
$this->assertEmpty($expiredInterview->active_peers);
|
||||
|
||||
// 2. Reset back to active and test show page view
|
||||
$expiredInterview->update(['call_status' => 'active']);
|
||||
$showResp = $this->actingAs($admin)->get(route('interview.show', $expiredInterview->id));
|
||||
$showResp->assertStatus(200);
|
||||
$showResp->assertSee('Call Ended');
|
||||
|
||||
$expiredInterview->refresh();
|
||||
$this->assertEquals('ended', $expiredInterview->call_status);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user