flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/status/src/time.ts

79 lines3,075 bytesCodeBlame
1/**
2 * Times in the reader's own zone. The zone comes from Cloudflare's guess
3 * for the request (`request.cf.timezone`), or a `g1t_tz` cookie when the
4 * reader has one; anything unknown is UTC. Every time shown carries its
5 * zone's abbreviation, and `<time datetime>` keeps the UTC instant. No
6 * Workers imports, so it is tested under Node.
7 */
8
9export const ZONE_COOKIE = "g1t_tz";
10
11const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
12
13/** Whether `tz` is a time zone this runtime knows. */
14export function validZone(tz: unknown): tz is string {
15 if (typeof tz !== "string" || !tz || tz.length > 64) return false;
16 try {
17 new Intl.DateTimeFormat("en-US", { timeZone: tz });
18 return true;
19 } catch {
20 return false;
21 }
22}
23
24function cookie(header: string | null | undefined, name: string): string | null {
25 for (const part of (header ?? "").split(";")) {
26 const [k, ...v] = part.trim().split("=");
27 if (k === name) {
28 try {
29 return decodeURIComponent(v.join("="));
30 } catch {
31 return null;
32 }
33 }
34 }
35 return null;
36}
37
38/** The reader's zone: their cookie, else Cloudflare's guess, else UTC. */
39export function readZone(cookieHeader: string | null | undefined, cfTimezone: unknown): string {
40 const chosen = cookie(cookieHeader, ZONE_COOKIE);
41 if (validZone(chosen)) return chosen;
42 return validZone(cfTimezone) ? cfTimezone : "UTC";
43}
44
45/** The zone's short name at that moment: "PDT", "CEST", "BST", or "UTC+9" where there is no common one. */
46export function zoneAbbr(date: Date, tz: string): string {
47 if (tz === "UTC" || tz === "Etc/UTC" || tz === "Etc/GMT") return "UTC";
48 let offset = "";
49 for (const locale of ["en-US", "en-GB"]) {
50 const name = new Intl.DateTimeFormat(locale, { timeZone: tz, timeZoneName: "short" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value ?? "";
51 if (name && !/^(GMT|UTC)[+-−]/.test(name)) return name;
52 offset ||= name;
53 }
54 return offset.replace(/^GMT/, "UTC") || "UTC";
55}
56
57/** The wall-clock parts of a moment in a zone. */
58function parts(date: Date, tz: string) {
59 const got = Object.fromEntries(
60 new Intl.DateTimeFormat("en-US", { timeZone: tz, year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", hourCycle: "h23" })
61 .formatToParts(date)
62 .map((p) => [p.type, p.value]),
63 );
64 return { year: Number(got.year), month: Number(got.month), day: Number(got.day), hh: String(got.hour).padStart(2, "0"), mm: got.minute };
65}
66
67/** "6 Oct 2026, 00:25 PDT". */
68export function timeIn(at: string | Date, tz = "UTC"): string {
69 const date = new Date(at);
70 const p = parts(date, tz);
71 return `${p.day} ${MONTHS[p.month - 1]} ${p.year}, ${p.hh}:${p.mm} ${zoneAbbr(date, tz)}`;
72}
73
74/** "6 Oct 00:25 PDT": shorter, for lines that already say the year. */
75export function stampIn(at: string | Date, tz = "UTC"): string {
76 const date = new Date(at);
77 const p = parts(date, tz);
78 return `${p.day} ${MONTHS[p.month - 1]} ${p.hh}:${p.mm} ${zoneAbbr(date, tz)}`;
79}