Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s

Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit

Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete

Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers

Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub

Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology

Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build

40 files changed, 1010 insertions(+), 612 deletions(-)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 08:20:13 +01:00
parent aa1bf564ee
commit 1308c3ba87
40 changed files with 1011 additions and 613 deletions

View File

@@ -408,64 +408,76 @@ export const analyticsRouter = router({
getCrossRoundComparison: observerProcedure
.input(z.object({ roundIds: z.array(z.string()).min(2) }))
.query(async ({ ctx, input }) => {
const comparisons = await Promise.all(
input.roundIds.map(async (roundId) => {
const round = await ctx.prisma.round.findUniqueOrThrow({
where: { id: roundId },
select: { id: true, name: true },
})
const { roundIds } = input
const [projectCount, assignmentCount, evaluationCount] = await Promise.all([
ctx.prisma.project.count({
where: { assignments: { some: { roundId } } },
}),
ctx.prisma.assignment.count({ where: { roundId } }),
ctx.prisma.evaluation.count({
where: {
assignment: { roundId },
status: 'SUBMITTED',
},
}),
])
// Batch: fetch all rounds, assignments, and evaluations in 3 queries
const [rounds, assignments, evaluations] = await Promise.all([
ctx.prisma.round.findMany({
where: { id: { in: roundIds } },
select: { id: true, name: true },
}),
ctx.prisma.assignment.groupBy({
by: ['roundId'],
where: { roundId: { in: roundIds } },
_count: true,
}),
ctx.prisma.evaluation.findMany({
where: {
assignment: { roundId: { in: roundIds } },
status: 'SUBMITTED',
},
select: { globalScore: true, assignment: { select: { roundId: true } } },
}),
])
const completionRate = assignmentCount > 0
? Math.round((evaluationCount / assignmentCount) * 100)
: 0
const roundMap = new Map(rounds.map((r) => [r.id, r.name]))
const assignmentCountMap = new Map(assignments.map((a) => [a.roundId, a._count]))
const evaluations = await ctx.prisma.evaluation.findMany({
where: {
assignment: { roundId },
status: 'SUBMITTED',
},
select: { globalScore: true },
})
// Group evaluations by round
const evalsByRound = new Map<string, number[]>()
const projectsByRound = new Map<string, Set<string>>()
for (const e of evaluations) {
const rid = e.assignment.roundId
if (!evalsByRound.has(rid)) evalsByRound.set(rid, [])
if (e.globalScore !== null) evalsByRound.get(rid)!.push(e.globalScore)
}
const globalScores = evaluations
.map((e) => e.globalScore)
.filter((s): s is number => s !== null)
// Count distinct projects per round via assignments
const projectAssignments = await ctx.prisma.assignment.findMany({
where: { roundId: { in: roundIds } },
select: { roundId: true, projectId: true },
distinct: ['roundId', 'projectId'],
})
for (const pa of projectAssignments) {
if (!projectsByRound.has(pa.roundId)) projectsByRound.set(pa.roundId, new Set())
projectsByRound.get(pa.roundId)!.add(pa.projectId)
}
const averageScore = globalScores.length > 0
? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
: null
return roundIds.map((roundId) => {
const globalScores = evalsByRound.get(roundId) ?? []
const assignmentCount = assignmentCountMap.get(roundId) ?? 0
const evaluationCount = globalScores.length
const completionRate = assignmentCount > 0
? Math.round((evaluationCount / assignmentCount) * 100)
: 0
const averageScore = globalScores.length > 0
? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
: null
const distribution = Array.from({ length: 10 }, (_, i) => ({
score: i + 1,
count: globalScores.filter((s) => Math.round(s) === i + 1).length,
}))
const distribution = Array.from({ length: 10 }, (_, i) => ({
score: i + 1,
count: globalScores.filter((s) => Math.round(s) === i + 1).length,
}))
return {
roundId,
roundName: round.name,
projectCount,
evaluationCount,
completionRate,
averageScore,
scoreDistribution: distribution,
}
})
)
return comparisons
return {
roundId,
roundName: roundMap.get(roundId) ?? roundId,
projectCount: projectsByRound.get(roundId)?.size ?? 0,
evaluationCount,
completionRate,
averageScore,
scoreDistribution: distribution,
}
})
}),
/**
@@ -620,55 +632,72 @@ export const analyticsRouter = router({
})
const allRounds = competitions.flatMap((c) => c.rounds)
const roundIds = allRounds.map((r) => r.id)
const stats = await Promise.all(
allRounds.map(async (round) => {
const [projectCount, evaluationCount, assignmentCount] = await Promise.all([
ctx.prisma.project.count({
where: { assignments: { some: { roundId: round.id } } },
}),
ctx.prisma.evaluation.count({
where: {
assignment: { roundId: round.id },
status: 'SUBMITTED',
},
}),
ctx.prisma.assignment.count({ where: { roundId: round.id } }),
])
if (roundIds.length === 0) return []
const completionRate = assignmentCount > 0
? Math.round((evaluationCount / assignmentCount) * 100)
: 0
// Batch: fetch assignments, evaluations, and distinct projects in 3 queries
const [assignmentCounts, evaluations, projectAssignments] = await Promise.all([
ctx.prisma.assignment.groupBy({
by: ['roundId'],
where: { roundId: { in: roundIds } },
_count: true,
}),
ctx.prisma.evaluation.findMany({
where: {
assignment: { roundId: { in: roundIds } },
status: 'SUBMITTED',
},
select: { globalScore: true, assignment: { select: { roundId: true } } },
}),
ctx.prisma.assignment.findMany({
where: { roundId: { in: roundIds } },
select: { roundId: true, projectId: true },
distinct: ['roundId', 'projectId'],
}),
])
const evaluations = await ctx.prisma.evaluation.findMany({
where: {
assignment: { roundId: round.id },
status: 'SUBMITTED',
},
select: { globalScore: true },
})
const assignmentCountMap = new Map(assignmentCounts.map((a) => [a.roundId, a._count]))
const scores = evaluations
.map((e) => e.globalScore)
.filter((s): s is number => s !== null)
// Group evaluation scores by round
const scoresByRound = new Map<string, number[]>()
const evalCountByRound = new Map<string, number>()
for (const e of evaluations) {
const rid = e.assignment.roundId
evalCountByRound.set(rid, (evalCountByRound.get(rid) ?? 0) + 1)
if (e.globalScore !== null) {
if (!scoresByRound.has(rid)) scoresByRound.set(rid, [])
scoresByRound.get(rid)!.push(e.globalScore)
}
}
const averageScore = scores.length > 0
? scores.reduce((a, b) => a + b, 0) / scores.length
: null
// Count distinct projects per round
const projectsByRound = new Map<string, number>()
for (const pa of projectAssignments) {
projectsByRound.set(pa.roundId, (projectsByRound.get(pa.roundId) ?? 0) + 1)
}
return {
roundId: round.id,
roundName: round.name,
createdAt: round.createdAt,
projectCount,
evaluationCount,
completionRate,
averageScore,
}
})
)
return allRounds.map((round) => {
const scores = scoresByRound.get(round.id) ?? []
const assignmentCount = assignmentCountMap.get(round.id) ?? 0
const evaluationCount = evalCountByRound.get(round.id) ?? 0
const completionRate = assignmentCount > 0
? Math.round((evaluationCount / assignmentCount) * 100)
: 0
const averageScore = scores.length > 0
? scores.reduce((a, b) => a + b, 0) / scores.length
: null
return stats
return {
roundId: round.id,
roundName: round.name,
createdAt: round.createdAt,
projectCount: projectsByRound.get(round.id) ?? 0,
evaluationCount,
completionRate,
averageScore,
}
})
}),
/**