Comprehensive platform audit: security, UX, performance, and visual polish

Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions

Phase 2: Admin UX - search/filter for awards, learning, partners pages

Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions

Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting

Phase 5: Portals - observer charts, mentor search, login/onboarding polish

Phase 6: Messages preview dialog, CsvExportDialog with column selection

Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook

Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 22:05:01 +01:00
parent e0e4cb2a32
commit e73a676412
33 changed files with 3193 additions and 977 deletions

View File

@@ -3,11 +3,7 @@ import { TRPCError } from '@trpc/server'
import { Prisma } from '@prisma/client'
import { router, protectedProcedure, adminProcedure } from '../trpc'
import { logAudit } from '../utils/audit'
import {
applyAutoTagRules,
aiInterpretCriteria,
type AutoTagRule,
} from '../services/ai-award-eligibility'
import { processEligibilityJob } from '../services/award-eligibility-job'
export const specialAwardRouter = router({
// ─── Admin Queries ──────────────────────────────────────────────────────
@@ -267,125 +263,53 @@ export const specialAwardRouter = router({
includeSubmitted: z.boolean().optional(),
}))
.mutation(async ({ ctx, input }) => {
const award = await ctx.prisma.specialAward.findUniqueOrThrow({
// Set job status to PENDING immediately
await ctx.prisma.specialAward.update({
where: { id: input.awardId },
include: { program: true },
})
// Get projects in the program's rounds
const statusFilter = input.includeSubmitted
? (['SUBMITTED', 'ELIGIBLE', 'ASSIGNED', 'SEMIFINALIST', 'FINALIST'] as const)
: (['ELIGIBLE', 'ASSIGNED', 'SEMIFINALIST', 'FINALIST'] as const)
const projects = await ctx.prisma.project.findMany({
where: {
round: { programId: award.programId },
status: { in: [...statusFilter] },
},
select: {
id: true,
title: true,
description: true,
competitionCategory: true,
country: true,
geographicZone: true,
tags: true,
oceanIssue: true,
data: {
eligibilityJobStatus: 'PENDING',
eligibilityJobTotal: null,
eligibilityJobDone: null,
eligibilityJobError: null,
eligibilityJobStarted: null,
},
})
if (projects.length === 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'No eligible projects found',
})
}
// Phase 1: Auto-tag rules (deterministic)
const autoTagRules = award.autoTagRulesJson as unknown as AutoTagRule[] | null
let autoResults: Map<string, boolean> | undefined
if (autoTagRules && Array.isArray(autoTagRules) && autoTagRules.length > 0) {
autoResults = applyAutoTagRules(autoTagRules, projects)
}
// Phase 2: AI interpretation (if criteria text exists AND AI eligibility is enabled)
let aiResults: Map<string, { eligible: boolean; confidence: number; reasoning: string }> | undefined
if (award.criteriaText && award.useAiEligibility) {
const aiEvals = await aiInterpretCriteria(award.criteriaText, projects)
aiResults = new Map(
aiEvals.map((e) => [
e.projectId,
{ eligible: e.eligible, confidence: e.confidence, reasoning: e.reasoning },
])
)
}
// Combine results: auto-tag AND AI must agree (or just one if only one configured)
const eligibilities = projects.map((project) => {
const autoEligible = autoResults?.get(project.id) ?? true
const aiEval = aiResults?.get(project.id)
const aiEligible = aiEval?.eligible ?? true
const eligible = autoEligible && aiEligible
const method = autoResults && aiResults ? 'AUTO' : autoResults ? 'AUTO' : 'MANUAL'
return {
projectId: project.id,
eligible,
method,
aiReasoningJson: aiEval
? { confidence: aiEval.confidence, reasoning: aiEval.reasoning }
: null,
}
})
// Upsert eligibilities
await ctx.prisma.$transaction(
eligibilities.map((e) =>
ctx.prisma.awardEligibility.upsert({
where: {
awardId_projectId: {
awardId: input.awardId,
projectId: e.projectId,
},
},
create: {
awardId: input.awardId,
projectId: e.projectId,
eligible: e.eligible,
method: e.method as 'AUTO' | 'MANUAL',
aiReasoningJson: e.aiReasoningJson ?? undefined,
},
update: {
eligible: e.eligible,
method: e.method as 'AUTO' | 'MANUAL',
aiReasoningJson: e.aiReasoningJson ?? undefined,
// Clear overrides
overriddenBy: null,
overriddenAt: null,
},
})
)
)
const eligibleCount = eligibilities.filter((e) => e.eligible).length
await logAudit({
userId: ctx.user.id,
action: 'UPDATE',
entityType: 'SpecialAward',
entityId: input.awardId,
detailsJson: {
action: 'RUN_ELIGIBILITY',
totalProjects: projects.length,
eligible: eligibleCount,
},
detailsJson: { action: 'RUN_ELIGIBILITY_STARTED' },
})
return {
total: projects.length,
eligible: eligibleCount,
ineligible: projects.length - eligibleCount,
}
// Fire and forget - process in background
void processEligibilityJob(
input.awardId,
input.includeSubmitted ?? false,
ctx.user.id
)
return { started: true }
}),
/**
* Get eligibility job status for polling
*/
getEligibilityJobStatus: protectedProcedure
.input(z.object({ awardId: z.string() }))
.query(async ({ ctx, input }) => {
const award = await ctx.prisma.specialAward.findUniqueOrThrow({
where: { id: input.awardId },
select: {
eligibilityJobStatus: true,
eligibilityJobTotal: true,
eligibilityJobDone: true,
eligibilityJobError: true,
eligibilityJobStarted: true,
},
})
return award
}),
/**