Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/api/interview/[id]/chaos-timeout/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/src/lib/db/mongoose';
import InterviewSession from '@/src/lib/db/models/InterviewSession';
import InterviewSession, { IConstraintChange } from '@/src/lib/db/models/InterviewSession';

export async function POST(
req: NextRequest,
Expand All @@ -19,7 +19,7 @@ export async function POST(

// 2. Locate Constraint
const constraintChanges = session.constraintChanges || [];
const constraintIndex = constraintChanges.findIndex((c: { id: string }) => c.id === constraintId);
const constraintIndex = constraintChanges.findIndex((c: IConstraintChange) => c.id === constraintId);
if (constraintIndex === -1) {
return NextResponse.json({ error: 'Constraint not found' }, { status: 404 });
}
Expand Down Expand Up @@ -62,4 +62,4 @@ export async function POST(
console.error('Chaos Timeout Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}
32 changes: 27 additions & 5 deletions app/api/interview/[id]/final-validation/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/src/lib/db/mongoose';
import dbConnect, { isValidObjectId } from '@/src/lib/db/mongoose';
import InterviewSession from '@/src/lib/db/models/InterviewSession';
import User from '@/src/lib/db/models/User';
import { getAuthenticatedUser } from '@/src/lib/firebase/firebaseAdmin';

export async function POST(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
await dbConnect();
const { id } = await context.params;

const session = await InterviewSession.findOne({ id });
// Validate session ID format
if (!isValidObjectId(id)) {
return NextResponse.json({ error: 'Invalid session ID' }, { status: 400 });
}

// Authenticate user
const authHeader = req.headers.get('Authorization');
const authenticatedUser = await getAuthenticatedUser(authHeader);
if (!authenticatedUser) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

await dbConnect();

// Find user in database
const user = await User.findOne({ firebaseUid: authenticatedUser.uid });
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}

// Find session and verify ownership
const session = await InterviewSession.findOne({ _id: id, userId: user._id });
if (!session) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
}

if (!session.aiMessages) session.aiMessages = [];

// Idempotency check: Have we already sent it?
const alreadySent = session.aiMessages.some((m: { role: string; content: string }) =>
const alreadySent = session.aiMessages.some((m: { role: string; content: string }) =>
m.role === 'interviewer' && m.content.includes("Final Validation Phase")
);

Expand All @@ -39,4 +61,4 @@ export async function POST(
console.error('Final Validation Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
}
18 changes: 13 additions & 5 deletions app/interview/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,30 @@ export default function InterviewCanvasPage({ params }: PageProps) {
onConstraintChange: handleConstraintChange
});

// Extract stable setter from ai object
const { setMessages } = ai;

// Final Validation Phase Trigger automatically checks and dispatches.
useEffect(() => {
if (!finalValidationTriggered && timer.minutes !== undefined && timer.minutes <= 10 && session?.status === 'in_progress') {
setFinalValidationTriggered(true);

authFetch(`/api/interview/${id}/final-validation`, { method: 'POST' })
.then(res => res.json())
.then(async (res) => {
if (!res.ok) {
throw new Error(await res.text() || res.statusText);
}
return res.json();
})
.then(data => {
if (data.success && data.messages) {
if (ai.setMessages) ai.setMessages(data.messages);
if (setMessages) setMessages(data.messages);
setIsInterviewPanelOpen(true);
}
})
.catch(err => console.error('Final validation trigger failed:', err));
}
}, [timer.minutes, finalValidationTriggered, session?.status, id, ai]);
}, [timer.minutes, finalValidationTriggered, session?.status, id, setMessages]);

// Fetch session data
const fetchSession = useCallback(async () => {
Expand Down Expand Up @@ -452,4 +460,4 @@ export default function InterviewCanvasPage({ params }: PageProps) {
)}
</div>
);
}
}
16 changes: 7 additions & 9 deletions components/interview/ChaosTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) {

const calculateRemaining = useCallback(() => {
if (constraint.failedAt) return { remaining: 0, mode: 'failed' as const };

const now = Date.now();
if (constraint.overtimeAt) {
const overtimeEnd = new Date(constraint.overtimeAt).getTime() + PENALTY_MS;
Expand All @@ -30,24 +30,22 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) {

const normalEnd = new Date(constraint.introducedAt).getTime() + WARNING_MS;
const remaining = normalEnd - now;

if (remaining <= 0) {
// It hit 0 naturally but we haven't received DB overtime flag yet
return { remaining: 0, mode: 'normal' as const };
}
return { remaining: Math.ceil(remaining / 1000), mode: 'normal' as const };
}, [constraint]);
}, [constraint, PENALTY_MS, WARNING_MS]);

const [state, setState] = useState(() => calculateRemaining());

// To prevent spamming the effect while waiting for DB response

// Prevent duplicate timeout callbacks while waiting for DB response.
// These refs track whether we've already sent a warning or penalty request.
const hasTriggeredWarning = useRef(false);
const hasTriggeredPenalty = useRef(false);

useEffect(() => {
// Reset triggers if DB fields update
if (constraint.overtimeAt) hasTriggeredWarning.current = false;
if (constraint.failedAt) hasTriggeredPenalty.current = false;

const interval = setInterval(() => {
const current = calculateRemaining();
Expand Down Expand Up @@ -95,4 +93,4 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) {
</div>
</div>
);
}
}