diff --git a/apps/dokploy/__test__/utils/time.test.ts b/apps/dokploy/__test__/utils/time.test.ts
new file mode 100644
index 0000000000..834df1b4f7
--- /dev/null
+++ b/apps/dokploy/__test__/utils/time.test.ts
@@ -0,0 +1,40 @@
+import { getUtcOffset } from "@/server/utils/time";
+import { describe, expect, test } from "vitest";
+
+describe("getUtcOffset", () => {
+ test("should return correct offset for major timezones", () => {
+ expect(getUtcOffset("UTC")).toBe("UTC+00:00");
+ expect(getUtcOffset("Etc/UTC")).toBe("UTC+00:00");
+ expect(getUtcOffset("Asia/Tokyo")).toBe("UTC+09:00");
+ expect(getUtcOffset("Europe/Berlin")).toMatch(/UTC\+0[12]:00/);
+ });
+
+ test("should return correct offset for negative timezones", () => {
+ expect(getUtcOffset("America/New_York")).toMatch(/UTC-0[45]:00/);
+ expect(getUtcOffset("America/Los_Angeles")).toMatch(/UTC-0[78]:00/);
+ expect(getUtcOffset("Pacific/Honolulu")).toBe("UTC-10:00");
+ });
+
+ test("should handle half-hour and quarter-hour offsets", () => {
+ expect(getUtcOffset("Asia/Kolkata")).toBe("UTC+05:30");
+ expect(getUtcOffset("Asia/Kathmandu")).toBe("UTC+05:45");
+ expect(getUtcOffset("Pacific/Marquesas")).toBe("UTC-09:30");
+ });
+
+ test("should handle edge case timezones", () => {
+ expect(getUtcOffset("Pacific/Kiritimati")).toBe("UTC+14:00");
+ expect(getUtcOffset("Pacific/Niue")).toBe("UTC-11:00");
+ });
+
+ test("should return fallback for invalid timezone", () => {
+ expect(getUtcOffset("Invalid/Timezone")).toBe("UTC+00:00");
+ expect(getUtcOffset("")).toBe("UTC+00:00");
+ expect(getUtcOffset("NotATimezone")).toBe("UTC+00:00");
+ });
+
+ test("should format output consistently", () => {
+ const offset = getUtcOffset("Asia/Tokyo");
+ expect(offset).toMatch(/^UTC[+-]\d{2}:\d{2}$/);
+ expect(offset).not.toContain("GMT");
+ });
+});
diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx
index 4cf778f252..c8e0d034a5 100644
--- a/apps/dokploy/components/ui/time-badge.tsx
+++ b/apps/dokploy/components/ui/time-badge.tsx
@@ -17,33 +17,17 @@ export function TimeBadge() {
const timer = setInterval(() => {
setTime((prevTime) => {
if (!prevTime) return null;
- const newTime = new Date(prevTime.getTime() + 1000);
- return newTime;
+ return new Date(prevTime.getTime() + 1000);
});
}, 1000);
- return () => {
- clearInterval(timer);
- };
+ return () => clearInterval(timer);
}, []);
if (!time || !serverTime?.timezone) {
return null;
}
- const getUtcOffset = (timeZone: string) => {
- const date = new Date();
- const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
- const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
- const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
- const sign = offset >= 0 ? "+" : "-";
- const hours = Math.floor(Math.abs(offset));
- const minutes = (Math.abs(offset) * 60) % 60;
- return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
- .toString()
- .padStart(2, "0")}`;
- };
-
const formattedTime = new Intl.DateTimeFormat("en-US", {
timeZone: serverTime.timezone,
timeStyle: "medium",
@@ -57,7 +41,7 @@ export function TimeBadge() {
{formattedTime}
- {serverTime.timezone} | {getUtcOffset(serverTime.timezone)}
+ {serverTime.timezone} | {serverTime.offset}
);
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index 4a044ec547..39269f9d13 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -38,6 +38,7 @@ import {
redis,
server,
} from "@/server/db/schema";
+import { getUtcOffset } from "@/server/utils/time";
export const serverRouter = createTRPCRouter({
create: protectedProcedure
@@ -409,9 +410,12 @@ export const serverRouter = createTRPCRouter({
if (IS_CLOUD) {
return null;
}
+
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return {
time: new Date(),
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ timezone,
+ offset: getUtcOffset(timezone),
};
}),
getServerMetrics: protectedProcedure
diff --git a/apps/dokploy/server/utils/time.ts b/apps/dokploy/server/utils/time.ts
new file mode 100644
index 0000000000..f4a4610115
--- /dev/null
+++ b/apps/dokploy/server/utils/time.ts
@@ -0,0 +1,24 @@
+/**
+ * Get UTC offset string for a given IANA timezone
+ * @param timeZone - IANA timezone identifier (e.g., "America/New_York", "Asia/Tokyo")
+ * @returns Formatted offset string (e.g., "UTC+09:00", "UTC-05:00")
+ */
+export function getUtcOffset(timeZone: string): string {
+ try {
+ const formatter = new Intl.DateTimeFormat("en-US", {
+ timeZone,
+ timeZoneName: "longOffset",
+ });
+ const parts = formatter.formatToParts(new Date());
+ const offsetPart = parts.find((p) => p.type === "timeZoneName");
+ const offset = offsetPart?.value;
+
+ if (!offset || offset === "GMT") {
+ return "UTC+00:00";
+ }
+
+ return offset.replace("GMT", "UTC");
+ } catch (error) {
+ return "UTC+00:00";
+ }
+}