3
3
import { api } from "~/trpc/react" ;
4
4
import { Badge } from "./ui/badge" ;
5
5
import { Button } from "./ui/button" ;
6
- import { ExternalLink , Download , RefreshCw , Loader2 , Check } from "lucide-react" ;
7
- import { useState } from "react" ;
8
6
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
+
11
26
return (
12
27
< 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 " >
14
29
< div className = "flex flex-col items-center space-y-4" >
15
30
< 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 >
18
33
</ div >
19
34
< 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" >
21
36
{ isNetworkError ? 'Server Restarting' : 'Updating Application' }
22
37
</ h3 >
23
- < p className = "text-sm text-gray-600 dark:text-gray-400 " >
38
+ < p className = "text-sm text-muted-foreground " >
24
39
{ isNetworkError
25
40
? 'The server is restarting after the update...'
26
41
: 'Please stand by while we update your application...'
27
42
}
28
43
</ 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" >
30
45
{ 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.'
32
47
: 'The server will restart automatically when complete.'
33
48
}
34
49
</ p >
35
50
</ 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
+
36
64
< 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 >
40
68
</ div >
41
69
</ div >
42
70
</ div >
@@ -48,79 +76,126 @@ export function VersionDisplay() {
48
76
const { data : versionStatus , isLoading, error } = api . version . getVersionStatus . useQuery ( ) ;
49
77
const [ isUpdating , setIsUpdating ] = useState ( false ) ;
50
78
const [ updateResult , setUpdateResult ] = useState < { success : boolean ; message : string } | null > ( null ) ;
51
- const [ updateStartTime , setUpdateStartTime ] = useState < number | null > ( null ) ;
52
79
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 ) ;
53
85
54
86
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 ) => {
60
88
setUpdateResult ( { success : result . success , message : result . message } ) ;
61
89
62
90
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...' ] ) ;
77
94
} 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 ) ;
83
96
}
84
97
} ,
85
98
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 ) ;
88
116
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
94
138
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' ) ;
96
141
setIsNetworkError ( true ) ;
97
- setUpdateResult ( { success : true , message : 'Update in progress ... Server is restarting.' } ) ;
142
+ setUpdateLogs ( prev => [ ... prev , 'Server restarting ... waiting for reconnection...' ] ) ;
98
143
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 ( ) ;
115
146
}
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
+ } , [ ] ) ;
118
190
119
191
const handleUpdate = ( ) => {
120
192
setIsUpdating ( true ) ;
121
193
setUpdateResult ( null ) ;
122
194
setIsNetworkError ( false ) ;
195
+ setUpdateLogs ( [ ] ) ;
196
+ setShouldSubscribe ( false ) ;
123
197
setUpdateStartTime ( Date . now ( ) ) ;
198
+ lastLogTimeRef . current = Date . now ( ) ;
124
199
executeUpdate . mutate ( ) ;
125
200
} ;
126
201
@@ -152,7 +227,7 @@ export function VersionDisplay() {
152
227
return (
153
228
< >
154
229
{ /* Loading overlay */ }
155
- { isUpdating && < LoadingOverlay isNetworkError = { isNetworkError } /> }
230
+ { isUpdating && < LoadingOverlay isNetworkError = { isNetworkError } logs = { updateLogs } /> }
156
231
157
232
< div className = "flex items-center gap-2" >
158
233
< Badge variant = { isUpToDate ? "default" : "secondary" } >
@@ -168,7 +243,7 @@ export function VersionDisplay() {
168
243
< 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" >
169
244
< div className = "text-center" >
170
245
< 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 >
172
247
< div > or update manually:</ div >
173
248
< div > cd $PVESCRIPTLOCAL_DIR</ div >
174
249
< div > git pull</ div >
@@ -213,8 +288,8 @@ export function VersionDisplay() {
213
288
{ updateResult && (
214
289
< div className = { `text-xs px-2 py-1 rounded ${
215
290
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 '
218
293
} `} >
219
294
{ updateResult . message }
220
295
</ div >
@@ -223,9 +298,8 @@ export function VersionDisplay() {
223
298
) }
224
299
225
300
{ 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
229
303
</ span >
230
304
) }
231
305
</ div >
0 commit comments