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:
@@ -14,6 +14,16 @@ function makeQueryClient() {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, error) => {
|
||||
// Retry up to 3 times on server errors (503 cold-start, etc.)
|
||||
if (failureCount >= 3) return false
|
||||
const msg = (error as Error)?.message ?? ''
|
||||
// Retry on JSON parse errors (HTML 503 from nginx) and server errors
|
||||
if (msg.includes('is not valid JSON') || msg.includes('Unexpected token')) return true
|
||||
if (msg.includes('500') || msg.includes('502') || msg.includes('503')) return true
|
||||
return failureCount < 2
|
||||
},
|
||||
retryDelay: (attemptIndex) => Math.min(2000 * (attemptIndex + 1), 8000),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -47,6 +57,21 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
transformer: superjson,
|
||||
async fetch(url, options) {
|
||||
const res = await globalThis.fetch(url, options)
|
||||
// Detect nginx 503 / HTML error pages before tRPC tries to JSON.parse
|
||||
if (!res.ok) {
|
||||
const ct = res.headers.get('content-type') ?? ''
|
||||
if (ct.includes('text/html') || !ct.includes('json')) {
|
||||
throw new Error(
|
||||
res.status >= 500
|
||||
? 'Server is starting up — please wait a moment and try again.'
|
||||
: `Server error (${res.status})`
|
||||
)
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user