From 513141a2186a1ed27e2fb171430d99c7c04e6a43 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 3 Sep 2026 17:46:33 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Age-adaptive=20timestamps=20wit?= =?UTF-8?q?h=20a=20relative=20label,=20and=20when=20the=20query=20ran?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time Sent adapts to age: time only for today, "yesterday 14:32" and weekday names for the past week, the regular browser format beyond, with a live coarse relative label and the full timestamp in both zones as tooltip. A "Times: local|UTC" toggle on the results line flips every timestamp to UTC wall-clock time (day boundaries included), persisted per browser. The results line also shows when the current results were fetched ("ran 3 minutes ago", exact timestamp as tooltip); that is forgotten with the results when the view is left. --- .../src/components/AdaptiveTimestamp.spec.ts | 77 +++++++++++++++++++ .../src/components/AdaptiveTimestamp.vue | 35 +++++++++ .../src/components/ResultsCount.spec.ts | 9 +++ src/Frontend/src/components/ResultsCount.vue | 20 ++++- .../src/components/audit/AuditList.vue | 4 +- .../src/components/audit/AuditListItem.vue | 3 +- .../src/components/audit/ResultsOptions.vue | 18 +++++ src/Frontend/src/composables/dateFormatter.ts | 32 ++++++++ .../src/composables/timestampZone.spec.ts | 38 +++++++++ src/Frontend/src/composables/timestampZone.ts | 33 ++++++++ src/Frontend/src/stores/AuditStore.spec.ts | 11 +++ src/Frontend/src/stores/AuditStore.ts | 4 + 12 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 src/Frontend/src/components/AdaptiveTimestamp.spec.ts create mode 100644 src/Frontend/src/components/AdaptiveTimestamp.vue create mode 100644 src/Frontend/src/composables/timestampZone.spec.ts create mode 100644 src/Frontend/src/composables/timestampZone.ts diff --git a/src/Frontend/src/components/AdaptiveTimestamp.spec.ts b/src/Frontend/src/components/AdaptiveTimestamp.spec.ts new file mode 100644 index 0000000000..2d37e1953b --- /dev/null +++ b/src/Frontend/src/components/AdaptiveTimestamp.spec.ts @@ -0,0 +1,77 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from "vitest"; +import { cleanup, render, screen } from "@testing-library/vue"; +import AdaptiveTimestamp from "@/components/AdaptiveTimestamp.vue"; +import { useTimestampZone } from "@/composables/timestampZone"; + +const NOW = new Date("2026-09-03T15:00:00"); + +function renderAt(date: Date) { + cleanup(); + render(AdaptiveTimestamp, { props: { dateUtc: date.toISOString() } }); + return { + absolute: screen.getByTestId("adaptive-absolute").textContent!, + relative: screen.getByTestId("adaptive-relative").textContent!, + }; +} + +describe("FEATURE: Age-adaptive timestamps", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(NOW); + }); + + afterEach(() => { + vi.useRealTimers(); + useTimestampZone().zone.value = "local"; + }); + + test("EXAMPLE: Today shows the time only", () => { + const date = new Date("2026-09-03T13:00:00"); + const { absolute, relative } = renderAt(date); + + expect(absolute).toBe(date.toLocaleTimeString()); + expect(relative).toContain("2 hours ago"); + }); + + test("EXAMPLE: Yesterday is labeled yesterday", () => { + const date = new Date("2026-09-02T13:00:00"); + const { absolute, relative } = renderAt(date); + + expect(absolute).toBe(`yesterday ${date.toLocaleTimeString()}`); + expect(relative).toContain("1 day ago"); + }); + + test("EXAMPLE: Within the past week the weekday is named", () => { + const date = new Date("2026-08-31T13:00:00"); // Monday + const { absolute, relative } = renderAt(date); + + expect(absolute).toBe(`${date.toLocaleDateString(undefined, { weekday: "long" })} ${date.toLocaleTimeString()}`); + expect(relative).toContain("3 days ago"); + }); + + test("EXAMPLE: Older dates keep the regular browser format", () => { + const date = new Date("2026-08-20T13:00:00"); + const { absolute, relative } = renderAt(date); + + expect(absolute).toBe(date.toLocaleString()); + expect(relative).toContain("2 weeks ago"); + }); + + test("EXAMPLE: In UTC mode the wall-clock time is UTC", () => { + const { zone } = useTimestampZone(); + zone.value = "utc"; + + const date = new Date("2026-09-03T13:00:00"); + const utcWall = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); + + // toContain: near the UTC midnight boundary the day prefix may differ from local mode + expect(renderAt(date).absolute).toContain(utcWall.toLocaleTimeString()); + }); + + test("EXAMPLE: The coarse relative label covers moments to years", () => { + expect(renderAt(new Date("2026-09-03T14:59:50")).relative).toContain("moments ago"); + expect(renderAt(new Date("2026-09-03T14:30:00")).relative).toContain("30 minutes ago"); + expect(renderAt(new Date("2026-06-03T15:00:00")).relative).toContain("3 months ago"); + expect(renderAt(new Date("2024-09-03T15:00:00")).relative).toContain("2 years ago"); + }); +}); diff --git a/src/Frontend/src/components/AdaptiveTimestamp.vue b/src/Frontend/src/components/AdaptiveTimestamp.vue new file mode 100644 index 0000000000..b66fb08c20 --- /dev/null +++ b/src/Frontend/src/components/AdaptiveTimestamp.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/Frontend/src/components/ResultsCount.spec.ts b/src/Frontend/src/components/ResultsCount.spec.ts index 20e695c3b1..84255aaf82 100644 --- a/src/Frontend/src/components/ResultsCount.spec.ts +++ b/src/Frontend/src/components/ResultsCount.spec.ts @@ -23,6 +23,15 @@ describe("FEATURE: Results count", () => { expect(screen.getByText("Showing 10 of 10 result(s) · took 320 ms")).toBeInTheDocument(); }); + test("EXAMPLE: When the query ran is shown relatively with the timestamp as tooltip", () => { + const completedAt = new Date(Date.now() - 3 * 60 * 1000).toISOString(); + render(ResultsCount, { props: { displayed: 10, total: 10, durationMs: 320, completedAt } }); + + const ran = screen.getByTestId("ran-ago"); + expect(ran.textContent).toContain("minutes ago"); + expect(ran.getAttribute("title")).toContain("(UTC)"); + }); + test("EXAMPLE: Zero results render plainly", () => { render(ResultsCount, { props: { displayed: 0, total: 0 } }); diff --git a/src/Frontend/src/components/ResultsCount.vue b/src/Frontend/src/components/ResultsCount.vue index c5eea31bca..5a90be26ff 100644 --- a/src/Frontend/src/components/ResultsCount.vue +++ b/src/Frontend/src/components/ResultsCount.vue @@ -1,12 +1,25 @@