authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-07-17 23:24:10+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-17 23:24:10+02:00
logb655a9f183cc9f6b2fd54bf11ea28acb50d23cfd
tree6775a41fbe63c65b3ef2bd5b11fa87a8b5589df6
parent4e3f2f04d577152672110303b432a72218fe4e61
signature Signed by PGP key B5690EEEBB952194

[codex] fix: guard invalid web timestamps (#3515)

Co-authored-by: Codex <codex@openai.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

4 files changed, 117 insertions(+), 18 deletions(-)

apps/web/src/components/settings/DiagnosticsSettings.tsx+8-5
......@@ -23,7 +23,7 @@ import * as Option from "effect/Option";
2323
2424import { cn } from "../../lib/utils";
2525import { resolveAndPersistPreferredEditor } from "../../editorPreferences";
26import { formatRelativeTime } from "../../timestampFormat";
26import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat";
2727import { useEnvironmentQuery } from "../../state/query";
2828import {
2929 primaryServerAvailableEditorsAtom,
......@@ -65,8 +65,7 @@ function formatBytes(value: number): string {
6565
6666function formatRelative(value: DateTime.Utc | null): string {
6767 if (!value) return "No trace records";
68 const relative = formatRelativeTime(DateTime.formatIso(value));
69 return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
68 return formatRelativeTimeLabel(DateTime.formatIso(value));
7069}
7170
7271function formatRelativeNoWrap(value: DateTime.Utc | null): string {
......@@ -755,12 +754,16 @@ function ProcessResourceHistoryTable({
755754
756755function DiagnosticsLastChecked({ checkedAt }: { checkedAt: DateTime.Utc | null }) {
757756 useRelativeTimeTick();
758 const relative = checkedAt ? formatRelativeTime(DateTime.formatIso(checkedAt)) : null;
757 const relative = getRelativeTimeState(checkedAt ? DateTime.formatIso(checkedAt) : null);
759758
760 if (!relative) {
759 if (relative.status === "missing") {
761760 return <span className="text-[11px] text-muted-foreground/50">Checking</span>;
762761 }
763762
763 if (relative.status === "invalid") {
764 return <span className="text-[11px] text-muted-foreground/50">Checked unavailable</span>;
765 }
766
764767 return (
765768 <span className="text-[11px] text-muted-foreground/60">
766769 {relative.suffix ? (
apps/web/src/components/settings/SettingsPanels.tsx+7-3
......@@ -58,7 +58,7 @@ import {
5858import { usePrimaryEnvironment } from "../../state/environments";
5959import { useProjects } from "../../state/entities";
6060import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";
61import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat";
61import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat";
6262import { Button } from "../ui/button";
6363import { DraftInput } from "../ui/draft-input";
6464import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
......@@ -134,12 +134,16 @@ const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({
134134
135135function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) {
136136 useRelativeTimeTick();
137 const lastCheckedRelative = lastCheckedAt ? formatRelativeTime(lastCheckedAt) : null;
137 const lastCheckedRelative = getRelativeTimeState(lastCheckedAt);
138138
139 if (!lastCheckedRelative) {
139 if (lastCheckedRelative.status === "missing") {
140140 return null;
141141 }
142142
143 if (lastCheckedRelative.status === "invalid") {
144 return <span className="text-[11px] text-muted-foreground/50">Checked unavailable</span>;
145 }
146
143147 return (
144148 <span className="text-[11px] text-muted-foreground/60">
145149 {lastCheckedRelative.suffix ? (
apps/web/src/timestampFormat.test.ts+60
......@@ -3,7 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
33import {
44 formatElapsedDurationLabel,
55 formatExpiresInLabel,
6 formatRelativeTime,
7 formatRelativeTimeLabel,
8 formatRelativeTimeUntil,
69 formatRelativeTimeUntilLabel,
10 formatShortTimestamp,
11 formatTimestamp,
12 getRelativeTimeState,
713 getTimestampFormatOptions,
814} from "./timestampFormat";
915
......@@ -90,6 +96,60 @@ describe("formatExpiresInLabel", () => {
9096 });
9197});
9298
99describe("invalid timestamp inputs", () => {
100 it("returns an empty timestamp instead of throwing", () => {
101 expect(() => formatTimestamp("not-a-date", "12-hour")).not.toThrow();
102 expect(formatTimestamp("not-a-date", "12-hour")).toBe("");
103 });
104
105 it("returns an empty short timestamp instead of throwing", () => {
106 expect(() => formatShortTimestamp("not-a-date", "12-hour")).not.toThrow();
107 expect(formatShortTimestamp("not-a-date", "12-hour")).toBe("");
108 });
109
110 it("returns an empty relative time label instead of a NaN label", () => {
111 expect(formatRelativeTime("not-a-date")).toBeNull();
112 expect(formatRelativeTimeLabel("not-a-date")).toBe("");
113 });
114
115 it("distinguishes missing and invalid relative time state", () => {
116 expect(getRelativeTimeState(null)).toEqual({ status: "missing" });
117 expect(getRelativeTimeState("not-a-date")).toEqual({ status: "invalid" });
118 });
119
120 it("returns an empty elapsed duration instead of a NaN label", () => {
121 expect(formatElapsedDurationLabel("not-a-date")).toBe("");
122 });
123
124 it("returns an empty relative time until label instead of a NaN label", () => {
125 expect(formatRelativeTimeUntil("not-a-date")).toBeNull();
126 expect(formatRelativeTimeUntilLabel("not-a-date")).toBe("");
127 });
128
129 it("returns an empty expires-in label instead of a NaN label", () => {
130 expect(formatExpiresInLabel("not-a-date")).toBe("");
131 });
132});
133
134describe("getRelativeTimeState", () => {
135 beforeEach(() => {
136 vi.useFakeTimers();
137 vi.setSystemTime(new Date("2026-04-07T12:00:00.000Z"));
138 });
139
140 afterEach(() => {
141 vi.useRealTimers();
142 });
143
144 it("returns relative parts for valid timestamps", () => {
145 expect(getRelativeTimeState("2026-04-07T11:45:00.000Z")).toEqual({
146 status: "relative",
147 value: "15m",
148 suffix: "ago",
149 });
150 });
151});
152
93153describe("formatElapsedDurationLabel", () => {
94154 beforeEach(() => {
95155 vi.useFakeTimers();
apps/web/src/timestampFormat.ts+42-10
......@@ -40,8 +40,15 @@ function getTimestampFormatter(
4040 return formatter;
4141}
4242
43function parseTimestampDate(isoDate: string): Date | null {
44 const date = new Date(isoDate);
45 return Number.isNaN(date.getTime()) ? null : date;
46}
47
4348export function formatTimestamp(isoDate: string, timestampFormat: TimestampFormat): string {
44 return getTimestampFormatter(timestampFormat, true).format(new Date(isoDate));
49 const date = parseTimestampDate(isoDate);
50 if (!date) return "";
51 return getTimestampFormatter(timestampFormat, true).format(date);
4552}
4653
4754const monthNameFormatter = new Intl.DateTimeFormat(undefined, { month: "long" });
......@@ -69,8 +76,8 @@ export function formatChatTimestampTooltip(
6976 isoDate: string,
7077 timestampFormat: TimestampFormat,
7178): string {
72 const date = new Date(isoDate);
73 if (Number.isNaN(date.getTime())) return "";
79 const date = parseTimestampDate(isoDate);
80 if (!date) return "";
7481 const time = formatShortTimestamp(isoDate, timestampFormat);
7582 const day = date.getDate();
7683 const month = monthNameFormatter.format(date);
......@@ -79,7 +86,9 @@ export function formatChatTimestampTooltip(
7986}
8087
8188export function formatShortTimestamp(isoDate: string, timestampFormat: TimestampFormat): string {
82 return getTimestampFormatter(timestampFormat, false).format(new Date(isoDate));
89 const date = parseTimestampDate(isoDate);
90 if (!date) return "";
91 return getTimestampFormatter(timestampFormat, false).format(date);
8392}
8493
8594/**
......@@ -87,8 +96,16 @@ export function formatShortTimestamp(isoDate: string, timestampFormat: Timestamp
8796 * Returns `{ value: "20s", suffix: "ago" }` or `{ value: "just now", suffix: null }`
8897 * so callers can style the numeric portion independently.
8998 */
90export function formatRelativeTime(isoDate: string): { value: string; suffix: string | null } {
91 const diffMs = Date.now() - new Date(isoDate).getTime();
99type RelativeTimeParts = { value: string; suffix: string | null };
100export type RelativeTimeState =
101 | { status: "missing" }
102 | { status: "invalid" }
103 | { status: "relative"; value: string; suffix: string | null };
104
105export function formatRelativeTime(isoDate: string): RelativeTimeParts | null {
106 const date = parseTimestampDate(isoDate);
107 if (!date) return null;
108 const diffMs = Date.now() - date.getTime();
92109 if (diffMs < 0) return { value: "just now", suffix: null };
93110 const seconds = Math.floor(diffMs / 1000);
94111 if (seconds < 60) return { value: "just now", suffix: null };
......@@ -102,15 +119,25 @@ export function formatRelativeTime(isoDate: string): { value: string; suffix: st
102119
103120export function formatRelativeTimeLabel(isoDate: string) {
104121 const relative = formatRelativeTime(isoDate);
122 if (!relative) return "";
105123 return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
106124}
107125
126export function getRelativeTimeState(isoDate: string | null): RelativeTimeState {
127 if (!isoDate) return { status: "missing" };
128 const relative = formatRelativeTime(isoDate);
129 if (!relative) return { status: "invalid" };
130 return { status: "relative", ...relative };
131}
132
108133/**
109134 * Relative elapsed duration since an ISO instant, without an "ago" suffix.
110135 * Useful for labels like "Connected for 3m".
111136 */
112137export function formatElapsedDurationLabel(isoDate: string, nowMs: number = Date.now()): string {
113 const diffMs = nowMs - new Date(isoDate).getTime();
138 const date = parseTimestampDate(isoDate);
139 if (!date) return "";
140 const diffMs = nowMs - date.getTime();
114141 if (diffMs <= 0) return "just now";
115142
116143 const seconds = Math.floor(diffMs / 1000);
......@@ -130,8 +157,10 @@ export function formatElapsedDurationLabel(isoDate: string, nowMs: number = Date
130157/**
131158 * Relative time until an ISO instant (e.g. expiry). Mirrors {@link formatRelativeTime} but for future times.
132159 */
133export function formatRelativeTimeUntil(isoDate: string): { value: string; suffix: string | null } {
134 const diffMs = new Date(isoDate).getTime() - Date.now();
160export function formatRelativeTimeUntil(isoDate: string): RelativeTimeParts | null {
161 const date = parseTimestampDate(isoDate);
162 if (!date) return null;
163 const diffMs = date.getTime() - Date.now();
135164 if (diffMs <= 0) return { value: "Expired", suffix: null };
136165 const seconds = Math.floor(diffMs / 1000);
137166 if (seconds < 5) return { value: "Soon", suffix: null };
......@@ -146,6 +175,7 @@ export function formatRelativeTimeUntil(isoDate: string): { value: string; suffi
146175
147176export function formatRelativeTimeUntilLabel(isoDate: string): string {
148177 const relative = formatRelativeTimeUntil(isoDate);
178 if (!relative) return "";
149179 return relative.suffix ? `${relative.value} ${relative.suffix}` : relative.value;
150180}
151181
......@@ -154,7 +184,9 @@ export function formatRelativeTimeUntilLabel(isoDate: string): string {
154184 * Pass `nowMs` when a parent tick drives re-renders so the diff matches that snapshot.
155185 */
156186export function formatExpiresInLabel(isoDate: string, nowMs: number = Date.now()): string {
157 const diffMs = new Date(isoDate).getTime() - nowMs;
187 const date = parseTimestampDate(isoDate);
188 if (!date) return "";
189 const diffMs = date.getTime() - nowMs;
158190 if (diffMs <= 0) return "Expired";
159191
160192 const totalSeconds = Math.floor(diffMs / 1000);