Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions apps/dokploy/__test__/utils/time.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
22 changes: 3 additions & 19 deletions apps/dokploy/components/ui/time-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -57,7 +41,7 @@ export function TimeBadge() {
<span className="font-medium tabular-nums">{formattedTime}</span>
</div>
<span className="hidden sm:inline text-primary/70 border rounded-full bg-foreground/5 px-1.5 py-0.5">
{serverTime.timezone} | {getUtcOffset(serverTime.timezone)}
{serverTime.timezone} | {serverTime.offset}
</span>
</div>
);
Expand Down
6 changes: 5 additions & 1 deletion apps/dokploy/server/api/routers/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
redis,
server,
} from "@/server/db/schema";
import { getUtcOffset } from "@/server/utils/time";

export const serverRouter = createTRPCRouter({
create: protectedProcedure
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions apps/dokploy/server/utils/time.ts
Original file line number Diff line number Diff line change
@@ -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";
}
}