From 6bd85671d27b4b4b78714b70b49cb0a29351c03d Mon Sep 17 00:00:00 2001 From: kcar Date: Mon, 25 May 2026 15:06:51 +0100 Subject: [PATCH] =?UTF-8?q?fix(panels):=20bypass=20GoTrueClient=20lock=20?= =?UTF-8?q?=E2=80=94=20expose=20accessToken=20via=20AuthContext,=20gate=20?= =?UTF-8?q?panels=20on=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: apiFetch called supabase.auth.getSession() on every API request, acquiring the GoTrueClient internal lock. On refresh, concurrent mount of panels caused lock contention — the loadCabinets/loadSessions fetch awaits hung, loading spinner never cleared. - AuthContext: add accessToken state, set/clear it alongside user on all auth events - CCFilesPanel: apiFetch reads accessToken from AuthContext (no lock), loadCabinets effect gated on authUser.id (fires only after SIGNED_IN, session guaranteed valid) - CCTranscriptionPanel: loadSessions effect gated on authUser.id for same reason Co-Authored-By: Claude Sonnet 4.6 --- src/contexts/AuthContext.tsx | 9 +++++++++ .../components/shared/CCFilesPanel.tsx | 16 ++++++++++------ .../components/shared/CCTranscriptionPanel.tsx | 12 ++++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 3fce4bd..9886446 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -9,6 +9,7 @@ import { storageService, StorageKeys } from '../services/auth/localStorageServic export interface AuthContextType { user: CCUser | null; user_role: string | null; + accessToken: string | null; loading: boolean; error: Error | null; signIn: (email: string, password: string) => Promise; @@ -19,6 +20,7 @@ export interface AuthContextType { export const AuthContext = createContext({ user: null, user_role: null, + accessToken: null, loading: true, error: null, signIn: async () => {}, @@ -30,6 +32,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const [user, setUser] = useState(null); const [user_role, setUserRole] = useState(null); + const [accessToken, setAccessToken] = useState(null); const [loading, setLoading] = useState(true); // true until INITIAL_SESSION fires const [error, setError] = useState(null); @@ -84,15 +87,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const { user: resolvedUser, role } = await buildUserFromSupabase(session.user); setUser(resolvedUser); setUserRole(role); + setAccessToken(session.access_token ?? null); } catch (buildError) { logger.error('auth-context', '❌ Failed to build user from session', { event, error: buildError }); setUser(null); setUserRole(null); + setAccessToken(null); setError(buildError instanceof Error ? buildError : new Error('Failed to load user')); } } else { setUser(null); setUserRole(null); + setAccessToken(null); } // Always clear loading after the first auth event resolves setLoading(false); @@ -102,6 +108,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { persistSession(null); setUser(null); setUserRole(null); + setAccessToken(null); setLoading(false); break; } @@ -132,6 +139,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const { user: resolvedUser, role } = await buildUserFromSupabase(data.user); setUser(resolvedUser); setUserRole(role); + setAccessToken(data.session?.access_token ?? null); } } catch (error) { logger.error('auth-context', '❌ Sign in failed', { error }); @@ -165,6 +173,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { value={{ user, user_role, + accessToken, loading, error, signIn, diff --git a/src/utils/tldraw/ui-overrides/components/shared/CCFilesPanel.tsx b/src/utils/tldraw/ui-overrides/components/shared/CCFilesPanel.tsx index eb5f802..fb6b830 100644 --- a/src/utils/tldraw/ui-overrides/components/shared/CCFilesPanel.tsx +++ b/src/utils/tldraw/ui-overrides/components/shared/CCFilesPanel.tsx @@ -40,7 +40,7 @@ import ImageIcon from '@mui/icons-material/Image'; import DescriptionIcon from '@mui/icons-material/Description'; import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import { useTLDraw } from '../../../../../contexts/TLDrawContext'; -import { supabase } from '../../../../../supabaseClient'; +import { useAuth } from '../../../../../contexts/AuthContext'; import { useNavigate } from 'react-router-dom'; import { calculateDirectoryStats, @@ -92,7 +92,8 @@ interface FileListResponse { } export const CCFilesPanel: React.FC = () => { - const { tldrawPreferences, authToken } = useTLDraw() as { tldrawPreferences?: { colorScheme?: 'light' | 'dark' | 'system' }, authToken?: string }; + const { tldrawPreferences } = useTLDraw() as { tldrawPreferences?: { colorScheme?: 'light' | 'dark' | 'system' } }; + const { user: authUser, accessToken } = useAuth(); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const [cabinets, setCabinets] = useState([]); const [selectedCabinet, setSelectedCabinet] = useState(''); @@ -144,14 +145,14 @@ export const CCFilesPanel: React.FC = () => { const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => { const headers: HeadersInitLike = { - 'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token || authToken || ''}`, + 'Authorization': `Bearer ${accessToken || ''}`, ...(init?.headers || {}) }; const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`; const res = await fetch(fullUrl, { ...(init || {}), headers }); if (!res.ok) throw new Error(await res.text()); return res.json(); - }, [authToken, API_BASE]); + }, [accessToken, API_BASE]); const loadCabinets = useCallback(async () => { setLoading(true); @@ -207,8 +208,11 @@ export const CCFilesPanel: React.FC = () => { }, [currentPage, itemsPerPage, sortBy, sortOrder, searchTerm, apiFetch, currentDirectoryId]); useEffect(() => { - loadCabinets(); - }, [loadCabinets]); + if (authUser?.id) { + initialSelectionDone.current = false; + loadCabinets(); + } + }, [loadCabinets, authUser?.id]); // Main loading effect - handles pagination, sorting, cabinet changes, directory navigation useEffect(() => { diff --git a/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx b/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx index b2922b7..7ed1215 100644 --- a/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx +++ b/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { useAuth } from "../../../../../contexts/AuthContext"; import { Mic as MicIcon, Stop as StopIcon, History as HistoryIcon, Settings as SettingsIcon, AutoAwesome as AutoAwesomeIcon, NotificationsActive as KeywordIcon, Close } from "@mui/icons-material"; import { useTranscriptionStore, TranscriptionSegment, TranscriptionSession, TimetablePeriod, KeywordWatch, KeywordMatch, ServerSegment } from "../../../../../stores/transcriptionStore"; import { TranscriptionService } from "../../../cc-base/cc-transcription/transcriptionService"; @@ -92,6 +93,7 @@ export const CCTranscriptionPanel: React.FC = () => { // Modal state const [showSettingsModal, setShowSettingsModal] = useState(false); + const { user: authUser } = useAuth(); const [showSummaryModal, setShowSummaryModal] = useState(false); const [summaryType, setSummaryType] = useState('full_lesson'); @@ -99,11 +101,13 @@ export const CCTranscriptionPanel: React.FC = () => { const [newKeyword, setNewKeyword] = useState(''); const [isAddingKeyword, setIsAddingKeyword] = useState(false); - // Load sessions and keyword watches on mount + // Load sessions when auth is confirmed (avoids GoTrueClient lock on mount) useEffect(() => { - loadSessions().then(setSessions); - loadKeywordWatches(); - }, []); + if (authUser?.id) { + loadSessions().then(setSessions); + loadKeywordWatches(); + } + }, [authUser?.id]); // Auto-detect timetable context on mount useEffect(() => {