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,69 @@ 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+ } : { name : string ; service : ServiceStatus } ) => (
42+ < div className = "flex items-center gap-2" >
43+ { service . status === "healthy" ? (
44+ < CheckCircle2 className = "h-4 w-4 text-green-500" />
45+ ) : (
46+ < XCircle className = "h-4 w-4 text-red-500" />
47+ ) }
48+ < span className = "text-sm font-medium" > { name } </ span >
49+ { service . status === "unhealthy" && service . message && (
50+ < span className = "text-xs text-muted-foreground" >
51+ — { service . message }
52+ </ span >
53+ ) }
54+ </ div >
55+ ) ;
56+
1857export const UpdateWebServer = ( ) => {
19- const [ updating , setUpdating ] = useState ( false ) ;
58+ const [ modalState , setModalState ] = useState < ModalState > ( "idle" ) ;
2059 const [ open , setOpen ] = useState ( false ) ;
60+ const [ healthResult , setHealthResult ] = useState < HealthResult | null > ( null ) ;
2161
2262 const { mutateAsync : updateServer } = api . settings . updateServer . useMutation ( ) ;
63+ const { refetch : checkHealth } =
64+ api . settings . checkInfrastructureHealth . useQuery ( undefined , {
65+ enabled : false ,
66+ } ) ;
67+
68+ const handleVerify = async ( ) => {
69+ setModalState ( "checking" ) ;
70+ setHealthResult ( null ) ;
71+
72+ try {
73+ const result = await checkHealth ( ) ;
74+ if ( result . data ) {
75+ setHealthResult ( result . data ) ;
76+ }
77+ } catch {
78+ // checkHealth failed entirely
79+ }
80+ setModalState ( "results" ) ;
81+ } ;
82+
83+ const allHealthy =
84+ healthResult &&
85+ healthResult . postgres . status === "healthy" &&
86+ healthResult . redis . status === "healthy" &&
87+ healthResult . traefik . status === "healthy" ;
2388
2489 const checkIsUpdateFinished = async ( ) => {
2590 try {
@@ -33,35 +98,39 @@ export const UpdateWebServer = () => {
3398 ) ;
3499
35100 setTimeout ( ( ) => {
36- // Allow seeing the toast before reloading
37101 window . location . reload ( ) ;
38102 } , 2000 ) ;
39103 } catch {
40- // Delay each request
41104 await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
42- // Keep running until it returns 200
43105 void checkIsUpdateFinished ( ) ;
44106 }
45107 } ;
46108
47109 const handleConfirm = async ( ) => {
48110 try {
49- setUpdating ( true ) ;
111+ setModalState ( "updating" ) ;
50112 await updateServer ( ) ;
51113
52- // Give some time for docker service restart before starting to check status
53114 await new Promise ( ( resolve ) => setTimeout ( resolve , 8000 ) ) ;
54115
55116 await checkIsUpdateFinished ( ) ;
56117 } catch ( error ) {
57- setUpdating ( false ) ;
118+ setModalState ( "results" ) ;
58119 console . error ( "Error updating server:" , error ) ;
59120 toast . error (
60121 "An error occurred while updating the server, please try again." ,
61122 ) ;
62123 }
63124 } ;
64125
126+ const handleClose = ( ) => {
127+ if ( modalState !== "updating" ) {
128+ setOpen ( false ) ;
129+ setModalState ( "idle" ) ;
130+ setHealthResult ( null ) ;
131+ }
132+ } ;
133+
65134 return (
66135 < AlertDialog open = { open } >
67136 < AlertDialogTrigger asChild >
@@ -81,36 +150,118 @@ export const UpdateWebServer = () => {
81150 < AlertDialogContent >
82151 < AlertDialogHeader >
83152 < AlertDialogTitle >
84- { updating
85- ? "Server update in progress"
86- : "Are you absolutely sure?" }
153+ { modalState === "idle" && "Are you absolutely sure?" }
154+ { modalState === "checking" && "Verifying Services..." }
155+ { modalState === "results" &&
156+ ( allHealthy
157+ ? "Ready to Update"
158+ : "Service Issues Detected" ) }
159+ { modalState === "updating" && "Server update in progress" }
87160 </ 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- ) }
161+ < AlertDialogDescription asChild >
162+ < div >
163+ { modalState === "idle" && (
164+ < span >
165+ This will update the web server to the new version. You will
166+ not be able to use the panel during the update process. The
167+ page will be reloaded once the update is finished.
168+ < br />
169+ < br />
170+ We recommend verifying that all services are running before
171+ updating.
172+ </ span >
173+ ) }
174+
175+ { modalState === "checking" && (
176+ < span className = "flex items-center gap-2" >
177+ < Loader2 className = "animate-spin h-4 w-4" />
178+ Checking PostgreSQL, Redis and Traefik...
179+ </ span >
180+ ) }
181+
182+ { modalState === "results" && healthResult && (
183+ < div className = "flex flex-col gap-3" >
184+ < div className = "flex flex-col gap-2" >
185+ < ServiceStatusItem
186+ name = "PostgreSQL"
187+ service = { healthResult . postgres }
188+ />
189+ < ServiceStatusItem
190+ name = "Redis"
191+ service = { healthResult . redis }
192+ />
193+ < ServiceStatusItem
194+ name = "Traefik"
195+ service = { healthResult . traefik }
196+ />
197+ </ div >
198+
199+ { ! allHealthy && (
200+ < div className = "flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3" >
201+ < AlertTriangle className = "h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
202+ < span className = "text-sm text-yellow-600 dark:text-yellow-400" >
203+ Some services are not healthy. You can still proceed
204+ with the update.
205+ </ span >
206+ </ div >
207+ ) }
208+
209+ { allHealthy && (
210+ < span className = "text-sm text-muted-foreground" >
211+ All services are running. You can proceed with the
212+ update.
213+ </ span >
214+ ) }
215+ </ div >
216+ ) }
217+
218+ { modalState === "results" && ! healthResult && (
219+ < div className = "flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3" >
220+ < AlertTriangle className = "h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
221+ < span className = "text-sm text-yellow-600 dark:text-yellow-400" >
222+ Could not verify services. You can still proceed with the
223+ update.
224+ </ span >
225+ </ div >
226+ ) }
227+
228+ { modalState === "updating" && (
229+ < span className = "flex items-center gap-2" >
230+ < Loader2 className = "animate-spin h-4 w-4" />
231+ The server is being updated, please wait...
232+ </ span >
233+ ) }
234+ </ div >
102235 </ AlertDialogDescription >
103236 </ AlertDialogHeader >
104- { ! updating && (
237+ { modalState === "idle" && (
105238 < AlertDialogFooter >
106- < AlertDialogCancel onClick = { ( ) => setOpen ( false ) } >
239+ < AlertDialogCancel onClick = { handleClose } >
107240 Cancel
108241 </ AlertDialogCancel >
242+ < Button variant = "secondary" onClick = { handleVerify } >
243+ < RefreshCw className = "h-4 w-4" />
244+ Verify Status
245+ </ Button >
109246 < AlertDialogAction onClick = { handleConfirm } >
110247 Confirm
111248 </ AlertDialogAction >
112249 </ AlertDialogFooter >
113250 ) }
251+ { modalState === "results" && (
252+ < AlertDialogFooter >
253+ < AlertDialogCancel onClick = { handleClose } >
254+ Cancel
255+ </ AlertDialogCancel >
256+ < Button variant = "secondary" onClick = { handleVerify } >
257+ < RefreshCw className = "h-4 w-4" />
258+ Re-check
259+ </ Button >
260+ < AlertDialogAction onClick = { handleConfirm } >
261+ { allHealthy ? "Confirm" : "Confirm Anyway" }
262+ </ AlertDialogAction >
263+ </ AlertDialogFooter >
264+ ) }
114265 </ AlertDialogContent >
115266 </ AlertDialog >
116267 ) ;
0 commit comments