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:
@@ -104,9 +104,10 @@ export default function MessagesPage() {
|
||||
{ enabled: recipientType === 'USER' }
|
||||
)
|
||||
|
||||
// Fetch sent messages for history
|
||||
const { data: sentMessages, isLoading: loadingSent } = trpc.message.inbox.useQuery(
|
||||
{ page: 1, pageSize: 50 }
|
||||
// Fetch sent messages for history (messages sent BY this admin)
|
||||
const { data: sentMessages, isLoading: loadingSent } = trpc.message.sent.useQuery(
|
||||
{ page: 1, pageSize: 50 },
|
||||
{ refetchInterval: 30_000 }
|
||||
)
|
||||
|
||||
const sendMutation = trpc.message.send.useMutation({
|
||||
@@ -114,7 +115,7 @@ export default function MessagesPage() {
|
||||
const count = (data as Record<string, unknown>)?.recipientCount || ''
|
||||
toast.success(`Message sent successfully${count ? ` to ${count} recipients` : ''}`)
|
||||
resetForm()
|
||||
utils.message.inbox.invalidate()
|
||||
utils.message.sent.invalidate()
|
||||
},
|
||||
onError: (e) => toast.error(e.message),
|
||||
})
|
||||
@@ -564,57 +565,70 @@ export default function MessagesPage() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Subject</TableHead>
|
||||
<TableHead className="hidden md:table-cell">From</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Channel</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Recipients</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Channels</TableHead>
|
||||
<TableHead className="hidden lg:table-cell">Status</TableHead>
|
||||
<TableHead className="text-right">Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sentMessages.items.map((item: Record<string, unknown>) => {
|
||||
const msg = item.message as Record<string, unknown> | undefined
|
||||
const sender = msg?.sender as Record<string, unknown> | undefined
|
||||
const channel = String(item.channel || 'EMAIL')
|
||||
const isRead = !!item.isRead
|
||||
{sentMessages.items.map((msg: any) => {
|
||||
const channels = (msg.deliveryChannels as string[]) || []
|
||||
const recipientCount = msg._count?.recipients ?? 0
|
||||
const isSent = !!msg.sentAt
|
||||
|
||||
return (
|
||||
<TableRow key={String(item.id)}>
|
||||
<TableRow key={msg.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isRead && (
|
||||
<div className="h-2 w-2 rounded-full bg-primary shrink-0" />
|
||||
)}
|
||||
<span className={isRead ? 'text-muted-foreground' : 'font-medium'}>
|
||||
{String(msg?.subject || 'No subject')}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-medium">
|
||||
{msg.subject || 'No subject'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell text-sm text-muted-foreground">
|
||||
{String(sender?.name || sender?.email || 'System')}
|
||||
{msg.recipientType === 'ALL'
|
||||
? 'All users'
|
||||
: msg.recipientType === 'ROLE'
|
||||
? `By role`
|
||||
: msg.recipientType === 'ROUND_JURY'
|
||||
? 'Round jury'
|
||||
: msg.recipientType === 'USER'
|
||||
? '1 user'
|
||||
: msg.recipientType}
|
||||
{recipientCount > 0 && ` (${recipientCount})`}
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{channel === 'EMAIL' ? (
|
||||
<><Mail className="mr-1 h-3 w-3" />Email</>
|
||||
) : (
|
||||
<><Bell className="mr-1 h-3 w-3" />In-App</>
|
||||
<div className="flex gap-1">
|
||||
{channels.includes('EMAIL') && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Mail className="mr-1 h-3 w-3" />Email
|
||||
</Badge>
|
||||
)}
|
||||
</Badge>
|
||||
{channels.includes('IN_APP') && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Bell className="mr-1 h-3 w-3" />In-App
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
{isRead ? (
|
||||
{isSent ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||
Read
|
||||
Sent
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="default" className="text-xs">New</Badge>
|
||||
<Badge variant="default" className="text-xs">
|
||||
<Clock className="mr-1 h-3 w-3" />
|
||||
Scheduled
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-sm text-muted-foreground">
|
||||
{msg?.createdAt
|
||||
? formatDate(msg.createdAt as string | Date)
|
||||
: ''}
|
||||
{msg.sentAt
|
||||
? formatDate(msg.sentAt)
|
||||
: msg.scheduledAt
|
||||
? formatDate(msg.scheduledAt)
|
||||
: ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user