diff --git a/app/Http/Controllers/InterviewController.php b/app/Http/Controllers/InterviewController.php
index a71007f..7aae89d 100644
--- a/app/Http/Controllers/InterviewController.php
+++ b/app/Http/Controllers/InterviewController.php
@@ -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;
}
diff --git a/app/Models/Interview.php b/app/Models/Interview.php
index a81f96b..22bdc45 100644
--- a/app/Models/Interview.php
+++ b/app/Models/Interview.php
@@ -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.
*/
diff --git a/resources/views/components/interview/call-controls.blade.php b/resources/views/components/interview/call-controls.blade.php
index 35804d7..cc2f890 100644
--- a/resources/views/components/interview/call-controls.blade.php
+++ b/resources/views/components/interview/call-controls.blade.php
@@ -10,11 +10,11 @@
- @if($interview->call_status === 'ended')
+ @if($interview->isExpired() || $interview->call_status === 'ended')
Call Ended
- @elseif($interview->call_status === 'active')
+ @elseif(!$interview->isExpired() && $interview->call_status === 'active')
Join Call
diff --git a/resources/views/components/interview/candidate-header.blade.php b/resources/views/components/interview/candidate-header.blade.php
index d621e83..e58d82f 100644
--- a/resources/views/components/interview/candidate-header.blade.php
+++ b/resources/views/components/interview/candidate-header.blade.php
@@ -76,7 +76,7 @@
Expired
@elseif($interview->status === 'completed')
Completed
- @elseif($interview->call_status === 'active')
+ @elseif(!$interview->isExpired() && $interview->call_status === 'active')
Call in progress
@elseif($interview->isActiveTimeline())
Timeline live
diff --git a/resources/views/components/interview/session-row.blade.php b/resources/views/components/interview/session-row.blade.php
index 34936e1..be312cc 100644
--- a/resources/views/components/interview/session-row.blade.php
+++ b/resources/views/components/interview/session-row.blade.php
@@ -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
@@ -36,7 +40,7 @@
EXPIRED
@elseif($interview->status === 'completed')
COMPLETED
- @elseif($interview->call_status === 'active')
+ @elseif(!$interview->isExpired() && $interview->call_status === 'active')
@elseif($interview->isActiveTimeline())
TIMELINE LIVE
@@ -71,7 +75,7 @@
- @if($interview->call_status === 'active')
+ @if(!$interview->isExpired() && $interview->call_status === 'active')
Join Active Call
diff --git a/tests/Feature/CandidateInterviewPortalTest.php b/tests/Feature/CandidateInterviewPortalTest.php
index fe71bbb..e19aeb6 100644
--- a/tests/Feature/CandidateInterviewPortalTest.php
+++ b/tests/Feature/CandidateInterviewPortalTest.php
@@ -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);
+ }
}