@@ -4,13 +4,26 @@ import { api } from "~/trpc/react";
44import { Badge } from "./ui/badge" ;
55import { Button } from "./ui/button" ;
66import { ExternalLink , Download , RefreshCw , Loader2 } from "lucide-react" ;
7- import { useState } from "react" ;
7+ import { useState , useEffect , useRef } from "react" ;
8+
9+ // Loading overlay component with log streaming
10+ function LoadingOverlay ( {
11+ isNetworkError = false ,
12+ logs = [ ]
13+ } : {
14+ isNetworkError ?: boolean ;
15+ logs ?: string [ ] ;
16+ } ) {
17+ const logsEndRef = useRef < HTMLDivElement > ( null ) ;
18+
19+ // Auto-scroll to bottom when new logs arrive
20+ useEffect ( ( ) => {
21+ logsEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
22+ } , [ logs ] ) ;
823
9- // Loading overlay component
10- function LoadingOverlay ( { isNetworkError = false } : { isNetworkError ?: boolean } ) {
1124 return (
1225 < 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" >
26+ < div className = "bg-white dark:bg-gray-800 rounded-lg p-8 shadow-2xl border border-gray-200 dark:border-gray-700 max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col " >
1427 < div className = "flex flex-col items-center space-y-4" >
1528 < div className = "relative" >
1629 < Loader2 className = "h-12 w-12 animate-spin text-blue-600 dark:text-blue-400" />
@@ -28,11 +41,24 @@ function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }
2841 </ p >
2942 < p className = "text-xs text-gray-500 dark:text-gray-500 mt-2" >
3043 { isNetworkError
31- ? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!. '
44+ ? 'This may take a few moments. The page will reload automatically.'
3245 : 'The server will restart automatically when complete.'
3346 }
3447 </ p >
3548 </ div >
49+
50+ { /* Log output */ }
51+ { logs . length > 0 && (
52+ < div className = "w-full mt-4 bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-xs text-green-400 max-h-60 overflow-y-auto" >
53+ { logs . map ( ( log , index ) => (
54+ < div key = { index } className = "mb-1 whitespace-pre-wrap break-words" >
55+ { log }
56+ </ div >
57+ ) ) }
58+ < div ref = { logsEndRef } />
59+ </ div >
60+ ) }
61+
3662 < div className = "flex space-x-1" >
3763 < div className = "w-2 h-2 bg-blue-600 rounded-full animate-bounce" > </ div >
3864 < div className = "w-2 h-2 bg-blue-600 rounded-full animate-bounce" style = { { animationDelay : '0.1s' } } > </ div >
@@ -48,79 +74,118 @@ export function VersionDisplay() {
4874 const { data : versionStatus , isLoading, error } = api . version . getVersionStatus . useQuery ( ) ;
4975 const [ isUpdating , setIsUpdating ] = useState ( false ) ;
5076 const [ updateResult , setUpdateResult ] = useState < { success : boolean ; message : string } | null > ( null ) ;
51- const [ updateStartTime , setUpdateStartTime ] = useState < number | null > ( null ) ;
5277 const [ isNetworkError , setIsNetworkError ] = useState ( false ) ;
78+ const [ updateLogs , setUpdateLogs ] = useState < string [ ] > ( [ ] ) ;
79+ const [ shouldSubscribe , setShouldSubscribe ] = useState ( false ) ;
80+ const lastLogTimeRef = useRef < number > ( Date . now ( ) ) ;
81+ const reconnectIntervalRef = useRef < NodeJS . Timeout | null > ( null ) ;
5382
5483 const executeUpdate = api . version . executeUpdate . useMutation ( {
55- onSuccess : ( result : any ) => {
56- const now = Date . now ( ) ;
57- const elapsed = updateStartTime ? now - updateStartTime : 0 ;
58-
59-
84+ onSuccess : ( result ) => {
6085 setUpdateResult ( { success : result . success , message : result . message } ) ;
6186
6287 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
88+ // Start subscribing to update logs
89+ setShouldSubscribe ( true ) ;
90+ setUpdateLogs ( [ 'Update started...' ] ) ;
7791 } 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 ) ;
92+ setIsUpdating ( false ) ;
8393 }
8494 } ,
8595 onError : ( error ) => {
86- const now = Date . now ( ) ;
87- const elapsed = updateStartTime ? now - updateStartTime : 0 ;
96+ setUpdateResult ( { success : false , message : error . message } ) ;
97+ setIsUpdating ( false ) ;
98+ }
99+ } ) ;
100+
101+ // Subscribe to update progress
102+ api . version . streamUpdateProgress . useSubscription ( undefined , {
103+ enabled : shouldSubscribe ,
104+ onData : ( data ) => {
105+ lastLogTimeRef . current = Date . now ( ) ;
88106
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' ) ;
107+ if ( data . type === 'log' ) {
108+ setUpdateLogs ( prev => [ ...prev , data . message ] ) ;
109+ } else if ( data . type === 'complete' ) {
110+ setUpdateLogs ( prev => [ ...prev , 'Update complete! Server restarting...' ] ) ;
111+ setIsNetworkError ( true ) ;
112+ } else if ( data . type === 'error' ) {
113+ setUpdateLogs ( prev => [ ...prev , `Error: ${ data . message } ` ] ) ;
114+ }
115+ } ,
116+ onError : ( error ) => {
117+ // Connection lost - likely server restarted
118+ console . log ( 'Update stream connection lost, server likely restarting' ) ;
119+ setIsNetworkError ( true ) ;
120+ setUpdateLogs ( prev => [ ...prev , 'Connection lost - server restarting...' ] ) ;
121+ } ,
122+ } ) ;
123+
124+ // Monitor for server connection loss and auto-reload
125+ useEffect ( ( ) => {
126+ if ( ! shouldSubscribe ) return ;
127+
128+ // Check if logs have stopped coming for a while
129+ const checkInterval = setInterval ( ( ) => {
130+ const timeSinceLastLog = Date . now ( ) - lastLogTimeRef . current ;
94131
95- if ( isNetworkError && elapsed < 60000 ) { // If it's a network error within 30 seconds, treat as success
132+ // If no logs for 3 seconds and we're updating, assume server is restarting
133+ if ( timeSinceLastLog > 3000 && isUpdating ) {
96134 setIsNetworkError ( true ) ;
97- setUpdateResult ( { success : true , message : 'Update in progress ... Server is restarting.' } ) ;
135+ setUpdateLogs ( prev => [ ... prev , 'Server restarting ... waiting for reconnection...' ] ) ;
98136
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
137+ // Start trying to reconnect
138+ startReconnectAttempts ( ) ;
139+ }
140+ } , 1000 ) ;
141+
142+ return ( ) => clearInterval ( checkInterval ) ;
143+ } , [ shouldSubscribe , isUpdating ] ) ;
144+
145+ // Attempt to reconnect and reload page when server is back
146+ const startReconnectAttempts = ( ) => {
147+ if ( reconnectIntervalRef . current ) return ;
148+
149+ setUpdateLogs ( prev => [ ...prev , 'Attempting to reconnect...' ] ) ;
150+
151+ reconnectIntervalRef . current = setInterval ( async ( ) => {
152+ try {
153+ // Try to fetch the root path to check if server is back
154+ const response = await fetch ( '/' , { method : 'HEAD' } ) ;
155+ if ( response . ok || response . status === 200 ) {
156+ setUpdateLogs ( prev => [ ...prev , 'Server is back online! Reloading...' ] ) ;
157+
158+ // Clear interval and reload
159+ if ( reconnectIntervalRef . current ) {
160+ clearInterval ( reconnectIntervalRef . current ) ;
161+ }
162+
104163 setTimeout ( ( ) => {
105164 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 ) ;
165+ } , 1000 ) ;
166+ }
167+ } catch {
168+ // Server still down, keep trying
115169 }
116- }
117- } ) ;
170+ } , 2000 ) ;
171+ } ;
172+
173+ // Cleanup reconnect interval on unmount
174+ useEffect ( ( ) => {
175+ return ( ) => {
176+ if ( reconnectIntervalRef . current ) {
177+ clearInterval ( reconnectIntervalRef . current ) ;
178+ }
179+ } ;
180+ } , [ ] ) ;
118181
119182 const handleUpdate = ( ) => {
120183 setIsUpdating ( true ) ;
121184 setUpdateResult ( null ) ;
122185 setIsNetworkError ( false ) ;
123- setUpdateStartTime ( Date . now ( ) ) ;
186+ setUpdateLogs ( [ ] ) ;
187+ setShouldSubscribe ( false ) ;
188+ lastLogTimeRef . current = Date . now ( ) ;
124189 executeUpdate . mutate ( ) ;
125190 } ;
126191
@@ -152,7 +217,7 @@ export function VersionDisplay() {
152217 return (
153218 < >
154219 { /* Loading overlay */ }
155- { isUpdating && < LoadingOverlay isNetworkError = { isNetworkError } /> }
220+ { isUpdating && < LoadingOverlay isNetworkError = { isNetworkError } logs = { updateLogs } /> }
156221
157222 < div className = "flex items-center gap-2" >
158223 < Badge variant = { isUpToDate ? "default" : "secondary" } >
0 commit comments