From 7243502873f269b0ef96d31e29379b5937a45d18 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 9 Dec 2025 11:52:52 -0600 Subject: [PATCH] fix(practice): make session plan page self-sufficient for data loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update useActiveSessionPlan to accept initialData from server props - Page now fetches its own data if cache is empty (no abstraction hole) - Three loading scenarios handled: 1. Cache populated (from ConfigureClient mutation): instant display 2. Cache miss: fetches from API with loading state 3. Direct page load: uses server props as initialData - Add loading view while fetching session plan 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/.claude/settings.local.json | 7 ++- .../[studentId]/StudentPracticeClient.tsx | 45 +++++++++++++++++-- apps/web/src/hooks/useSessionPlan.ts | 14 +++++- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/apps/web/.claude/settings.local.json b/apps/web/.claude/settings.local.json index d52af211..2b088fad 100644 --- a/apps/web/.claude/settings.local.json +++ b/apps/web/.claude/settings.local.json @@ -133,11 +133,14 @@ "Bash(.claude/CLAUDE.md)", "Bash(mcp__sqlite__describe_table:*)", "Bash(ls:*)", - "Bash(mcp__sqlite__list_tables:*)" + "Bash(mcp__sqlite__list_tables:*)", + "Bash(mcp__sqlite__read_query:*)" ], "deny": [], "ask": [] }, "enableAllProjectMcpServers": true, - "enabledMcpjsonServers": ["sqlite"] + "enabledMcpjsonServers": [ + "sqlite" + ] } diff --git a/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx b/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx index 5e5d2c31..a0928d27 100644 --- a/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx +++ b/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx @@ -27,6 +27,7 @@ import type { PracticeSession } from '@/db/schema/practice-sessions' import type { SessionPlan, SlotResult } from '@/db/schema/session-plans' import { useAbandonSession, + useActiveSessionPlan, useApproveSessionPlan, useEndSessionEarly, useGenerateSessionPlan, @@ -122,13 +123,23 @@ export function StudentPracticeClient({ const endEarly = useEndSessionEarly() const abandonSession = useAbandonSession() - // Current plan from mutations or initial active session (use the latest successful result) + // Fetch active session plan from cache or API + // - If cache has data (from ConfigureClient mutation): uses cache immediately + // - If no cache but initialActiveSession exists: uses server props as initial data + // - If neither: fetches from API (shows loading state briefly) + const { + data: fetchedPlan, + isLoading: isPlanLoading, + } = useActiveSessionPlan(studentId, initialActiveSession) + + // Current plan from mutations or fetched data (priority order) + // Mutations take priority (most recent user action), then fetched/cached data const currentPlan = recordResult.data ?? startPlan.data ?? approvePlan.data ?? generatePlan.data ?? - initialActiveSession ?? + fetchedPlan ?? null // Derive error state from mutations @@ -144,13 +155,16 @@ export function StudentPracticeClient({ // Derive view from session plan state - NO useState! // This eliminates the "bastard state" problem where viewState and currentPlan could diverge - const sessionView: SessionView = useMemo(() => { + const sessionView: SessionView | 'loading' = useMemo(() => { + // Show loading only if we're fetching AND don't have any data yet + // (mutations or initial data would give us something to show) + if (isPlanLoading && !currentPlan) return 'loading' if (!currentPlan) return 'dashboard' if (currentPlan.completedAt) return 'summary' if (currentPlan.startedAt) return 'practicing' if (currentPlan.approvedAt) return 'reviewing' return 'continue' // Plan exists but not yet approved - }, [currentPlan]) + }, [currentPlan, isPlanLoading]) // Handle continue practice - navigate to configuration page const handleContinuePractice = useCallback(() => { @@ -437,6 +451,29 @@ export function StudentPracticeClient({ )} {/* Content based on session view (derived from data) */} + {sessionView === 'loading' && ( +
+
+ Loading practice session... +
+
+ )} + {sessionView === 'continue' && currentPlan && ( fetchActiveSessionPlan(playerId!), enabled: !!playerId, + // Use server-provided data as initial cache value + // This prevents a loading flash on direct page loads while still allowing refetch + initialData: initialData ?? undefined, + // Don't refetch on mount if we have initial data - trust the server + // The query will still refetch on window focus or after stale time + staleTime: initialData ? 30000 : 0, // 30s stale time if we have initial data }) }