1- import { HardDriveDownload , Loader2 } from "lucide-react" ;
1+ import {
2+ AlertTriangle ,
3+ CheckCircle2 ,
4+ HardDriveDownload ,
5+ Loader2 ,
6+ RefreshCw ,
7+ XCircle ,
8+ } from "lucide-react" ;
29import { useState } from "react" ;
310import { toast } from "sonner" ;
411import {
@@ -15,11 +22,70 @@ import {
1522import { Button } from "@/components/ui/button" ;
1623import { api } from "@/utils/api" ;
1724
25+ type ServiceStatus = {
26+ status : "healthy" | "unhealthy" ;
27+ message ?: string ;
28+ } ;
29+
30+ type HealthResult = {
31+ postgres : ServiceStatus ;
32+ redis : ServiceStatus ;
33+ traefik : ServiceStatus ;
34+ } ;
35+
36+ type ModalState = "idle" | "checking" | "results" | "updating" ;
37+
38+ const ServiceStatusItem = ( {
39+ name,
40+ service,
41+ } : {
42+ name : string ;
43+ service : ServiceStatus ;
44+ } ) => (
45+ < div className = "flex items-center gap-2" >
46+ { service . status === "healthy" ? (
47+ < CheckCircle2 className = "h-4 w-4 text-green-500" />
48+ ) : (
49+ < XCircle className = "h-4 w-4 text-red-500" />
50+ ) }
51+ < span className = "text-sm font-medium" > { name } </ span >
52+ { service . status === "unhealthy" && service . message && (
53+ < span className = "text-xs text-muted-foreground" > — { service . message } </ span >
54+ ) }
55+ </ div >
56+ ) ;
57+
1858export const UpdateWebServer = ( ) => {
19- const [ updating , setUpdating ] = useState ( false ) ;
59+ const [ modalState , setModalState ] = useState < ModalState > ( "idle" ) ;
2060 const [ open , setOpen ] = useState ( false ) ;
61+ const [ healthResult , setHealthResult ] = useState < HealthResult | null > ( null ) ;
2162
2263 const { mutateAsync : updateServer } = api . settings . updateServer . useMutation ( ) ;
64+ const { refetch : checkHealth } =
65+ api . settings . checkInfrastructureHealth . useQuery ( undefined , {
66+ enabled : false ,
67+ } ) ;
68+
69+ const handleVerify = async ( ) => {
70+ setModalState ( "checking" ) ;
71+ setHealthResult ( null ) ;
72+
73+ try {
74+ const result = await checkHealth ( ) ;
75+ if ( result . data ) {
76+ setHealthResult ( result . data ) ;
77+ }
78+ } catch {
79+ // checkHealth failed entirely
80+ }
81+ setModalState ( "results" ) ;
82+ } ;
83+
84+ const allHealthy =
85+ healthResult &&
86+ healthResult . postgres . status === "healthy" &&
87+ healthResult . redis . status === "healthy" &&
88+ healthResult . traefik . status === "healthy" ;
2389
2490 const checkIsUpdateFinished = async ( ) => {
2591 try {
@@ -33,35 +99,39 @@ export const UpdateWebServer = () => {
3399 ) ;
34100
35101 setTimeout ( ( ) => {
36- // Allow seeing the toast before reloading
37102 window . location . reload ( ) ;
38103 } , 2000 ) ;
39104 } catch {
40- // Delay each request
41105 await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
42- // Keep running until it returns 200
43106 void checkIsUpdateFinished ( ) ;
44107 }
45108 } ;
46109
47110 const handleConfirm = async ( ) => {
48111 try {
49- setUpdating ( true ) ;
112+ setModalState ( "updating" ) ;
50113 await updateServer ( ) ;
51114
52- // Give some time for docker service restart before starting to check status
53115 await new Promise ( ( resolve ) => setTimeout ( resolve , 8000 ) ) ;
54116
55117 await checkIsUpdateFinished ( ) ;
56118 } catch ( error ) {
57- setUpdating ( false ) ;
119+ setModalState ( "results" ) ;
58120 console . error ( "Error updating server:" , error ) ;
59121 toast . error (
60122 "An error occurred while updating the server, please try again." ,
61123 ) ;
62124 }
63125 } ;
64126
127+ const handleClose = ( ) => {
128+ if ( modalState !== "updating" ) {
129+ setOpen ( false ) ;
130+ setModalState ( "idle" ) ;
131+ setHealthResult ( null ) ;
132+ }
133+ } ;
134+
65135 return (
66136 < AlertDialog open = { open } >
67137 < AlertDialogTrigger asChild >
@@ -81,36 +151,111 @@ export const UpdateWebServer = () => {
81151 < AlertDialogContent >
82152 < AlertDialogHeader >
83153 < AlertDialogTitle >
84- { updating
85- ? "Server update in progress"
86- : "Are you absolutely sure?" }
154+ { modalState === "idle" && "Are you absolutely sure?" }
155+ { modalState === "checking" && "Verifying Services..." }
156+ { modalState === "results" &&
157+ ( allHealthy ? "Ready to Update" : "Service Issues Detected" ) }
158+ { modalState === "updating" && "Server update in progress" }
87159 </ AlertDialogTitle >
88- < AlertDialogDescription >
89- { updating ? (
90- < span className = "flex items-center gap-1" >
91- < Loader2 className = "animate-spin" />
92- The server is being updated, please wait...
93- </ span >
94- ) : (
95- < >
96- This action cannot be undone. This will update the web server to
97- the new version. You will not be able to use the panel during
98- the update process. The page will be reloaded once the update is
99- finished.
100- </ >
101- ) }
160+ < AlertDialogDescription asChild >
161+ < div >
162+ { modalState === "idle" && (
163+ < span >
164+ This will update the web server to the new version. You will
165+ not be able to use the panel during the update process. The
166+ page will be reloaded once the update is finished.
167+ < br />
168+ < br />
169+ We recommend verifying that all services are running before
170+ updating.
171+ </ span >
172+ ) }
173+
174+ { modalState === "checking" && (
175+ < span className = "flex items-center gap-2" >
176+ < Loader2 className = "animate-spin h-4 w-4" />
177+ Checking PostgreSQL, Redis and Traefik...
178+ </ span >
179+ ) }
180+
181+ { modalState === "results" && healthResult && (
182+ < div className = "flex flex-col gap-3" >
183+ < div className = "flex flex-col gap-2" >
184+ < ServiceStatusItem
185+ name = "PostgreSQL"
186+ service = { healthResult . postgres }
187+ />
188+ < ServiceStatusItem
189+ name = "Redis"
190+ service = { healthResult . redis }
191+ />
192+ < ServiceStatusItem
193+ name = "Traefik"
194+ service = { healthResult . traefik }
195+ />
196+ </ div >
197+
198+ { ! allHealthy && (
199+ < div className = "flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3" >
200+ < AlertTriangle className = "h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
201+ < span className = "text-sm text-yellow-600 dark:text-yellow-400" >
202+ Some services are not healthy. You can still proceed
203+ with the update.
204+ </ span >
205+ </ div >
206+ ) }
207+
208+ { allHealthy && (
209+ < span className = "text-sm text-muted-foreground" >
210+ All services are running. You can proceed with the update.
211+ </ span >
212+ ) }
213+ </ div >
214+ ) }
215+
216+ { modalState === "results" && ! healthResult && (
217+ < div className = "flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3" >
218+ < AlertTriangle className = "h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
219+ < span className = "text-sm text-yellow-600 dark:text-yellow-400" >
220+ Could not verify services. You can still proceed with the
221+ update.
222+ </ span >
223+ </ div >
224+ ) }
225+
226+ { modalState === "updating" && (
227+ < span className = "flex items-center gap-2" >
228+ < Loader2 className = "animate-spin h-4 w-4" />
229+ The server is being updated, please wait...
230+ </ span >
231+ ) }
232+ </ div >
102233 </ AlertDialogDescription >
103234 </ AlertDialogHeader >
104- { ! updating && (
235+ { modalState === "idle" && (
105236 < AlertDialogFooter >
106- < AlertDialogCancel onClick = { ( ) => setOpen ( false ) } >
107- Cancel
108- </ AlertDialogCancel >
237+ < AlertDialogCancel onClick = { handleClose } > Cancel</ AlertDialogCancel >
238+ < Button variant = "secondary" onClick = { handleVerify } >
239+ < RefreshCw className = "h-4 w-4" />
240+ Verify Status
241+ </ Button >
109242 < AlertDialogAction onClick = { handleConfirm } >
110243 Confirm
111244 </ AlertDialogAction >
112245 </ AlertDialogFooter >
113246 ) }
247+ { modalState === "results" && (
248+ < AlertDialogFooter >
249+ < AlertDialogCancel onClick = { handleClose } > Cancel</ AlertDialogCancel >
250+ < Button variant = "secondary" onClick = { handleVerify } >
251+ < RefreshCw className = "h-4 w-4" />
252+ Re-check
253+ </ Button >
254+ < AlertDialogAction onClick = { handleConfirm } >
255+ { allHealthy ? "Confirm" : "Confirm Anyway" }
256+ </ AlertDialogAction >
257+ </ AlertDialogFooter >
258+ ) }
114259 </ AlertDialogContent >
115260 </ AlertDialog >
116261 ) ;
0 commit comments