From 05c92be874b07edf9295b2636dff135f57b2b07b Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 24 Jun 2026 18:56:56 -0400 Subject: [PATCH] feat(agent-team): render all dashboard times as Eastern 24h (EDT/EST) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add src/lib/time.ts formatEastern() — parses both backend timestamp shapes (the 'YYYY-MM-DD HH:MM:SS UTC' snapshot string and ISO offset transition times), renders them in America/New_York (DST-aware: EDT/EST) as 24h 'YYYY-MM-DD HH:MM:SS ZZZ'. Wire into TopBar (snapshot clock + tooltip) and the TaskDrawer timeline (entered_at). Adds time.test.ts (8 cases incl. DST + day boundary). 44 frontend tests green. --- agent-team/web/src/components/TaskDrawer.tsx | 5 +- agent-team/web/src/components/TopBar.tsx | 8 +-- agent-team/web/src/lib/time.test.ts | 55 ++++++++++++++++++++ agent-team/web/src/lib/time.ts | 43 +++++++++++++++ 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 agent-team/web/src/lib/time.test.ts create mode 100644 agent-team/web/src/lib/time.ts diff --git a/agent-team/web/src/components/TaskDrawer.tsx b/agent-team/web/src/components/TaskDrawer.tsx index 0470e82..7a29839 100644 --- a/agent-team/web/src/components/TaskDrawer.tsx +++ b/agent-team/web/src/components/TaskDrawer.tsx @@ -12,6 +12,7 @@ import { CollapsibleContent, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; +import { formatEastern } from "@/lib/time"; interface Props { detail: TaskDetail | null; @@ -171,8 +172,8 @@ export function TaskDrawer({ detail, loading, error, onClose }: Props) {
{step.entered_at && ( - - {step.entered_at} + + {formatEastern(step.entered_at)} )} {step.status && ( diff --git a/agent-team/web/src/components/TopBar.tsx b/agent-team/web/src/components/TopBar.tsx index 738e8c2..57820f0 100644 --- a/agent-team/web/src/components/TopBar.tsx +++ b/agent-team/web/src/components/TopBar.tsx @@ -1,4 +1,5 @@ import { cn } from "@/lib/utils"; +import { formatEastern } from "@/lib/time"; import type { StateResponse } from "../api"; interface Props { @@ -50,9 +51,10 @@ const CHIPS: ChipDef[] = [ // Top status bar: brand, summary chips, connection health, and snapshot clock. export function TopBar({ state, connected, generatedAt }: Props) { const summary = state?.summary; + const snapshot = formatEastern(generatedAt); const title = connected - ? `Live — snapshot at ${generatedAt ?? "—"}` - : `Disconnected — last snapshot at ${generatedAt ?? "—"}`; + ? `Live — snapshot at ${snapshot}` + : `Disconnected — last snapshot at ${snapshot}`; return (
- {generatedAt ?? "—"} + {snapshot}
diff --git a/agent-team/web/src/lib/time.test.ts b/agent-team/web/src/lib/time.test.ts new file mode 100644 index 0000000..6a930ee --- /dev/null +++ b/agent-team/web/src/lib/time.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { formatEastern, parseServerDate } from "./time"; + +describe("formatEastern", () => { + it("formats the snapshot 'YYYY-MM-DD HH:MM:SS UTC' shape as Eastern 24h (EDT in summer)", () => { + // 22:54:48 UTC on 2026-06-24 is 18:54:48 EDT (UTC-4). + expect(formatEastern("2026-06-24 22:54:48 UTC")).toBe( + "2026-06-24 18:54:48 EDT", + ); + }); + + it("formats ISO offset timestamps as Eastern 24h", () => { + expect(formatEastern("2026-06-24T22:48:10.437876+00:00")).toBe( + "2026-06-24 18:48:10 EDT", + ); + }); + + it("uses EST in winter (DST-aware)", () => { + // 12:00 UTC on 2026-01-15 is 07:00 EST (UTC-5). + expect(formatEastern("2026-01-15 12:00:00 UTC")).toBe( + "2026-01-15 07:00:00 EST", + ); + }); + + it("crosses the day boundary correctly (late UTC -> earlier Eastern, prior day)", () => { + // 02:30 UTC on 2026-06-25 is 22:30 EDT on 2026-06-24. + expect(formatEastern("2026-06-25T02:30:00+00:00")).toBe( + "2026-06-24 22:30:00 EDT", + ); + }); + + it("returns the em-dash for null / undefined / unparseable input", () => { + expect(formatEastern(null)).toBe("—"); + expect(formatEastern(undefined)).toBe("—"); + expect(formatEastern("not a date")).toBe("—"); + }); +}); + +describe("parseServerDate", () => { + it("treats the bare 'UTC'-suffixed shape as UTC", () => { + const d = parseServerDate("2026-06-24 22:54:48 UTC"); + expect(d?.toISOString()).toBe("2026-06-24T22:54:48.000Z"); + }); + + it("parses ISO offsets", () => { + const d = parseServerDate("2026-06-24T22:48:10.000+00:00"); + expect(d?.toISOString()).toBe("2026-06-24T22:48:10.000Z"); + }); + + it("returns null for empty/unparseable input", () => { + expect(parseServerDate(null)).toBeNull(); + expect(parseServerDate("")).toBeNull(); + expect(parseServerDate("nope")).toBeNull(); + }); +}); diff --git a/agent-team/web/src/lib/time.ts b/agent-team/web/src/lib/time.ts new file mode 100644 index 0000000..ae30268 --- /dev/null +++ b/agent-team/web/src/lib/time.ts @@ -0,0 +1,43 @@ +// Time formatting for the dashboard. All backend timestamps are UTC; we render +// them in US Eastern (America/New_York — EDT in summer, EST in winter, DST-aware) +// as 24-hour `YYYY-MM-DD HH:MM:SS ZZZ`. +// +// The backend emits two shapes: +// - "2026-06-24 22:54:48 UTC" (status snapshot generated_at) +// - "2026-06-24T22:48:10.437876+00:00" (ISO transition timestamps) +// parseServerDate normalizes both to a Date; everything else is ISO that the +// JS engine parses natively. + +const EASTERN_FMT = new Intl.DateTimeFormat("en-CA", { + timeZone: "America/New_York", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", // 24-hour, 00–23 (never "24:xx") + timeZoneName: "short", +}); + +/** Parse a backend timestamp (UTC) into a Date, or null if unparseable. */ +export function parseServerDate(raw: string | null | undefined): Date | null { + if (!raw) return null; + let s = String(raw).trim(); + // "YYYY-MM-DD HH:MM:SS UTC" -> ISO "YYYY-MM-DDTHH:MM:SSZ" + if (s.endsWith(" UTC")) s = s.slice(0, -4).trim().replace(" ", "T") + "Z"; + const d = new Date(s); + return Number.isNaN(d.getTime()) ? null : d; +} + +/** + * Format a backend UTC timestamp as Eastern 24-hour time: + * "2026-06-24 18:54:48 EDT". Returns "—" for null/unparseable input. + */ +export function formatEastern(raw: string | null | undefined): string { + const d = parseServerDate(raw); + if (!d) return "—"; + const p: Record = {}; + for (const part of EASTERN_FMT.formatToParts(d)) p[part.type] = part.value; + return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}:${p.second} ${p.timeZoneName}`; +}