diff --git a/agents/drivers/duckdb/Cargo.lock b/agents/drivers/duckdb/Cargo.lock index 54376b084e..6b6600204b 100644 --- a/agents/drivers/duckdb/Cargo.lock +++ b/agents/drivers/duckdb/Cargo.lock @@ -1252,7 +1252,6 @@ dependencies = [ "fs2", "futures", "httpdate", - "iana-time-zone", "insta", "json5", "jsonwebtoken", diff --git a/apps/desktop/src/components/grid/TemporalCellEditor.vue b/apps/desktop/src/components/grid/TemporalCellEditor.vue index 91d044a04b..d0cdbcb91c 100644 --- a/apps/desktop/src/components/grid/TemporalCellEditor.vue +++ b/apps/desktop/src/components/grid/TemporalCellEditor.vue @@ -4,7 +4,7 @@ import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui"; import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { formatTemporalInputValue, parseTemporalInputValue, stepTemporalInputValue, temporalOffsetSuffix, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor"; +import { formatTemporalInputValue, hostTimezoneOffsetSuffix, parseTemporalInputValue, stepTemporalInputValue, temporalOffsetSuffix, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor"; const props = withDefaults( defineProps<{ @@ -192,9 +192,12 @@ function setNow() { const now = new Date(); const dateText = [String(now.getFullYear()).padStart(4, "0"), String(now.getMonth() + 1).padStart(2, "0"), String(now.getDate()).padStart(2, "0")].join("-"); const nextTime = [String(now.getHours()).padStart(2, "0"), String(now.getMinutes()).padStart(2, "0"), String(now.getSeconds()).padStart(2, "0")].join(":") + nowFractionSuffix(now); + // tz-aware columns must carry an explicit offset, otherwise the server parses + // the host wall clock in its own session timezone + const offsetSuffix = temporalOffsetSuffix(localValue.value, props.kind) ? hostTimezoneOffsetSuffix(now) : ""; if (props.kind === "date") setModelValue(dateText, true); - else if (props.kind === "time") setModelValue(nextTime, true); - else setModelValue(`${dateText} ${nextTime}`, true); + else if (props.kind === "time") setModelValue(`${nextTime}${offsetSuffix}`, true); + else setModelValue(`${dateText} ${nextTime}${offsetSuffix}`, true); } function finishCommit() { diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridTemporalEditor.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridTemporalEditor.spec.ts index 7bc3245387..86fb1544f3 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridTemporalEditor.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridTemporalEditor.spec.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { formatTemporalInputValue, parseTemporalInputValue, stepTemporalInputValue, temporalCellEditorConfig, temporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { formatTemporalInputValue, hostTimezoneOffsetSuffix, parseTemporalInputValue, stepTemporalInputValue, temporalCellEditorConfig, temporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor"; describe("dataGridTemporalEditor", () => { it("resolves temporal editor configs with fractional precision", () => { @@ -54,4 +54,26 @@ describe("dataGridTemporalEditor", () => { expect(formatTemporalInputValue("2026-07-09 12:34:56", "datetime")).toBe("2026-07-09T12:34:56"); expect(parseTemporalInputValue("2026-07-09T12:34:56", "datetime")).toBe("2026-07-09 12:34:56"); }); + + it("formats the host timezone offset for tz-aware Now values", () => { + const now = new Date("2026-09-13T12:00:00Z"); + const spy = vi.spyOn(Date.prototype, "getTimezoneOffset"); + + try { + spy.mockReturnValue(-480); + expect(hostTimezoneOffsetSuffix(now)).toBe("+08:00"); + + spy.mockReturnValue(330); + expect(hostTimezoneOffsetSuffix(now)).toBe("-05:30"); + + spy.mockReturnValue(0); + expect(hostTimezoneOffsetSuffix(now)).toBe("+00:00"); + } finally { + spy.mockRestore(); + } + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); }); diff --git a/apps/desktop/src/lib/dataGrid/dataGridTemporalEditor.ts b/apps/desktop/src/lib/dataGrid/dataGridTemporalEditor.ts index 2c7199ec2e..ec7c7bc283 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridTemporalEditor.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridTemporalEditor.ts @@ -78,6 +78,13 @@ export function temporalOffsetSuffix(value: string, kind: TemporalCellEditorKind return basePattern.test(withoutOffset) ? offset : ""; } +export function hostTimezoneOffsetSuffix(now: Date): string { + const minutes = -now.getTimezoneOffset(); + const sign = minutes >= 0 ? "+" : "-"; + const abs = Math.abs(minutes); + return `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`; +} + export function stepTemporalInputValue(value: string, kind: TemporalCellEditorKind, part: TemporalCellEditorPart, delta: number): string { const dateParts = temporalDateParts(value); const timeParts = temporalTimeParts(value, kind);