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"; + } +}