Skip to content

Commit 75398cc

Browse files
committed
Fix team page loading issues and Next.js config warnings
- Add timeout handling and error display to team members/invitations - Improve proxy error handling with 15s timeout and better URL format handling - Fix useState -> useEffect bug in team page - Remove deprecated instrumentationHook from Next.js config - Add outputFileTracingRoot to match turbopack.root
1 parent 5e7204c commit 75398cc

4 files changed

Lines changed: 141 additions & 29 deletions

File tree

frontend/app/dashboard/team/page.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useState, useEffect } from 'react'
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
55
import { Button } from '@/components/ui/button'
66
import { Input } from '@/components/ui/input'
@@ -58,8 +58,8 @@ type InviteFormValues = z.infer<typeof inviteSchema>
5858

5959
export default function TeamPage() {
6060
const router = useRouter()
61-
const { data: members, isLoading: membersLoading } = useTeamMembers()
62-
const { data: invitations, isLoading: invitationsLoading } = useInvitations()
61+
const { data: members, isLoading: membersLoading, error: membersError } = useTeamMembers()
62+
const { data: invitations, isLoading: invitationsLoading, error: invitationsError } = useInvitations()
6363
const inviteUser = useInviteUser()
6464
const deleteInvitation = useDeleteInvitation()
6565
const updateMemberRole = useUpdateMemberRole()
@@ -146,7 +146,7 @@ export default function TeamPage() {
146146
// Get current user to check permissions
147147
const [currentUser, setCurrentUser] = useState<{ id: string; role: string } | null>(null)
148148

149-
useState(() => {
149+
useEffect(() => {
150150
const supabase = createClient()
151151
supabase.auth.getUser().then(({ data: { user } }) => {
152152
if (user) {
@@ -160,7 +160,7 @@ export default function TeamPage() {
160160
})
161161
}
162162
})
163-
})
163+
}, [])
164164

165165
const canManageTeam = currentUser?.role === 'owner' || currentUser?.role === 'admin'
166166

@@ -190,8 +190,21 @@ export default function TeamPage() {
190190
</CardDescription>
191191
</CardHeader>
192192
<CardContent>
193-
{membersLoading ? (
194-
<div className="text-center py-8 text-muted-foreground">Loading...</div>
193+
{membersError ? (
194+
<div className="text-center py-8">
195+
<p className="text-destructive mb-2">Failed to load team members</p>
196+
<p className="text-sm text-muted-foreground">{membersError.message}</p>
197+
<Button
198+
variant="outline"
199+
size="sm"
200+
className="mt-4"
201+
onClick={() => window.location.reload()}
202+
>
203+
Retry
204+
</Button>
205+
</div>
206+
) : membersLoading ? (
207+
<div className="text-center py-8 text-muted-foreground">Loading team members...</div>
195208
) : members && members.length > 0 ? (
196209
<Table>
197210
<TableHeader>
@@ -266,8 +279,13 @@ export default function TeamPage() {
266279
</CardDescription>
267280
</CardHeader>
268281
<CardContent>
269-
{invitationsLoading ? (
270-
<div className="text-center py-8 text-muted-foreground">Loading...</div>
282+
{invitationsError ? (
283+
<div className="text-center py-8">
284+
<p className="text-destructive mb-2">Failed to load invitations</p>
285+
<p className="text-sm text-muted-foreground">{invitationsError.message}</p>
286+
</div>
287+
) : invitationsLoading ? (
288+
<div className="text-center py-8 text-muted-foreground">Loading invitations...</div>
271289
) : invitations && invitations.length > 0 ? (
272290
<Table>
273291
<TableHeader>

frontend/lib/api/proxy.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,21 @@ export async function proxyRequest(
4646
const method = options.method || request.method
4747
const url = new URL(request.url)
4848
const queryString = url.searchParams.toString()
49-
const targetUrl = `${options.serviceUrl}${options.path}${queryString ? `?${queryString}` : ''}`
49+
50+
// Ensure serviceUrl doesn't have trailing slash and path starts with /
51+
// Handle URLs with or without protocol
52+
let serviceUrl = options.serviceUrl.replace(/\/$/, '')
53+
if (!serviceUrl.startsWith('http://') && !serviceUrl.startsWith('https://')) {
54+
serviceUrl = `https://${serviceUrl}`
55+
}
56+
const path = options.path.startsWith('/') ? options.path : `/${options.path}`
57+
const targetUrl = `${serviceUrl}${path}${queryString ? `?${queryString}` : ''}`
58+
59+
logger.debug('Proxying request', {
60+
targetUrl,
61+
method,
62+
path: options.path,
63+
})
5064

5165
const headers: HeadersInit = {
5266
Authorization: `Bearer ${ctx.session.access_token}`,
@@ -72,11 +86,33 @@ export async function proxyRequest(
7286
}
7387
}
7488

75-
const response = await fetch(targetUrl, {
76-
method,
77-
headers,
78-
body,
79-
})
89+
const controller = new AbortController()
90+
const timeoutId = setTimeout(() => controller.abort(), 15000) // 15 second timeout
91+
92+
let response: Response
93+
try {
94+
response = await fetch(targetUrl, {
95+
method,
96+
headers,
97+
body,
98+
signal: controller.signal,
99+
})
100+
clearTimeout(timeoutId)
101+
} catch (error: any) {
102+
clearTimeout(timeoutId)
103+
if (error.name === 'AbortError') {
104+
logger.error('Request timeout to backend service', {
105+
serviceUrl: options.serviceUrl,
106+
path: options.path,
107+
method,
108+
})
109+
return NextResponse.json(
110+
{ error: 'Request timeout', details: 'The service is taking too long to respond' },
111+
{ status: 504 }
112+
)
113+
}
114+
throw error
115+
}
80116

81117
// Check content type before parsing
82118
const contentType = response.headers.get('content-type')
@@ -108,6 +144,13 @@ export async function proxyRequest(
108144
const nested = (data as Record<string, unknown>)[options.extractNestedData]
109145
if (nested !== undefined) {
110146
data = nested
147+
} else {
148+
// If nested data not found, log warning but return original data
149+
logger.warn('Nested data not found in response', {
150+
extractNestedData: options.extractNestedData,
151+
responseKeys: Object.keys(data as Record<string, unknown>),
152+
path: options.path,
153+
})
111154
}
112155
}
113156

frontend/lib/api/team.ts

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,65 @@ export interface UpdateMemberRoleInput {
3838

3939
// API functions
4040
async function fetchTeamMembers(): Promise<TeamMember[]> {
41-
const response = await fetch('/api/team/members')
42-
if (!response.ok) {
43-
const error = await response.json().catch(() => ({ error: 'Failed to fetch team members' }))
44-
throw new Error(error.error || 'Failed to fetch team members')
41+
try {
42+
const controller = new AbortController()
43+
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout
44+
45+
const response = await fetch('/api/team/members', {
46+
signal: controller.signal,
47+
})
48+
49+
clearTimeout(timeoutId)
50+
51+
if (!response.ok) {
52+
const error = await response.json().catch(() => ({ error: 'Failed to fetch team members' }))
53+
throw new Error(error.error || `Failed to fetch team members: ${response.status}`)
54+
}
55+
56+
const data = await response.json()
57+
// Response is already extracted by proxy (extractNestedData: 'members')
58+
// So data is either the array directly or { members: [...] }
59+
if (Array.isArray(data)) {
60+
return data
61+
}
62+
return data.members || []
63+
} catch (error: any) {
64+
if (error.name === 'AbortError') {
65+
throw new Error('Request timeout: Team service is not responding')
66+
}
67+
throw error
4568
}
46-
const data = await response.json()
47-
return data.members || []
4869
}
4970

5071
async function fetchInvitations(): Promise<TeamInvitation[]> {
51-
const response = await fetch('/api/team/invitations')
52-
if (!response.ok) {
53-
const error = await response.json().catch(() => ({ error: 'Failed to fetch invitations' }))
54-
throw new Error(error.error || 'Failed to fetch invitations')
72+
try {
73+
const controller = new AbortController()
74+
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout
75+
76+
const response = await fetch('/api/team/invitations', {
77+
signal: controller.signal,
78+
})
79+
80+
clearTimeout(timeoutId)
81+
82+
if (!response.ok) {
83+
const error = await response.json().catch(() => ({ error: 'Failed to fetch invitations' }))
84+
throw new Error(error.error || `Failed to fetch invitations: ${response.status}`)
85+
}
86+
87+
const data = await response.json()
88+
// Response is already extracted by proxy (extractNestedData: 'invitations')
89+
// So data is either the array directly or { invitations: [...] }
90+
if (Array.isArray(data)) {
91+
return data
92+
}
93+
return data.invitations || []
94+
} catch (error: any) {
95+
if (error.name === 'AbortError') {
96+
throw new Error('Request timeout: Team service is not responding')
97+
}
98+
throw error
5599
}
56-
const data = await response.json()
57-
return data.invitations || []
58100
}
59101

60102
async function inviteUser(input: InviteUserInput): Promise<{ invitation: TeamInvitation; token?: string }> {
@@ -120,13 +162,21 @@ export function useTeamMembers() {
120162
return useQuery({
121163
queryKey: ['team', 'members'],
122164
queryFn: fetchTeamMembers,
165+
retry: 1,
166+
retryDelay: 2000,
167+
staleTime: 30000, // 30 seconds
168+
gcTime: 5 * 60 * 1000, // 5 minutes
123169
})
124170
}
125171

126172
export function useInvitations() {
127173
return useQuery({
128174
queryKey: ['team', 'invitations'],
129175
queryFn: fetchInvitations,
176+
retry: 1,
177+
retryDelay: 2000,
178+
staleTime: 30000, // 30 seconds
179+
gcTime: 5 * 60 * 1000, // 5 minutes
130180
})
131181
}
132182

frontend/next.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ const nextConfig: NextConfig = {
4141
// Ensure CSS is properly compiled with Tailwind v4
4242
experimental: {
4343
optimizeCss: true,
44-
// Enable instrumentation hook for Sentry
45-
instrumentationHook: true,
4644
},
45+
// Set output file tracing root to match turbopack root
46+
// This ensures consistent behavior between webpack and turbopack
47+
outputFileTracingRoot: frontendRoot,
4748
// Transpile shared package for Next.js
4849
transpilePackages: ['@syntera/shared'],
4950
// Image optimization configuration

0 commit comments

Comments
 (0)