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