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 <noreply@anthropic.com>
- Remove NeoUserProvider + NeoInstituteProvider from App.tsx startup chain
- Strip user_db_name/school_db_name from CCUser; add school_id (Phase B wires it to Supabase)
- Remove DatabaseNameService from AuthContext and UserContext
- Remove provisionUser() call from login path; API endpoint preserved for Phase B decision
- Simplify UserContext.resolveProfile: fast-path JWT metadata then background Supabase fetch
- Replace user.user_db_name reads in singlePlayerPage + snapshotService with null-safe guards
- Add useDeviceContext hook (desktop/tablet/phone/iwb, persists to localStorage)
App now loads to dashboard without any Neo4j dependency at startup.
Canvas opens to blank TLDraw state; Phase B rebuilds navigation on Supabase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before: UserContext waited for a Supabase profiles table query (~200ms)
before setting isInitialized=true, causing FullContextRoutes to show
a spinner on every page refresh even with a valid stored session.
After: UserContext immediately builds a profile from auth metadata +
localStorage (synchronous, zero network calls) and sets isInitialized=true
before the Supabase query. The Supabase profiles table is still queried
in the background and the profile is updated when it returns with
authoritative user_db_name/school_db_name data.
On refresh with a stored session: auth metadata is available
synchronously via INITIAL_SESSION, so the page renders with no visible
spinner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous implementation had two concurrent session recovery paths:
1. loadInitialSession() calling supabase.auth.getSession()
2. onAuthStateChange handling INITIAL_SESSION/SIGNED_IN
These raced unpredictably causing setLoading(false) to never fire in
certain interleavings, leaving the app permanently stuck on the spinner.
Fix: Remove loadInitialSession() entirely. Start loading=true. Rely
solely on onAuthStateChange — INITIAL_SESSION fires immediately with
the current session state, eliminating the race. One path, one
setLoading(false) call, no interleaving possible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
singlePlayerPage was using useUser().user which is always null (no setter
in UserContext). This caused the redirect effect to always fire, making
the workspace completely inaccessible. Fixed by destructuring profile as
user, which IS properly set from the Supabase profiles query.
Also added 8s timeout to NeoUserContext.switchContext call to match the
NeoInstituteContext timeout fix, preventing infinite spinner if the
navigation API call hangs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Races the SchoolNeoDBService.getSchoolNode() call against an 8-second
timeout. If Neo4j is slow or unavailable the workspace now loads within
seconds rather than waiting for the full axios 120s timeout. The context
degrades gracefully — workspace opens without institute data, error is
logged as a warning not an exception.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>