From 721d1cafc444bcd268863e8e1f5b26a91cd6c9fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 19:42:54 +1100 Subject: [PATCH] fix(frontend): show upload times in the viewer's local time (#119) Migrated files and Import history showed UTC without saying so, so an upload at 19:33 AEDT read as 08:33. Both now show "27 Oct 2026, 19:33" in the browser's zone, with the exact UTC time in the tooltip. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019YPoXzBSj89ZHgw7nfTmbB --- .../src/__tests__/local-time.test.ts | 30 +++++++++++++++++++ csv-table-hub-main/src/lib/utils.ts | 23 ++++++++++++++ .../src/routes/_authenticated/index.tsx | 3 +- csv-table-hub-main/src/routes/audit.tsx | 3 +- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 csv-table-hub-main/src/__tests__/local-time.test.ts diff --git a/csv-table-hub-main/src/__tests__/local-time.test.ts b/csv-table-hub-main/src/__tests__/local-time.test.ts new file mode 100644 index 0000000..e529201 --- /dev/null +++ b/csv-table-hub-main/src/__tests__/local-time.test.ts @@ -0,0 +1,30 @@ +// src/__tests__/local-time.test.ts — upload times in the viewer's time zone (#119). +import { describe, it, expect } from "vitest"; +import { formatLocalTime, formatUtc } from "@/lib/utils"; + +describe("formatLocalTime", () => { + it("shows the #119 upload in Sydney time, not UTC", () => { + // Uploaded 27 Oct 2026 19:33 AEDT, stored as 08:33 UTC. + expect(formatLocalTime("2026-10-27T08:33:00Z", "Australia/Sydney")).toBe("27 Oct 2026, 19:33"); + }); + + it("crosses midnight correctly", () => { + expect(formatLocalTime("2026-10-26T15:16:00Z", "Australia/Sydney")).toBe("27 Oct 2026, 02:16"); + }); + + it("uses each viewer's own zone", () => { + expect(formatLocalTime("2026-10-27T08:33:00Z", "Europe/London")).toBe("27 Oct 2026, 08:33"); + }); + + it("accepts epoch milliseconds (import history)", () => { + expect(formatLocalTime(Date.UTC(2026, 9, 27, 8, 33), "Australia/Sydney")).toBe( + "27 Oct 2026, 19:33", + ); + }); +}); + +describe("formatUtc (tooltip)", () => { + it("keeps the exact UTC time for audit", () => { + expect(formatUtc("2026-10-27T08:33:05.123Z")).toBe("2026-10-27 08:33:05 UTC"); + }); +}); diff --git a/csv-table-hub-main/src/lib/utils.ts b/csv-table-hub-main/src/lib/utils.ts index a5ef193..cffef86 100644 --- a/csv-table-hub-main/src/lib/utils.ts +++ b/csv-table-hub-main/src/lib/utils.ts @@ -4,3 +4,26 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** "27 Oct 2026, 19:33" in the viewer's time zone (#119). Pass timeZone for tests. */ +export function formatLocalTime(iso: string | number, timeZone?: string): string { + const p = Object.fromEntries( + new Intl.DateTimeFormat("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hourCycle: "h23", + timeZone, + }) + .formatToParts(new Date(iso)) + .map((x) => [x.type, x.value]), + ); + return `${p.day} ${p.month} ${p.year}, ${p.hour}:${p.minute}`; +} + +/** "2026-10-27 08:33:05 UTC", kept as the tooltip for audit (#119). */ +export function formatUtc(iso: string | number): string { + return `${new Date(iso).toISOString().replace("T", " ").slice(0, 19)} UTC`; +} diff --git a/csv-table-hub-main/src/routes/_authenticated/index.tsx b/csv-table-hub-main/src/routes/_authenticated/index.tsx index b5bad0f..cfc995a 100644 --- a/csv-table-hub-main/src/routes/_authenticated/index.tsx +++ b/csv-table-hub-main/src/routes/_authenticated/index.tsx @@ -34,6 +34,7 @@ import { } from "@/lib/mapping-templates"; import { downloadErrorReport, buildErrorRows, type ExportFormat } from "@/lib/export"; import type { Job, JobStatus } from "@/lib/job-types"; +import { formatLocalTime, formatUtc } from "@/lib/utils"; import { classifyUploadResult, classifyThrownError, buildCancelPatch, buildCancelToastMessage, decideOverwriteAction, failedRowsNote, @@ -690,7 +691,7 @@ function FilesList({files}:{files:CsvFileSummary[]}){ <>
#FileTableColsRowsUploaded - {visible.map((f,i)=>{startIndex+i+1}{f.file_name}{f.table_name}{f.column_names.length}{f.row_count}{failedRowsNote(f)&&{failedRowsNote(f)}})} + {visible.map((f,i)=>{startIndex+i+1}{f.file_name}{f.table_name}{f.column_names.length}{f.row_count}{failedRowsNote(f)&&{failedRowsNote(f)}})}
diff --git a/csv-table-hub-main/src/routes/audit.tsx b/csv-table-hub-main/src/routes/audit.tsx index 65951b0..ae3fbda 100644 --- a/csv-table-hub-main/src/routes/audit.tsx +++ b/csv-table-hub-main/src/routes/audit.tsx @@ -3,6 +3,7 @@ import { createFileRoute, Link } from "@tanstack/react-router"; import { useCallback, useMemo, useState } from "react"; import { AUDIT_LOG } from "@/lib/audit-log"; +import { formatLocalTime, formatUtc } from "@/lib/utils"; import { useLocalStorageState } from "@/hooks/use-local-storage"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -171,7 +172,7 @@ function AuditPage() { {job.totalRows!=null ?
{job.totalRows} total{job.insertedRows!=null&&{job.insertedRows} ok}{(job.failedRows??0)>0&&{job.failedRows} failed}
: —}
- {new Date(job.createdAt).toISOString().replace("T"," ").slice(0,19)} UTC + {mappingSummary(job.headerMapping)||—}