2026-02-14 15:26:42 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react'
|
|
|
|
|
import { SessionProvider } from 'next-auth/react'
|
|
|
|
|
import { ThemeProvider } from 'next-themes'
|
|
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
|
import { httpBatchLink } from '@trpc/client'
|
|
|
|
|
import superjson from 'superjson'
|
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
|
|
|
|
|
|
|
|
function makeQueryClient() {
|
|
|
|
|
return new QueryClient({
|
|
|
|
|
defaultOptions: {
|
|
|
|
|
queries: {
|
|
|
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
|
|
|
refetchOnWindowFocus: false,
|
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>
2026-03-04 17:55:44 +01:00
|
|
|
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),
|
2026-02-14 15:26:42 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let browserQueryClient: QueryClient | undefined = undefined
|
|
|
|
|
|
|
|
|
|
function getQueryClient() {
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
// Server: always make a new query client
|
|
|
|
|
return makeQueryClient()
|
|
|
|
|
} else {
|
|
|
|
|
// Browser: make a new query client if we don't already have one
|
|
|
|
|
if (!browserQueryClient) browserQueryClient = makeQueryClient()
|
|
|
|
|
return browserQueryClient
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getBaseUrl() {
|
|
|
|
|
if (typeof window !== 'undefined') return ''
|
|
|
|
|
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
|
|
|
|
|
return `http://localhost:${process.env.PORT ?? 3000}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
|
|
const queryClient = getQueryClient()
|
|
|
|
|
|
|
|
|
|
const [trpcClient] = useState(() =>
|
|
|
|
|
trpc.createClient({
|
|
|
|
|
links: [
|
|
|
|
|
httpBatchLink({
|
|
|
|
|
url: `${getBaseUrl()}/api/trpc`,
|
|
|
|
|
transformer: superjson,
|
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>
2026-03-04 17:55:44 +01:00
|
|
|
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
|
|
|
|
|
},
|
2026-02-14 15:26:42 +01:00
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
|
|
|
|
|
<SessionProvider>
|
|
|
|
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
|
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
|
|
|
</trpc.Provider>
|
|
|
|
|
</SessionProvider>
|
|
|
|
|
</ThemeProvider>
|
|
|
|
|
)
|
|
|
|
|
}
|