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
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:
@@ -696,29 +696,30 @@ export const userRouter = router({
|
||||
select: { id: true, email: true, name: true, role: true },
|
||||
})
|
||||
|
||||
// Create pre-assignments for users who have them
|
||||
let assignmentsCreated = 0
|
||||
// Create pre-assignments for users who have them (batched)
|
||||
const assignmentData: Array<{ userId: string; projectId: string; roundId: string; method: 'MANUAL'; createdBy: string }> = []
|
||||
for (const user of createdUsers) {
|
||||
const assignments = emailToAssignments.get(user.email.toLowerCase())
|
||||
if (assignments && assignments.length > 0) {
|
||||
for (const assignment of assignments) {
|
||||
try {
|
||||
await ctx.prisma.assignment.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
projectId: assignment.projectId,
|
||||
roundId: assignment.roundId,
|
||||
method: 'MANUAL',
|
||||
createdBy: ctx.user.id,
|
||||
},
|
||||
})
|
||||
assignmentsCreated++
|
||||
} catch {
|
||||
// Skip if assignment already exists (shouldn't happen for new users)
|
||||
}
|
||||
assignmentData.push({
|
||||
userId: user.id,
|
||||
projectId: assignment.projectId,
|
||||
roundId: assignment.roundId,
|
||||
method: 'MANUAL',
|
||||
createdBy: ctx.user.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let assignmentsCreated = 0
|
||||
if (assignmentData.length > 0) {
|
||||
const result = await ctx.prisma.assignment.createMany({
|
||||
data: assignmentData,
|
||||
skipDuplicates: true,
|
||||
})
|
||||
assignmentsCreated = result.count
|
||||
}
|
||||
|
||||
// Audit log for assignments if any were created
|
||||
if (assignmentsCreated > 0) {
|
||||
@@ -733,65 +734,104 @@ export const userRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
// Create JuryGroupMember records for users with juryGroupIds
|
||||
let juryGroupMembershipsCreated = 0
|
||||
let assignmentIntentsCreated = 0
|
||||
// Create JuryGroupMember records for users with juryGroupIds (batched)
|
||||
const juryGroupMemberData: Array<{ juryGroupId: string; userId: string; role: 'CHAIR' | 'MEMBER' | 'OBSERVER' }> = []
|
||||
for (const user of createdUsers) {
|
||||
const groupInfo = emailToJuryGroupIds.get(user.email.toLowerCase())
|
||||
if (groupInfo) {
|
||||
for (const groupId of groupInfo.ids) {
|
||||
try {
|
||||
await ctx.prisma.juryGroupMember.create({
|
||||
data: {
|
||||
juryGroupId: groupId,
|
||||
userId: user.id,
|
||||
role: groupInfo.role,
|
||||
},
|
||||
})
|
||||
juryGroupMembershipsCreated++
|
||||
} catch {
|
||||
// Skip if membership already exists
|
||||
}
|
||||
juryGroupMemberData.push({
|
||||
juryGroupId: groupId,
|
||||
userId: user.id,
|
||||
role: groupInfo.role,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let juryGroupMembershipsCreated = 0
|
||||
if (juryGroupMemberData.length > 0) {
|
||||
const result = await ctx.prisma.juryGroupMember.createMany({
|
||||
data: juryGroupMemberData,
|
||||
skipDuplicates: true,
|
||||
})
|
||||
juryGroupMembershipsCreated = result.count
|
||||
}
|
||||
|
||||
// Create AssignmentIntents for users who have them
|
||||
const intents = emailToIntents.get(user.email.toLowerCase())
|
||||
if (intents) {
|
||||
for (const intent of intents) {
|
||||
try {
|
||||
// Look up the round's juryGroupId to find the matching JuryGroupMember
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: intent.roundId },
|
||||
select: { juryGroupId: true },
|
||||
})
|
||||
if (round?.juryGroupId) {
|
||||
const member = await ctx.prisma.juryGroupMember.findUnique({
|
||||
where: {
|
||||
juryGroupId_userId: {
|
||||
juryGroupId: round.juryGroupId,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (member) {
|
||||
await ctx.prisma.assignmentIntent.create({
|
||||
data: {
|
||||
juryGroupMemberId: member.id,
|
||||
roundId: intent.roundId,
|
||||
projectId: intent.projectId,
|
||||
source: 'INVITE',
|
||||
status: 'INTENT_PENDING',
|
||||
},
|
||||
})
|
||||
assignmentIntentsCreated++
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip duplicate intents
|
||||
}
|
||||
// Create AssignmentIntents for users who have them
|
||||
let assignmentIntentsCreated = 0
|
||||
const allIntentUsers = createdUsers.filter(
|
||||
(u) => emailToIntents.has(u.email.toLowerCase())
|
||||
)
|
||||
if (allIntentUsers.length > 0) {
|
||||
// Batch-fetch all relevant rounds to avoid N+1 lookups
|
||||
const allIntentRoundIds = new Set<string>()
|
||||
for (const u of allIntentUsers) {
|
||||
for (const intent of emailToIntents.get(u.email.toLowerCase())!) {
|
||||
allIntentRoundIds.add(intent.roundId)
|
||||
}
|
||||
}
|
||||
const rounds = await ctx.prisma.round.findMany({
|
||||
where: { id: { in: [...allIntentRoundIds] } },
|
||||
select: { id: true, juryGroupId: true },
|
||||
})
|
||||
const roundJuryGroupMap = new Map(rounds.map((r) => [r.id, r.juryGroupId]))
|
||||
|
||||
// Batch-fetch all matching JuryGroupMembers
|
||||
const memberLookups = allIntentUsers.flatMap((u) => {
|
||||
const intents = emailToIntents.get(u.email.toLowerCase())!
|
||||
return intents
|
||||
.map((intent) => {
|
||||
const juryGroupId = roundJuryGroupMap.get(intent.roundId)
|
||||
return juryGroupId ? { juryGroupId, userId: u.id } : null
|
||||
})
|
||||
.filter((x): x is { juryGroupId: string; userId: string } => x !== null)
|
||||
})
|
||||
const members = memberLookups.length > 0
|
||||
? await ctx.prisma.juryGroupMember.findMany({
|
||||
where: {
|
||||
OR: memberLookups.map((l) => ({
|
||||
juryGroupId: l.juryGroupId,
|
||||
userId: l.userId,
|
||||
})),
|
||||
},
|
||||
select: { id: true, juryGroupId: true, userId: true },
|
||||
})
|
||||
: []
|
||||
const memberMap = new Map(
|
||||
members.map((m) => [`${m.juryGroupId}:${m.userId}`, m.id])
|
||||
)
|
||||
|
||||
// Batch-create all intents
|
||||
const intentData: Array<{
|
||||
juryGroupMemberId: string
|
||||
roundId: string
|
||||
projectId: string
|
||||
source: 'INVITE'
|
||||
status: 'INTENT_PENDING'
|
||||
}> = []
|
||||
for (const user of allIntentUsers) {
|
||||
const intents = emailToIntents.get(user.email.toLowerCase())!
|
||||
for (const intent of intents) {
|
||||
const juryGroupId = roundJuryGroupMap.get(intent.roundId)
|
||||
if (!juryGroupId) continue
|
||||
const memberId = memberMap.get(`${juryGroupId}:${user.id}`)
|
||||
if (!memberId) continue
|
||||
intentData.push({
|
||||
juryGroupMemberId: memberId,
|
||||
roundId: intent.roundId,
|
||||
projectId: intent.projectId,
|
||||
source: 'INVITE',
|
||||
status: 'INTENT_PENDING',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (intentData.length > 0) {
|
||||
const result = await ctx.prisma.assignmentIntent.createMany({
|
||||
data: intentData,
|
||||
skipDuplicates: true,
|
||||
})
|
||||
assignmentIntentsCreated = result.count
|
||||
}
|
||||
}
|
||||
|
||||
if (juryGroupMembershipsCreated > 0) {
|
||||
|
||||
Reference in New Issue
Block a user