diff --git a/src/components/settings/__tests__/environment-section-dates.test.tsx b/src/components/settings/__tests__/environment-section-dates.test.tsx new file mode 100644 index 000000000..a37b6c670 --- /dev/null +++ b/src/components/settings/__tests__/environment-section-dates.test.tsx @@ -0,0 +1,58 @@ +/** + * The environment settings default their date fields to today. "Today" is + * the user's day: the UTC day was yesterday west of UTC in the evening and + * tomorrow east of it in the small hours. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { I18nProvider } from "@/lib/i18n/context"; + +const auth = vi.hoisted(() => ({ timezone: "America/Los_Angeles" })); +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { timezone: auth.timezone, modules: { environment: true } }, + isAuthenticated: true, + }), +})); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + usePathname: () => "/settings/environment", + useSearchParams: () => new URLSearchParams(""), +})); + +import { EnvironmentSection } from "../environment-section"; + +function backfillEnd(): string | undefined { + const client = new QueryClient({ defaultOptions: { queries: { retry: 0 } } }); + const html = renderToStaticMarkup( + + + + + , + ); + return /id="env-backfill-end"[^>]*value="([^"]*)"/.exec(html)?.[1]; +} + +beforeEach(() => { + vi.useFakeTimers({ toFake: ["Date"] }); +}); +afterEach(() => { + vi.useRealTimers(); +}); + +describe(" date defaults", () => { + it("defaults to the user's day west of UTC", () => { + auth.timezone = "America/Los_Angeles"; + vi.setSystemTime(new Date("2026-07-03T03:00:00.000Z")); // 2 July, 20:00 + expect(backfillEnd()).toBe("2026-07-02"); + }); + + it("defaults to the user's day east of UTC", () => { + auth.timezone = "Pacific/Kiritimati"; + vi.setSystemTime(new Date("2026-07-02T12:00:00.000Z")); // 3 July, 02:00 + expect(backfillEnd()).toBe("2026-07-03"); + }); +}); diff --git a/src/components/settings/environment-section.tsx b/src/components/settings/environment-section.tsx index 0a9de3eb6..166500a69 100644 --- a/src/components/settings/environment-section.tsx +++ b/src/components/settings/environment-section.tsx @@ -32,6 +32,12 @@ import { formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { apiDelete, apiGet, apiPost, apiPut } from "@/lib/api/api-fetch"; import { queryKeys } from "@/lib/query-keys"; +import { + DEFAULT_TIMEZONE, + detectBrowserTimezone, + userDayKey, + validTimezoneOr, +} from "@/lib/tz/format"; interface GeocodeResult { lat: number; @@ -72,14 +78,18 @@ interface EnvironmentOverview { attribution: string; } -function todayKey(): string { - return new Date().toISOString().slice(0, 10); -} - export function EnvironmentSection() { const { t, tCount } = useTranslations(); const { user } = useAuth(); const queryClient = useQueryClient(); + // Date defaults are the user's own days. The UTC day was tomorrow for + // anyone east of UTC in the small hours and yesterday west of it in the + // evening. + const tz = validTimezoneOr( + user?.timezone ?? detectBrowserTimezone(), + DEFAULT_TIMEZONE, + ); + const todayKey = () => userDayKey(new Date(), tz); const enabled = user?.modules?.environment === true; @@ -217,7 +227,7 @@ export function EnvironmentSection() { // user can still type an earlier start, but pre-home days resolve to SKIP on // the server — to fill the deep past they add an explicit location period. const effectiveBackfillStart = - backfillStart || home?.since?.slice(0, 10) || ""; + backfillStart || (home?.since ? userDayKey(new Date(home.since), tz) : ""); return (