@@ -187,6 +187,12 @@ const TRANSCRIPTS_KEY = 'transcripts';
187187const getSettingsDb = ( ) => openIdb ( SETTINGS_DB_NAME , SETTINGS_STORE_NAME ) ;
188188const getTranscriptsDb = ( ) => openIdb ( TRANSCRIPTS_DB_NAME , TRANSCRIPTS_STORE_NAME ) ;
189189
190+ // Watchdog cap for restoring settings on startup. The scalar settings are tiny
191+ // IndexedDB reads (sub-ms normally), so the only way they stall is a wedged DB
192+ // (e.g. another tab holding a versionchange that blocks our open). Past this we
193+ // stop waiting, log it, and boot on defaults rather than hang on a blank state.
194+ const SETTINGS_LOAD_TIMEOUT_MS = 6000 ;
195+
190196async function loadSetting ( key , defaultValue ) {
191197 try {
192198 const value = await idbGet ( await getSettingsDb ( ) , SETTINGS_STORE_NAME , STORAGE_KEY_PREFIX + key ) ;
@@ -859,6 +865,12 @@ export default function App() {
859865 const [ showSettings , setShowSettings ] = useState ( false ) ;
860866 const [ showShortcuts , setShowShortcuts ] = useState ( false ) ;
861867 const [ settingsLoaded , setSettingsLoaded ] = useState ( false ) ;
868+ // True once the (potentially large/slow) transcript history has actually been
869+ // read back into state. The history loads *after* settingsLoaded flips, so the
870+ // transcript-persist effect must wait on this too: otherwise the early
871+ // settingsLoaded=true would let it write the still-empty array over the
872+ // on-disk history before the read resolves. See the load effect below.
873+ const transcriptsRestoredRef = useRef ( false ) ;
862874 const [ showConfidenceHeatmap , setShowConfidenceHeatmap ] = useState ( false ) ;
863875 // Auto-copy: when enabled, every transcription is written to the system
864876 // clipboard. F-125: default OFF so the headline "audio never leaves your
@@ -901,24 +913,44 @@ export default function App() {
901913
902914 // Load settings from IndexedDB on mount
903915 useEffect ( ( ) => {
916+ // `booted` guards against the watchdog and the real load both finishing the
917+ // restore. Whichever flips it first wins; the loser bails (so a late real
918+ // load does not overwrite the defaults the watchdog already booted on).
919+ let booted = false ;
920+ const watchdog = setTimeout ( ( ) => {
921+ if ( booted ) return ;
922+ booted = true ;
923+ console . warn ( `[App] Settings restore timed out after ${ SETTINGS_LOAD_TIMEOUT_MS } ms `
924+ + `(IndexedDB likely blocked by another tab holding a versionchange); `
925+ + `booting on defaults. Saved preferences were not applied this session.` ) ;
926+ setSettingsLoaded ( true ) ;
927+ } , SETTINGS_LOAD_TIMEOUT_MS ) ;
928+
904929 async function loadSettings ( ) {
905930 try {
906931 // Check version first - purge old data if version mismatch
907932 const storedVersion = await loadSetting ( 'version' , null ) ;
908-
933+ if ( booted ) return ; // watchdog already booted on defaults; skip late restore
934+
909935 if ( ! storedVersion || storedVersion !== VERSION ) {
910936 console . log ( `[App] Version mismatch (stored: ${ storedVersion } , current: ${ VERSION } ). Purging old data...` ) ;
911937 await clearAllSettings ( ) ;
912938 await saveSetting ( 'version' , VERSION ) ;
939+ if ( booted ) return ;
940+ booted = true ;
941+ clearTimeout ( watchdog ) ;
913942 // Set defaults without loading old values
914943 setSettingsLoaded ( true ) ;
915944 return ;
916945 }
917-
946+
947+ // Fast phase: the scalar settings are tiny reads loaded together. The
948+ // transcript history (potentially large/slow) is deliberately NOT in
949+ // this batch; it loads last, after the app boots, so it can never delay
950+ // restore or trip the watchdog.
918951 const [
919952 savedBackend ,
920953 savedPreprocessor ,
921- savedTranscriptions ,
922954 savedVerboseLog ,
923955 savedFrameStride ,
924956 savedBeamWidth ,
@@ -948,7 +980,6 @@ export default function App() {
948980 ] = await Promise . all ( [
949981 loadSetting ( 'backend' , null ) ,
950982 loadSetting ( 'preprocessor' , 'nemo128' ) ,
951- loadPersistedTranscripts ( ) ,
952983 loadSetting ( 'verboseLog' , false ) ,
953984 loadSetting ( 'frameStride' , 1 ) ,
954985 loadSetting ( 'beamWidth' , 1 ) ,
@@ -963,10 +994,8 @@ export default function App() {
963994 loadSetting ( 'showConfidenceHeatmap' , false ) ,
964995 loadSetting ( 'autoTranscribe' , true ) ,
965996 loadSetting ( 'autoCopyToClipboard' , false ) ,
966- // Migration: load with `null` so we can distinguish "user opted in/out"
967- // from "never set, default to false". Resolved below against existing
968- // transcript presence so we don't silently delete history of users
969- // who upgraded into the post-F-55 build.
997+ // Load with `null` so the F-132 default below can tell "never set"
998+ // apart from an explicit choice (see the setPersistTranscripts comment).
970999 loadSetting ( 'persistTranscripts' , null ) ,
9711000 loadSetting ( 'showAdvancedInfo' , false ) ,
9721001 loadSetting ( 'enableChunking' , true ) ,
@@ -980,6 +1009,7 @@ export default function App() {
9801009 loadSetting ( 'boostCustomText' , '' ) ,
9811010 loadSetting ( 'boostCaseInsensitive' , false ) ,
9821011 ] ) ;
1012+ if ( booted ) return ; // watchdog won while we awaited; skip the stale restore
9831013
9841014 // A saved value means the user previously picked a backend explicitly;
9851015 // honour it (subject to the WebGPU-availability override below). When
@@ -990,7 +1020,6 @@ export default function App() {
9901020 setBackend ( savedBackend ) ;
9911021 }
9921022 setPreprocessor ( savedPreprocessor ) ;
993- setTranscriptions ( savedTranscriptions . filter ( t => t . text && t . text . trim ( ) !== '' ) ) ;
9941023 setVerboseLog ( savedVerboseLog ) ;
9951024 setFrameStride ( savedFrameStride ) ;
9961025 setBeamWidth ( Number . isInteger ( savedBeamWidth ) && savedBeamWidth >= 1 ? Math . min ( 25 , savedBeamWidth ) : 1 ) ;
@@ -1045,14 +1074,31 @@ export default function App() {
10451074 boostCustomTextRef . current = seedCustom ;
10461075 setBoostSource ( restoredSource ) ;
10471076 }
1077+ // Scalar settings are in; boot the app now so the UI is configured and
1078+ // persistence/boost-init can proceed, and stop the watchdog.
1079+ booted = true ;
1080+ clearTimeout ( watchdog ) ;
10481081 setSettingsLoaded ( true ) ;
1082+
1083+ // Slow phase, last: restore the transcript history. Done after booting
1084+ // so a large/slow read never blocks restore. transcriptsRestoredRef
1085+ // gates the persist effect until this lands, so the setSettingsLoaded
1086+ // above cannot write the empty in-memory array over the on-disk history.
1087+ const savedTranscriptions = await loadPersistedTranscripts ( ) ;
1088+ setTranscriptions ( savedTranscriptions . filter ( t => t . text && t . text . trim ( ) !== '' ) ) ;
1089+ transcriptsRestoredRef . current = true ;
10491090 } catch ( e ) {
10501091 console . error ( 'Failed to load settings from IndexedDB:' , e ) ;
1051- setSettingsLoaded ( true ) ;
1092+ if ( ! booted ) {
1093+ booted = true ;
1094+ clearTimeout ( watchdog ) ;
1095+ setSettingsLoaded ( true ) ;
1096+ }
10521097 }
10531098 }
1054-
1099+
10551100 loadSettings ( ) ;
1101+ return ( ) => clearTimeout ( watchdog ) ;
10561102 } , [ maxCores ] ) ;
10571103
10581104 // Cleanup on component unmount
@@ -1408,7 +1454,10 @@ export default function App() {
14081454 // the prior longer array. Append-only growth uses a plain idbPut.
14091455 const prevTranscriptsLenRef = useRef ( transcriptions . length ) ;
14101456 useEffect ( ( ) => {
1411- if ( ! settingsLoaded || ! persistTranscripts ) {
1457+ // Wait for the history read to land (transcriptsRestoredRef) before writing:
1458+ // settingsLoaded flips before the read resolves, so persisting here too early
1459+ // would clobber the on-disk history with the still-empty in-memory array.
1460+ if ( ! settingsLoaded || ! transcriptsRestoredRef . current || ! persistTranscripts ) {
14121461 prevTranscriptsLenRef . current = transcriptions . length ;
14131462 return ;
14141463 }
0 commit comments