Fix stale session redirect loop, filtering stats to reflect overrides
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m56s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m56s
- Auth layout verifies user exists in DB before redirecting to dashboard, breaking infinite loop for deleted accounts with stale sessions - Jury/Mentor layouts handle null user (deleted) by redirecting to login - Filtering stats cards and result list now use effective outcome (finalOutcome ?? outcome) instead of raw AI outcome - Award eligibility evaluation includes admin-overridden PASSED projects - Award shortlist reasoning column shows full text instead of truncating Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import Image from 'next/image'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export default async function AuthLayout({
|
||||
children,
|
||||
@@ -18,6 +19,13 @@ export default async function AuthLayout({
|
||||
// Redirect logged-in users to their dashboard
|
||||
// But NOT if they still need to set their password
|
||||
if (session?.user && !session.user.mustSetPassword) {
|
||||
// Verify user still exists in DB (handles deleted accounts with stale sessions)
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
if (dbUser) {
|
||||
const role = session.user.role
|
||||
if (role === 'SUPER_ADMIN' || role === 'PROGRAM_ADMIN') {
|
||||
redirect('/admin')
|
||||
@@ -29,6 +37,8 @@ export default async function AuthLayout({
|
||||
redirect('/mentor')
|
||||
}
|
||||
}
|
||||
// If user doesn't exist in DB, fall through and show auth page
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
|
||||
@@ -18,7 +18,12 @@ export default async function JuryLayout({
|
||||
select: { onboardingCompletedAt: true },
|
||||
})
|
||||
|
||||
if (!user?.onboardingCompletedAt) {
|
||||
if (!user) {
|
||||
// User was deleted — session is stale, send to login
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
if (!user.onboardingCompletedAt) {
|
||||
redirect('/onboarding')
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ export default async function MentorLayout({
|
||||
select: { onboardingCompletedAt: true },
|
||||
})
|
||||
|
||||
if (!user?.onboardingCompletedAt) {
|
||||
if (!user) {
|
||||
// User was deleted — session is stale, send to login
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
if (!user.onboardingCompletedAt) {
|
||||
redirect('/onboarding')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
Play,
|
||||
Trophy,
|
||||
AlertTriangle,
|
||||
ChevronsUpDown,
|
||||
} from 'lucide-react'
|
||||
|
||||
type AwardShortlistProps = {
|
||||
@@ -59,7 +58,6 @@ export function AwardShortlist({
|
||||
jobDone,
|
||||
}: AwardShortlistProps) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [expandedReasoning, setExpandedReasoning] = useState<Set<string>>(new Set())
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
const isRunning = jobStatus === 'PENDING' || jobStatus === 'PROCESSING'
|
||||
@@ -130,17 +128,6 @@ export function AwardShortlist({
|
||||
const allShortlisted = shortlist && shortlist.eligibilities.length > 0 && shortlist.eligibilities.every((e) => e.shortlisted)
|
||||
const someShortlisted = shortlistedCount > 0 && !allShortlisted
|
||||
|
||||
const toggleReasoning = (id: string) => {
|
||||
setExpandedReasoning((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleBulkToggle = () => {
|
||||
if (!shortlist) return
|
||||
@@ -286,7 +273,7 @@ export function AwardShortlist({
|
||||
<th className="px-3 py-2 text-left w-8">#</th>
|
||||
<th className="px-3 py-2 text-left">Project</th>
|
||||
<th className="px-3 py-2 text-left w-24">Score</th>
|
||||
<th className="px-3 py-2 text-left w-44">Reasoning</th>
|
||||
<th className="px-3 py-2 text-left min-w-[300px]">Reasoning</th>
|
||||
<th className="px-3 py-2 text-center w-20">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Checkbox
|
||||
@@ -304,7 +291,6 @@ export function AwardShortlist({
|
||||
{shortlist.eligibilities.map((e, i) => {
|
||||
const reasoning = (e.aiReasoningJson as Record<string, unknown>)?.reasoning as string | undefined
|
||||
const isTop5 = i < shortlistSize
|
||||
const isReasoningExpanded = expandedReasoning.has(e.id)
|
||||
return (
|
||||
<tr key={e.id} className={`border-t ${isTop5 ? 'bg-amber-50/50' : ''}`}>
|
||||
<td className="px-3 py-2 text-muted-foreground font-mono">
|
||||
@@ -337,18 +323,9 @@ export function AwardShortlist({
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{reasoning ? (
|
||||
<button
|
||||
onClick={() => toggleReasoning(e.id)}
|
||||
className="text-left w-full group"
|
||||
>
|
||||
<p className={`text-xs text-muted-foreground ${isReasoningExpanded ? '' : 'line-clamp-2'}`}>
|
||||
<p className="text-xs text-muted-foreground whitespace-pre-wrap leading-relaxed">
|
||||
{reasoning}
|
||||
</p>
|
||||
<span className="text-xs text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-0.5 mt-0.5">
|
||||
<ChevronsUpDown className="h-3 w-3" />
|
||||
{isReasoningExpanded ? 'Collapse' : 'Expand'}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">—</span>
|
||||
)}
|
||||
|
||||
@@ -855,7 +855,13 @@ export const filteringRouter = router({
|
||||
const skip = (page - 1) * perPage
|
||||
|
||||
const where: Record<string, unknown> = { roundId }
|
||||
if (outcome) where.outcome = outcome
|
||||
if (outcome) {
|
||||
// Filter by effective outcome (finalOutcome if overridden, otherwise original outcome)
|
||||
where.OR = [
|
||||
{ finalOutcome: outcome },
|
||||
{ finalOutcome: null, outcome },
|
||||
]
|
||||
}
|
||||
|
||||
const [results, total] = await Promise.all([
|
||||
ctx.prisma.filteringResult.findMany({
|
||||
@@ -896,15 +902,34 @@ export const filteringRouter = router({
|
||||
getResultStats: protectedProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Use effective outcome (finalOutcome if overridden, otherwise original outcome)
|
||||
const [passed, filteredOut, flagged, overridden] = await Promise.all([
|
||||
ctx.prisma.filteringResult.count({
|
||||
where: { roundId: input.roundId, outcome: 'PASSED' },
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
OR: [
|
||||
{ finalOutcome: 'PASSED' },
|
||||
{ finalOutcome: null, outcome: 'PASSED' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
ctx.prisma.filteringResult.count({
|
||||
where: { roundId: input.roundId, outcome: 'FILTERED_OUT' },
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
OR: [
|
||||
{ finalOutcome: 'FILTERED_OUT' },
|
||||
{ finalOutcome: null, outcome: 'FILTERED_OUT' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
ctx.prisma.filteringResult.count({
|
||||
where: { roundId: input.roundId, outcome: 'FLAGGED' },
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
OR: [
|
||||
{ finalOutcome: 'FLAGGED' },
|
||||
{ finalOutcome: null, outcome: 'FLAGGED' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
ctx.prisma.filteringResult.count({
|
||||
where: { roundId: input.roundId, overriddenBy: { not: null } },
|
||||
|
||||
@@ -59,9 +59,15 @@ export async function processEligibilityJob(
|
||||
}>
|
||||
|
||||
if (filteringRoundId) {
|
||||
// Scope to projects that PASSED filtering in the specified round
|
||||
// Scope to projects that effectively PASSED filtering (including admin overrides)
|
||||
const passedResults = await prisma.filteringResult.findMany({
|
||||
where: { roundId: filteringRoundId, outcome: 'PASSED' },
|
||||
where: {
|
||||
roundId: filteringRoundId,
|
||||
OR: [
|
||||
{ finalOutcome: 'PASSED' },
|
||||
{ finalOutcome: null, outcome: 'PASSED' },
|
||||
],
|
||||
},
|
||||
select: { projectId: true },
|
||||
})
|
||||
const passedIds = passedResults.map((r) => r.projectId)
|
||||
|
||||
Reference in New Issue
Block a user