feat: ranking in-progress indicator persists across all admin users
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m1s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m1s
- Create snapshot with status RUNNING before AI call starts - Update to COMPLETED/FAILED when done - Dashboard derives rankingInProgress from server snapshot status - All admins see the spinner, not just the one who triggered it - Poll snapshots every 3s so progress updates appear quickly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -274,11 +274,17 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
|||||||
// ─── tRPC queries ─────────────────────────────────────────────────────────
|
// ─── tRPC queries ─────────────────────────────────────────────────────────
|
||||||
const { data: snapshots, isLoading: snapshotsLoading } = trpc.ranking.listSnapshots.useQuery(
|
const { data: snapshots, isLoading: snapshotsLoading } = trpc.ranking.listSnapshots.useQuery(
|
||||||
{ roundId },
|
{ roundId },
|
||||||
{ refetchInterval: 30_000 },
|
// Poll every 3s so all admins see ranking progress/completion quickly
|
||||||
|
{ refetchInterval: 3_000 },
|
||||||
)
|
)
|
||||||
|
|
||||||
const latestSnapshotId = snapshots?.[0]?.id ?? null
|
// Derive ranking-in-progress from server state (visible to ALL admins)
|
||||||
const latestSnapshot = snapshots?.[0] ?? null
|
const rankingInProgress = snapshots?.[0]?.status === 'RUNNING'
|
||||||
|
|
||||||
|
// Find the latest COMPLETED snapshot (skip RUNNING/FAILED)
|
||||||
|
const latestCompleted = snapshots?.find((s) => s.status === 'COMPLETED')
|
||||||
|
const latestSnapshotId = latestCompleted?.id ?? null
|
||||||
|
const latestSnapshot = latestCompleted ?? null
|
||||||
|
|
||||||
const { data: snapshot, isLoading: snapshotLoading } = trpc.ranking.getSnapshot.useQuery(
|
const { data: snapshot, isLoading: snapshotLoading } = trpc.ranking.getSnapshot.useQuery(
|
||||||
{ snapshotId: latestSnapshotId! },
|
{ snapshotId: latestSnapshotId! },
|
||||||
@@ -322,20 +328,16 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
|||||||
onError: (err) => toast.error(`Failed to save: ${err.message}`),
|
onError: (err) => toast.error(`Failed to save: ${err.message}`),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [rankingInProgress, setRankingInProgress] = useState(false)
|
|
||||||
|
|
||||||
const triggerRankMutation = trpc.ranking.triggerAutoRank.useMutation({
|
const triggerRankMutation = trpc.ranking.triggerAutoRank.useMutation({
|
||||||
onMutate: () => setRankingInProgress(true),
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Ranking complete!')
|
toast.success('Ranking complete!')
|
||||||
initialized.current = false // allow re-init on next snapshot load
|
initialized.current = false // allow re-init on next snapshot load
|
||||||
void utils.ranking.listSnapshots.invalidate({ roundId })
|
void utils.ranking.listSnapshots.invalidate({ roundId })
|
||||||
void utils.ranking.getSnapshot.invalidate()
|
void utils.ranking.getSnapshot.invalidate()
|
||||||
setRankingInProgress(false)
|
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err.message)
|
toast.error(err.message)
|
||||||
setRankingInProgress(false)
|
void utils.ranking.listSnapshots.invalidate({ roundId })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -278,37 +278,55 @@ export const rankingRouter = router({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await aiQuickRank(criteriaText, roundId, ctx.prisma, ctx.user.id)
|
// Create a RUNNING snapshot so all admins see the in-progress indicator
|
||||||
|
|
||||||
// Embed weights alongside rules for audit
|
|
||||||
const criteriaWeights = config.criteriaWeights ?? undefined
|
|
||||||
const parsedRulesWithWeights = { rules: result.parsedRules, weights: criteriaWeights } as unknown as Prisma.InputJsonValue
|
|
||||||
const snapshot = await ctx.prisma.rankingSnapshot.create({
|
const snapshot = await ctx.prisma.rankingSnapshot.create({
|
||||||
data: {
|
data: {
|
||||||
roundId,
|
roundId,
|
||||||
triggeredById: ctx.user.id,
|
triggeredById: ctx.user.id,
|
||||||
triggerType: 'MANUAL',
|
triggerType: 'MANUAL',
|
||||||
criteriaText,
|
criteriaText,
|
||||||
parsedRulesJson: parsedRulesWithWeights,
|
parsedRulesJson: {} as Prisma.InputJsonValue,
|
||||||
startupRankingJson: result.startup.rankedProjects as unknown as Prisma.InputJsonValue,
|
|
||||||
conceptRankingJson: result.concept.rankedProjects as unknown as Prisma.InputJsonValue,
|
|
||||||
mode: 'QUICK',
|
mode: 'QUICK',
|
||||||
status: 'COMPLETED',
|
status: 'RUNNING',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await logAudit({
|
try {
|
||||||
prisma: ctx.prisma,
|
const result = await aiQuickRank(criteriaText, roundId, ctx.prisma, ctx.user.id)
|
||||||
userId: ctx.user.id,
|
|
||||||
action: 'RANKING_MANUAL_TRIGGERED',
|
|
||||||
entityType: 'RankingSnapshot',
|
|
||||||
entityId: snapshot.id,
|
|
||||||
detailsJson: { roundId },
|
|
||||||
ipAddress: ctx.ip,
|
|
||||||
userAgent: ctx.userAgent,
|
|
||||||
})
|
|
||||||
|
|
||||||
return { snapshotId: snapshot.id, startup: result.startup, concept: result.concept }
|
// Embed weights alongside rules for audit
|
||||||
|
const criteriaWeights = config.criteriaWeights ?? undefined
|
||||||
|
const parsedRulesWithWeights = { rules: result.parsedRules, weights: criteriaWeights } as unknown as Prisma.InputJsonValue
|
||||||
|
await ctx.prisma.rankingSnapshot.update({
|
||||||
|
where: { id: snapshot.id },
|
||||||
|
data: {
|
||||||
|
status: 'COMPLETED',
|
||||||
|
parsedRulesJson: parsedRulesWithWeights,
|
||||||
|
startupRankingJson: result.startup.rankedProjects as unknown as Prisma.InputJsonValue,
|
||||||
|
conceptRankingJson: result.concept.rankedProjects as unknown as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await logAudit({
|
||||||
|
prisma: ctx.prisma,
|
||||||
|
userId: ctx.user.id,
|
||||||
|
action: 'RANKING_MANUAL_TRIGGERED',
|
||||||
|
entityType: 'RankingSnapshot',
|
||||||
|
entityId: snapshot.id,
|
||||||
|
detailsJson: { roundId },
|
||||||
|
ipAddress: ctx.ip,
|
||||||
|
userAgent: ctx.userAgent,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { snapshotId: snapshot.id, startup: result.startup, concept: result.concept }
|
||||||
|
} catch (err) {
|
||||||
|
// Mark snapshot as FAILED so the indicator clears
|
||||||
|
await ctx.prisma.rankingSnapshot.update({
|
||||||
|
where: { id: snapshot.id },
|
||||||
|
data: { status: 'FAILED' },
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user