Skip to content

Commit cddb06f

Browse files
committed
feat: enhance web server update process with health checks
- Added health check functionality for PostgreSQL, Redis, and Traefik services before updating the web server. - Introduced a modal state management system to guide users through the verification and update process. - Updated UI components to display service health status and relevant messages during the update workflow. - Refactored the update server button to reflect the latest version and availability of updates.
1 parent d0c92d8 commit cddb06f

3 files changed

Lines changed: 373 additions & 28 deletions

File tree

apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx

Lines changed: 178 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
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";
29
import { useState } from "react";
310
import { toast } from "sonner";
411
import {
@@ -15,11 +22,69 @@ import {
1522
import { Button } from "@/components/ui/button";
1623
import { 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+
1857
export 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
);

apps/dokploy/server/api/routers/settings.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import {
22
CLEANUP_CRON_JOB,
33
checkGPUStatus,
44
checkPortInUse,
5+
checkPostgresHealth,
6+
checkRedisHealth,
7+
checkTraefikHealth,
58
cleanupAll,
69
cleanupAllBackground,
710
cleanupBuilders,
@@ -44,8 +47,8 @@ import {
4447
writeTraefikConfigInPath,
4548
writeTraefikSetup,
4649
} from "@dokploy/server";
47-
import { checkPermission } from "@dokploy/server/services/permission";
4850
import { db } from "@dokploy/server/db";
51+
import { checkPermission } from "@dokploy/server/services/permission";
4952
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
5053
import { TRPCError } from "@trpc/server";
5154
import { eq, sql } from "drizzle-orm";
@@ -864,6 +867,23 @@ export const settingsRouter = createTRPCRouter({
864867
throw error;
865868
}
866869
}),
870+
checkInfrastructureHealth: adminProcedure.query(async () => {
871+
if (IS_CLOUD) {
872+
return {
873+
postgres: { status: "healthy" as const },
874+
redis: { status: "healthy" as const },
875+
traefik: { status: "healthy" as const },
876+
};
877+
}
878+
879+
const [postgres, redis, traefik] = await Promise.all([
880+
checkPostgresHealth(),
881+
checkRedisHealth(),
882+
checkTraefikHealth(),
883+
]);
884+
885+
return { postgres, redis, traefik };
886+
}),
867887
setupGPU: adminProcedure
868888
.input(
869889
z.object({

0 commit comments

Comments
 (0)