diff --git a/src/components/v3/app/ActivityLogPanel.tsx b/src/components/v3/app/ActivityLogPanel.tsx index db6a1f2..aa45061 100644 --- a/src/components/v3/app/ActivityLogPanel.tsx +++ b/src/components/v3/app/ActivityLogPanel.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { fetchActivity } from "@/lib/gateway/client"; import type { GatewayActivity } from "@/lib/gateway/types"; +import { formatDateTime } from "@/lib/format"; import { AccentButton, Card } from "@/components/v3/ui"; export function ActivityLogPanel() { @@ -42,20 +43,25 @@ export function ActivityLogPanel() { activity.map((a) => (
◎ -
-
{a.action}
-
- {[a.ip, a.device, a.app].filter(Boolean).join(" · ")} +
+
+
{a.action}
+
+ {[a.ip, a.device, a.app].filter(Boolean).join(" · ")} +
+
- - {new Date(a.created_at).toLocaleDateString()} -
)) )} @@ -73,4 +79,4 @@ export function ActivityLogPanel() { )} ); -} \ No newline at end of file +} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index aa3e9a9..66e587c 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi, afterEach } from "vitest"; import { clientActivity, formatBytes, + formatDateTime, isRecentTimestamp, timestampMs, } from "./format"; @@ -42,6 +43,20 @@ describe("timestampMs", () => { }); }); +describe("formatDateTime", () => { + it("includes the clock time for ISO timestamps", () => { + expect( + formatDateTime("2026-08-28T14:35:00.000Z", "en-US", { + timeZone: "UTC", + }) + ).toBe("Aug 28, 2026, 2:35 PM"); + }); + + it("returns a placeholder for missing timestamps", () => { + expect(formatDateTime("")).toBe("—"); + }); +}); + describe("time-window helpers", () => { afterEach(() => vi.useRealTimers()); diff --git a/src/lib/format.ts b/src/lib/format.ts index ca54fe2..97c97e9 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -41,6 +41,24 @@ export function formatRelativeTime(input?: string | number | null): string { return `${day}d ago`; } +/** Locale-aware date + time for audit/activity timestamps. */ +export function formatDateTime( + input?: string | number | null, + locale?: Intl.LocalesArgument, + options: Intl.DateTimeFormatOptions = {} +): string { + const ms = timestampMs(input); + if (ms == null) return "—"; + return new Intl.DateTimeFormat(locale, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + ...options, + }).format(new Date(ms)); +} + /** Pick label + timestamp for node liveness UI (matches VPN app logic). */ export function nodeActivityDisplay( node: {