feat: impersonation system, semi-finalist detail page, tRPC resilience

- Add super-admin impersonation: "Login As" from user list, red banner
  with "Return to Admin", audit logged start/end, nested impersonation
  blocked, onboarding gate skipped during impersonation
- Fix semi-finalist stats: check latest terminal state (not any PASSED),
  use passwordHash OR status=ACTIVE for activation check
- Add /admin/semi-finalists detail page with search, category/status filters
- Add account_reminder_days setting to notifications tab
- Add tRPC resilience: retry on 503/HTML responses, custom fetch detects
  nginx error pages, exponential backoff (2s/4s/8s)
- Reduce dashboard polling intervals (60s stats, 30s activity, 120s semi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 17:55:44 +01:00
parent b1a994a9d6
commit 6c52e519e5
18 changed files with 814 additions and 74 deletions

View File

@@ -5,6 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import Link from 'next/link'
import type { Route } from 'next'
import {
Users,
Send,
@@ -12,6 +14,7 @@ import {
AlertCircle,
Trophy,
Loader2,
ExternalLink,
} from 'lucide-react'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
@@ -44,6 +47,7 @@ type SemiFinalistTrackerProps = {
byAward: AwardStat[]
unactivatedProjects: UnactivatedProject[]
editionId: string
reminderThresholdDays?: number
}
const categoryLabels: Record<string, string> = {
@@ -57,6 +61,7 @@ export function SemiFinalistTracker({
byAward,
unactivatedProjects,
editionId,
reminderThresholdDays = 3,
}: SemiFinalistTrackerProps) {
const utils = trpc.useUtils()
const sendReminders = trpc.dashboard.sendAccountReminders.useMutation({
@@ -97,9 +102,16 @@ export function SemiFinalistTracker({
<Users className="h-4 w-4 text-brand-blue" />
Semi-Finalist Tracker
</CardTitle>
<Badge variant="outline" className="text-xs">
{totalActivated}/{totalProjects} activated
</Badge>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{totalActivated}/{totalProjects} activated
</Badge>
<Link href={`/admin/semi-finalists?editionId=${editionId}` as Route}>
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs">
See All <ExternalLink className="ml-1 h-3 w-3" />
</Button>
</Link>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">