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

@@ -181,7 +181,7 @@ export async function processEligibilityJob(
}
})
// Upsert eligibilities
// Upsert eligibilities — preserve manual overrides and shortlist status
await prisma.$transaction(
eligibilities.map((e) =>
prisma.awardEligibility.upsert({
@@ -200,26 +200,54 @@ export async function processEligibilityJob(
aiReasoningJson: e.aiReasoningJson ?? undefined,
},
update: {
eligible: e.eligible,
method: e.method as 'AUTO' | 'MANUAL',
// Only update AI-computed fields; DO NOT reset overriddenBy,
// overriddenAt, shortlisted, confirmedAt, confirmedBy — those
// reflect admin decisions that must survive re-runs.
qualityScore: e.qualityScore,
aiReasoningJson: e.aiReasoningJson ?? undefined,
overriddenBy: null,
overriddenAt: null,
shortlisted: false,
confirmedAt: null,
confirmedBy: null,
},
})
)
)
// For records without manual override, sync the eligible/method fields
const nonOverridden = await prisma.awardEligibility.findMany({
where: { awardId, overriddenBy: null },
select: { projectId: true },
})
const nonOverriddenIds = new Set(nonOverridden.map((r) => r.projectId))
if (nonOverriddenIds.size > 0) {
await prisma.$transaction(
eligibilities
.filter((e) => nonOverriddenIds.has(e.projectId))
.map((e) =>
prisma.awardEligibility.update({
where: {
awardId_projectId: { awardId, projectId: e.projectId },
},
data: {
eligible: e.eligible,
method: e.method as 'AUTO' | 'MANUAL',
},
})
)
)
}
// Auto-shortlist top N eligible projects by qualityScore
// Only auto-shortlist records that aren't already manually shortlisted
const shortlistSize = award.shortlistSize ?? 10
const alreadyShortlisted = await prisma.awardEligibility.findMany({
where: { awardId, shortlisted: true, overriddenBy: { not: null } },
select: { projectId: true },
})
const manuallyShortlistedIds = new Set(alreadyShortlisted.map((r) => r.projectId))
const topEligible = eligibilities
.filter((e) => e.eligible && e.qualityScore != null)
.filter((e) => e.eligible && e.qualityScore != null && !manuallyShortlistedIds.has(e.projectId))
.sort((a, b) => (b.qualityScore ?? 0) - (a.qualityScore ?? 0))
.slice(0, shortlistSize)
.slice(0, Math.max(0, shortlistSize - manuallyShortlistedIds.size))
if (topEligible.length > 0) {
await prisma.$transaction(