33import  {  api  }  from  "~/trpc/react" ; 
44import  {  Badge  }  from  "./ui/badge" ; 
55import  {  Button  }  from  "./ui/button" ; 
6- import  {  ExternalLink ,  Download ,  RefreshCw ,  Loader2 ,  Check  }  from  "lucide-react" ; 
7- import  {  useState  }  from  "react" ; 
86
9- // Loading overlay component 
10- function  LoadingOverlay ( {  isNetworkError =  false  } : {  isNetworkError ?: boolean  } )  { 
7+ import  {  ExternalLink ,  Download ,  RefreshCw ,  Loader2  }  from  "lucide-react" ; 
8+ import  {  useState ,  useEffect ,  useRef  }  from  "react" ; 
9+ 
10+ // Loading overlay component with log streaming 
11+ function  LoadingOverlay ( {  
12+   isNetworkError =  false ,  
13+   logs =  [ ]  
14+ } : {  
15+   isNetworkError ?: boolean ;  
16+   logs ?: string [ ] ; 
17+ } )  { 
18+   const  logsEndRef  =  useRef < HTMLDivElement > ( null ) ; 
19+ 
20+   // Auto-scroll to bottom when new logs arrive 
21+   useEffect ( ( )  =>  { 
22+     logsEndRef . current ?. scrollIntoView ( {  behavior : 'smooth'  } ) ; 
23+   } ,  [ logs ] ) ; 
24+ 
25+ 
1126  return  ( 
1227    < div  className = "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" > 
13-       < div  className = "bg-white dark:bg-gray-800  rounded-lg p-8 shadow-2xl border border-gray-200 dark: border-gray-700  max-w-md  mx-4" > 
28+       < div  className = "bg-card  rounded-lg p-8 shadow-2xl border border-border max-w-2xl w-full  mx-4 max-h-[80vh] flex flex-col " > 
1429        < div  className = "flex flex-col items-center space-y-4" > 
1530          < div  className = "relative" > 
16-             < Loader2  className = "h-12 w-12 animate-spin text-blue-600 dark:text-blue-400 "  /> 
17-             < div  className = "absolute inset-0 rounded-full border-2 border-blue-200 dark:border-blue-800  animate-pulse" > </ div > 
31+             < Loader2  className = "h-12 w-12 animate-spin text-primary "  /> 
32+             < div  className = "absolute inset-0 rounded-full border-2 border-primary/20  animate-pulse" > </ div > 
1833          </ div > 
1934          < div  className = "text-center" > 
20-             < h3  className = "text-lg font-semibold text-gray-900 dark:text-gray-100  mb-2" > 
35+             < h3  className = "text-lg font-semibold text-card-foreground  mb-2" > 
2136              { isNetworkError  ? 'Server Restarting'  : 'Updating Application' } 
2237            </ h3 > 
23-             < p  className = "text-sm text-gray-600 dark:text-gray-400 " > 
38+             < p  className = "text-sm text-muted-foreground " > 
2439              { isNetworkError  
2540                ? 'The server is restarting after the update...'  
2641                : 'Please stand by while we update your application...' 
2742              } 
2843            </ p > 
29-             < p  className = "text-xs text-gray-500 dark:text-gray-500  mt-2" > 
44+             < p  className = "text-xs text-muted-foreground  mt-2" > 
3045              { isNetworkError  
31-                 ? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!. ' 
46+                 ? 'This may take a few moments. The page will reload automatically.' 
3247                : 'The server will restart automatically when complete.' 
3348              } 
3449            </ p > 
3550          </ div > 
51+           
52+           { /* Log output */ } 
53+           { logs . length  >  0  &&  ( 
54+             < div  className = "w-full mt-4 bg-card border border-border rounded-lg p-4 font-mono text-xs text-chart-2 max-h-60 overflow-y-auto terminal-output" > 
55+               { logs . map ( ( log ,  index )  =>  ( 
56+                 < div  key = { index }  className = "mb-1 whitespace-pre-wrap break-words" > 
57+                   { log } 
58+                 </ div > 
59+               ) ) } 
60+               < div  ref = { logsEndRef }  /> 
61+             </ div > 
62+           ) } 
63+ 
3664          < div  className = "flex space-x-1" > 
37-             < div  className = "w-2 h-2 bg-blue-600  rounded-full animate-bounce" > </ div > 
38-             < div  className = "w-2 h-2 bg-blue-600  rounded-full animate-bounce"  style = { {  animationDelay : '0.1s'  } } > </ div > 
39-             < div  className = "w-2 h-2 bg-blue-600  rounded-full animate-bounce"  style = { {  animationDelay : '0.2s'  } } > </ div > 
65+             < div  className = "w-2 h-2 bg-primary  rounded-full animate-bounce" > </ div > 
66+             < div  className = "w-2 h-2 bg-primary  rounded-full animate-bounce"  style = { {  animationDelay : '0.1s'  } } > </ div > 
67+             < div  className = "w-2 h-2 bg-primary  rounded-full animate-bounce"  style = { {  animationDelay : '0.2s'  } } > </ div > 
4068          </ div > 
4169        </ div > 
4270      </ div > 
@@ -48,79 +76,126 @@ export function VersionDisplay() {
4876  const  {  data : versionStatus ,  isLoading,  error }  =  api . version . getVersionStatus . useQuery ( ) ; 
4977  const  [ isUpdating ,  setIsUpdating ]  =  useState ( false ) ; 
5078  const  [ updateResult ,  setUpdateResult ]  =  useState < {  success : boolean ;  message : string  }  |  null > ( null ) ; 
51-   const  [ updateStartTime ,  setUpdateStartTime ]  =  useState < number  |  null > ( null ) ; 
5279  const  [ isNetworkError ,  setIsNetworkError ]  =  useState ( false ) ; 
80+   const  [ updateLogs ,  setUpdateLogs ]  =  useState < string [ ] > ( [ ] ) ; 
81+   const  [ shouldSubscribe ,  setShouldSubscribe ]  =  useState ( false ) ; 
82+   const  [ updateStartTime ,  setUpdateStartTime ]  =  useState < number  |  null > ( null ) ; 
83+   const  lastLogTimeRef  =  useRef < number > ( Date . now ( ) ) ; 
84+   const  reconnectIntervalRef  =  useRef < NodeJS . Timeout  |  null > ( null ) ; 
5385
5486  const  executeUpdate  =  api . version . executeUpdate . useMutation ( { 
55-     onSuccess : ( result : any )  =>  { 
56-       const  now  =  Date . now ( ) ; 
57-       const  elapsed  =  updateStartTime  ? now  -  updateStartTime  : 0 ; 
58-  
59-       
87+     onSuccess : ( result )  =>  { 
6088      setUpdateResult ( {  success : result . success ,  message : result . message  } ) ; 
6189
6290      if  ( result . success )  { 
63-         // The script now runs independently, so we show a longer overlay 
64-         // and wait for the server to restart 
65-         setIsNetworkError ( true ) ; 
66-         setUpdateResult ( {  success : true ,  message : 'Update in progress... Server will restart automatically.'  } ) ; 
67- 
68-         // Wait longer for the update to complete and server to restart 
69-         setTimeout ( ( )  =>  { 
70-           setIsUpdating ( false ) ; 
71-           setIsNetworkError ( false ) ; 
72-           // Try to reload after the update completes 
73-           setTimeout ( ( )  =>  { 
74-             window . location . reload ( ) ; 
75-           } ,  10000 ) ;  // 10 seconds to allow for update completion 
76-         } ,  5000 ) ;  // Show overlay for 5 seconds 
91+         // Start subscribing to update logs 
92+         setShouldSubscribe ( true ) ; 
93+         setUpdateLogs ( [ 'Update started...' ] ) ; 
7794      }  else  { 
78-         // For errors, show for at least 1 second 
79-         const  remainingTime  =  Math . max ( 0 ,  1000  -  elapsed ) ; 
80-         setTimeout ( ( )  =>  { 
81-           setIsUpdating ( false ) ; 
82-         } ,  remainingTime ) ; 
95+         setIsUpdating ( false ) ; 
8396      } 
8497    } , 
8598    onError : ( error )  =>  { 
86-       const  now  =  Date . now ( ) ; 
87-       const  elapsed  =  updateStartTime  ? now  -  updateStartTime  : 0 ; 
99+       setUpdateResult ( {  success : false ,  message : error . message  } ) ; 
100+       setIsUpdating ( false ) ; 
101+     } 
102+   } ) ; 
103+ 
104+   // Poll for update logs 
105+   const  {  data : updateLogsData  }  =  api . version . getUpdateLogs . useQuery ( undefined ,  { 
106+     enabled : shouldSubscribe , 
107+     refetchInterval : 1000 ,  // Poll every second 
108+     refetchIntervalInBackground : true , 
109+   } ) ; 
110+ 
111+   // Update logs when data changes 
112+   useEffect ( ( )  =>  { 
113+     if  ( updateLogsData ?. success  &&  updateLogsData . logs )  { 
114+       lastLogTimeRef . current  =  Date . now ( ) ; 
115+       setUpdateLogs ( updateLogsData . logs ) ; 
88116
89-       // Check if this is a network error (expected during server restart) 
90-       const  isNetworkError  =  error . message . includes ( 'Failed to fetch' )  ||  
91-                             error . message . includes ( 'NetworkError' )  || 
92-                             error . message . includes ( 'fetch' )  || 
93-                             error . message . includes ( 'network' ) ; 
117+       if  ( updateLogsData . isComplete )  { 
118+         setUpdateLogs ( prev  =>  [ ...prev ,  'Update complete! Server restarting...' ] ) ; 
119+         setIsNetworkError ( true ) ; 
120+         // Start reconnection attempts when we know update is complete 
121+         startReconnectAttempts ( ) ; 
122+       } 
123+     } 
124+   } ,  [ updateLogsData ] ) ; 
125+ 
126+   // Monitor for server connection loss and auto-reload (fallback only) 
127+   useEffect ( ( )  =>  { 
128+     if  ( ! shouldSubscribe )  return ; 
129+ 
130+     // Only use this as a fallback - the main trigger should be completion detection 
131+     const  checkInterval  =  setInterval ( ( )  =>  { 
132+       const  timeSinceLastLog  =  Date . now ( )  -  lastLogTimeRef . current ; 
133+       
134+       // Only start reconnection if we've been updating for at least 3 minutes 
135+       // and no logs for 60 seconds (very conservative fallback) 
136+       const  hasBeenUpdatingLongEnough  =  updateStartTime  &&  ( Date . now ( )  -  updateStartTime )  >  180000 ;  // 3 minutes 
137+       const  noLogsForAWhile  =  timeSinceLastLog  >  60000 ;  // 60 seconds 
94138
95-       if  ( isNetworkError  &&  elapsed  <  60000 )  {  // If it's a network error within 30 seconds, treat as success 
139+       if  ( hasBeenUpdatingLongEnough  &&  noLogsForAWhile  &&  isUpdating  &&  ! isNetworkError )  { 
140+         console . log ( 'Fallback: Assuming server restart due to long silence' ) ; 
96141        setIsNetworkError ( true ) ; 
97-         setUpdateResult ( {   success :  true ,   message :  'Update in progress ... Server is restarting.'   } ) ; 
142+         setUpdateLogs ( prev   =>   [ ... prev ,   'Server restarting ... waiting for reconnection...' ] ) ; 
98143
99-         // Wait longer for server to come back up 
100-         setTimeout ( ( )  =>  { 
101-           setIsUpdating ( false ) ; 
102-           setIsNetworkError ( false ) ; 
103-           // Try to reload after a longer delay 
104-           setTimeout ( ( )  =>  { 
105-             window . location . reload ( ) ; 
106-           } ,  5000 ) ; 
107-         } ,  3000 ) ; 
108-       }  else  { 
109-         // For real errors, show for at least 1 second 
110-         setUpdateResult ( {  success : false ,  message : error . message  } ) ; 
111-         const  remainingTime  =  Math . max ( 0 ,  1000  -  elapsed ) ; 
112-         setTimeout ( ( )  =>  { 
113-           setIsUpdating ( false ) ; 
114-         } ,  remainingTime ) ; 
144+         // Start trying to reconnect 
145+         startReconnectAttempts ( ) ; 
115146      } 
116-     } 
117-   } ) ; 
147+     } ,  10000 ) ;  // Check every 10 seconds 
148+ 
149+     return  ( )  =>  clearInterval ( checkInterval ) ; 
150+   } ,  [ shouldSubscribe ,  isUpdating ,  updateStartTime ,  isNetworkError ] ) ; 
151+ 
152+   // Attempt to reconnect and reload page when server is back 
153+   const  startReconnectAttempts  =  ( )  =>  { 
154+     if  ( reconnectIntervalRef . current )  return ; 
155+     
156+     setUpdateLogs ( prev  =>  [ ...prev ,  'Attempting to reconnect...' ] ) ; 
157+     
158+     reconnectIntervalRef . current  =  setInterval ( ( )  =>  { 
159+       void  ( async  ( )  =>  { 
160+         try  { 
161+           // Try to fetch the root path to check if server is back 
162+           const  response  =  await  fetch ( '/' ,  {  method : 'HEAD'  } ) ; 
163+           if  ( response . ok  ||  response . status  ===  200 )  { 
164+             setUpdateLogs ( prev  =>  [ ...prev ,  'Server is back online! Reloading...' ] ) ; 
165+             
166+             // Clear interval and reload 
167+             if  ( reconnectIntervalRef . current )  { 
168+               clearInterval ( reconnectIntervalRef . current ) ; 
169+             } 
170+             
171+             setTimeout ( ( )  =>  { 
172+               window . location . reload ( ) ; 
173+             } ,  1000 ) ; 
174+           } 
175+         }  catch  { 
176+           // Server still down, keep trying 
177+         } 
178+       } ) ( ) ; 
179+     } ,  2000 ) ; 
180+   } ; 
181+ 
182+   // Cleanup reconnect interval on unmount 
183+   useEffect ( ( )  =>  { 
184+     return  ( )  =>  { 
185+       if  ( reconnectIntervalRef . current )  { 
186+         clearInterval ( reconnectIntervalRef . current ) ; 
187+       } 
188+     } ; 
189+   } ,  [ ] ) ; 
118190
119191  const  handleUpdate  =  ( )  =>  { 
120192    setIsUpdating ( true ) ; 
121193    setUpdateResult ( null ) ; 
122194    setIsNetworkError ( false ) ; 
195+     setUpdateLogs ( [ ] ) ; 
196+     setShouldSubscribe ( false ) ; 
123197    setUpdateStartTime ( Date . now ( ) ) ; 
198+     lastLogTimeRef . current  =  Date . now ( ) ; 
124199    executeUpdate . mutate ( ) ; 
125200  } ; 
126201
@@ -152,7 +227,7 @@ export function VersionDisplay() {
152227  return  ( 
153228    < > 
154229      { /* Loading overlay */ } 
155-       { isUpdating  &&  < LoadingOverlay  isNetworkError = { isNetworkError }  /> } 
230+       { isUpdating  &&  < LoadingOverlay  isNetworkError = { isNetworkError }  logs = { updateLogs }   /> } 
156231
157232      < div  className = "flex items-center gap-2" > 
158233        < Badge  variant = { isUpToDate  ? "default"  : "secondary" } > 
@@ -168,7 +243,7 @@ export function VersionDisplay() {
168243              < div  className = "absolute top-full left-1/2 transform -translate-x-1/2 mt-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-10" > 
169244                < div  className = "text-center" > 
170245                  < div  className = "font-semibold mb-1" > How to update:</ div > 
171-                   < div > Click the button to update</ div > 
246+                   < div > Click the button to update, when installed via the helper script </ div > 
172247                  < div > or update manually:</ div > 
173248                  < div > cd $PVESCRIPTLOCAL_DIR</ div > 
174249                  < div > git pull</ div > 
@@ -213,8 +288,8 @@ export function VersionDisplay() {
213288            { updateResult  &&  ( 
214289              < div  className = { `text-xs px-2 py-1 rounded ${  
215290                updateResult . success   
216-                   ? 'bg-green-100 dark:bg-green-900  text-green-800 dark:text-green-200 '   
217-                   : 'bg-red-100 dark:bg-red-900  text-red-800 dark:text-red-200 '  
291+                   ? 'bg-chart-2/20  text-chart-2 border border-chart-2/30 '   
292+                   : 'bg-destructive/20  text-destructive border border-destructive/30 '  
218293              }  `} > 
219294                { updateResult . message } 
220295              </ div > 
@@ -223,9 +298,8 @@ export function VersionDisplay() {
223298        ) } 
224299
225300        { isUpToDate  &&  ( 
226-           < span  className = "text-xs text-green-600 dark:text-green-400 flex items-center gap-1" > 
227-             < Check  className = "h-3 w-3"  /> 
228-             Up to date
301+           < span  className = "text-xs text-chart-2" > 
302+             ✓ Up to date
229303          </ span > 
230304        ) } 
231305      </ div > 
0 commit comments