From 5a4de51db46e1f9331a3722f42f6ad103642b6c1 Mon Sep 17 00:00:00 2001 From: Ronald Roy Date: Tue, 20 Jan 2026 14:04:26 -0800 Subject: [PATCH] fix: Restore meeting state after page refresh (#377) Move meeting state restoration from NoteCapture to MainContent so it runs regardless of which tab is active. Previously, refreshing while on any tab except Capture would orphan an active meeting because get_meeting_state was never sent. Changes: - App.tsx: Send select_vault and get_meeting_state on mount and reconnect - NoteCapture.tsx: Remove redundant vault/meeting restoration logic Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.tsx | 45 +++++++++++++++++- frontend/src/components/NoteCapture.tsx | 63 +++---------------------- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d581260e..f27d2071 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ * - Session management */ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import { SessionProvider, useSession, useServerMessageHandler } from "./contexts/SessionContext"; import { VaultSelect } from "./components/VaultSelect"; import { ModeToggle } from "./components/ModeToggle"; @@ -35,7 +35,6 @@ type DialogType = "changeVault" | null; */ function MainContent(): React.ReactNode { const { mode, vault, clearVault } = useSession(); - const { sendMessage, lastMessage } = useWebSocket(); const handleServerMessage = useServerMessageHandler(); const [activeDialog, setActiveDialog] = useState(null); const [configEditorOpen, setConfigEditorOpen] = useState(false); @@ -50,6 +49,21 @@ function MainContent(): React.ReactNode { const holiday = useHoliday(); // Mobile header collapse state const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(true); + // Session restoration tracking (for page refresh and WebSocket reconnection) + const hasRequestedMeetingStateRef = useRef(false); + const hasSentVaultSelectionRef = useRef(false); + + // Re-establish vault context and meeting state on WebSocket reconnection. + // After reconnect, the server has a fresh connection state and needs to know + // which vault we're using before we can query meeting state. + const handleReconnect = useCallback(() => { + hasSentVaultSelectionRef.current = false; + hasRequestedMeetingStateRef.current = false; + }, []); + + const { sendMessage, lastMessage, connectionStatus } = useWebSocket({ + onReconnect: handleReconnect, + }); // Process server messages through the session handler useEffect(() => { @@ -58,6 +72,33 @@ function MainContent(): React.ReactNode { } }, [lastMessage, handleServerMessage]); + // Re-send vault selection on WebSocket reconnect (server needs vault context) + useEffect(() => { + if ( + connectionStatus === "connected" && + vault && + !hasSentVaultSelectionRef.current + ) { + sendMessage({ type: "select_vault", vaultId: vault.id }); + hasSentVaultSelectionRef.current = true; + } + }, [connectionStatus, vault, sendMessage]); + + // Request meeting state after vault selection to restore any active meeting. + // This runs on initial mount (page refresh) and after WebSocket reconnection. + // Fixes #377 where refreshing with an active meeting orphaned it. + useEffect(() => { + if ( + connectionStatus === "connected" && + vault && + hasSentVaultSelectionRef.current && + !hasRequestedMeetingStateRef.current + ) { + sendMessage({ type: "get_meeting_state" }); + hasRequestedMeetingStateRef.current = true; + } + }, [connectionStatus, vault, sendMessage]); + // Use holiday-specific logo if available const logoSrc = holiday ? `/images/holiday/${holiday}-logo.webp` : "/images/logo.webp"; diff --git a/frontend/src/components/NoteCapture.tsx b/frontend/src/components/NoteCapture.tsx index 8cc5c2e5..a5284cfe 100644 --- a/frontend/src/components/NoteCapture.tsx +++ b/frontend/src/components/NoteCapture.tsx @@ -9,7 +9,7 @@ * - Meeting: Captures go to meeting-specific file */ -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useWebSocket } from "../hooks/useWebSocket"; import { useSession } from "../contexts/SessionContext"; import "./NoteCapture.css"; @@ -60,55 +60,10 @@ export function NoteCapture({ onCaptured }: NoteCaptureProps): React.ReactNode { const meetingTitleRef = useRef(null); const retryCountRef = useRef(0); const retryTimeoutRef = useRef | null>(null); - const hasSentVaultSelectionRef = useRef(false); - const hasRequestedMeetingStateRef = useRef(false); - const { vault, meeting, setMeetingState, clearMeeting, setDiscussionPrefill, setMode } = useSession(); + const { vault, meeting, clearMeeting, setDiscussionPrefill, setMode } = useSession(); - // Callback to re-send vault selection on WebSocket reconnect - const handleReconnect = useCallback(() => { - hasSentVaultSelectionRef.current = false; - hasRequestedMeetingStateRef.current = false; - }, []); - - const { sendMessage, lastMessage, connectionStatus } = useWebSocket({ - onReconnect: handleReconnect, - }); - - // Send vault selection when WebSocket connects (initial or reconnect) - useEffect(() => { - if ( - connectionStatus === "connected" && - vault && - !hasSentVaultSelectionRef.current - ) { - sendMessage({ - type: "select_vault", - vaultId: vault.id, - }); - hasSentVaultSelectionRef.current = true; - } - }, [connectionStatus, vault, sendMessage]); - - // Request meeting state after vault selection (to restore state after reconnect) - useEffect(() => { - if ( - connectionStatus === "connected" && - vault && - hasSentVaultSelectionRef.current && - !hasRequestedMeetingStateRef.current - ) { - sendMessage({ type: "get_meeting_state" }); - hasRequestedMeetingStateRef.current = true; - } - }, [connectionStatus, vault, sendMessage]); - - // Handle meeting_state response (for restoring state after reconnect) - useEffect(() => { - if (lastMessage?.type === "meeting_state") { - setMeetingState(lastMessage.state); - } - }, [lastMessage, setMeetingState]); + const { sendMessage, lastMessage, connectionStatus } = useWebSocket(); // Detect touch-only devices (no hover capability) // On touch devices, Enter adds newlines; send button is the only way to submit @@ -170,25 +125,19 @@ export function NoteCapture({ onCaptured }: NoteCaptureProps): React.ReactNode { } }, [lastMessage, isSubmitting]); - // Handle meeting_started response + // Handle meeting_started response (local UI updates only; session context + // is updated by useServerMessageHandler in MainContent) useEffect(() => { if (lastMessage?.type === "meeting_started" && isStartingMeeting) { setIsStartingMeeting(false); setShowMeetingPrompt(false); setMeetingTitle(""); - // Update session context with meeting state - setMeetingState({ - isActive: true, - title: lastMessage.title, - filePath: lastMessage.filePath, - startedAt: lastMessage.startedAt, - }); showToast("success", `Meeting started: ${lastMessage.title}`); requestAnimationFrame(() => { textareaRef.current?.focus(); }); } - }, [lastMessage, isStartingMeeting, setMeetingState]); + }, [lastMessage, isStartingMeeting]); // Handle meeting_stopped response useEffect(() => {