diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte
index 1c822955cc..d4ebf6529a 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte
+++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte
@@ -1,4 +1,6 @@
-
+{#if date}
+
+
+
+ {#snippet child({ props })}
+
+
+
+ {/snippet}
+
+ {fullTimestamp}
+
+
+{/if}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte.test.ts
index 07b5c4e611..af53d4730b 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte.test.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/formatters/time-ago.svelte.test.ts
@@ -1,11 +1,26 @@
-import { render, screen } from '@testing-library/svelte';
+import { fireEvent, render, screen } from '@testing-library/svelte';
import { tick } from 'svelte';
import Time from 'svelte-time';
-import { afterEach, describe, expect, it, vi } from 'vitest';
+import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import TimeAgo from './time-ago.svelte';
describe('TimeAgo', () => {
+ beforeAll(() => {
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ disconnect() {}
+ observe() {}
+ unobserve() {}
+ }
+ );
+ });
+
+ afterAll(() => {
+ vi.unstubAllGlobals();
+ });
+
afterEach(() => {
vi.useRealTimers();
});
@@ -37,4 +52,38 @@ describe('TimeAgo', () => {
expect(screen.getByText('an hour ago')).toBeTruthy();
});
+
+ it('exposes the full timestamp through an accessible tooltip', async () => {
+ const value = new Date('2026-08-11T12:34:56Z');
+ const { container } = render(TimeAgo, { value });
+
+ const trigger = container.querySelector('[data-slot="tooltip-trigger"]');
+ expect(trigger).not.toBeNull();
+ await fireEvent.focus(trigger!);
+
+ const tooltip = await screen.findByRole('tooltip');
+ const expectedTimestamp = new Intl.DateTimeFormat(undefined, {
+ day: 'numeric',
+ hour: 'numeric',
+ hour12: true,
+ minute: '2-digit',
+ month: 'short',
+ second: '2-digit',
+ timeZoneName: 'short',
+ year: 'numeric'
+ }).format(value);
+
+ expect(tooltip.textContent).toContain(expectedTimestamp);
+ });
+
+ it('omits missing and invalid timestamps', () => {
+ const missing = render(TimeAgo, { value: undefined });
+ expect(missing.container.textContent).toBe('');
+ expect(missing.container.querySelector('[data-slot="tooltip-trigger"]')).toBeNull();
+ missing.unmount();
+
+ const invalid = render(TimeAgo, { value: 'not a timestamp' });
+ expect(invalid.container.textContent).toBe('');
+ expect(invalid.container.querySelector('[data-slot="tooltip-trigger"]')).toBeNull();
+ });
});
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.test.ts
index 7cbae81e50..31d9f46663 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.test.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import { formatDateLabel, getDifferenceInSeconds, getRelativeTimeFormatUnit, getSetIntervalTime } from './dates';
+import { formatDateLabel, formatDateTime, getDifferenceInSeconds, getRelativeTimeFormatUnit, getSetIntervalTime } from './dates';
describe('formatDateLabel', () => {
it('preserves local date and relative-label behavior without a timezone override', () => {
@@ -53,6 +53,21 @@ describe('formatDateLabel', () => {
});
});
+describe('formatDateTime', () => {
+ it('keeps midnight and zero seconds instead of reducing the value to a date', () => {
+ const formatted = formatDateTime(new Date(2026, 0, 2, 0, 0, 0));
+ expect(formatted).toContain('2026');
+ expect(formatted).toContain('12:00:00');
+ });
+
+ it('preserves nonzero hours, minutes, seconds and the local timezone', () => {
+ const date = new Date(2026, 0, 2, 13, 4, 5);
+ const timezone = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(date).find((part) => part.type === 'timeZoneName')!.value;
+ expect(formatDateTime(date)).toContain('1:04:05');
+ expect(formatDateTime(date)).toContain(timezone);
+ });
+});
+
const Time = {
days: (n: number) => n * 60 * 60 * 24,
hours: (n: number) => n * 60 * 60,
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.ts
index c4072edcb9..71ff580999 100644
--- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.ts
+++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.ts
@@ -131,6 +131,19 @@ export function formatDateRangeLabel(start: Date, end: Date, currentDate: Date =
return `${startLabel} to ${endLabel}`;
}
+export function formatDateTime(date: Date): string {
+ return new Intl.DateTimeFormat(undefined, {
+ day: 'numeric',
+ hour: 'numeric',
+ hour12: true,
+ minute: '2-digit',
+ month: 'short',
+ second: '2-digit',
+ timeZoneName: 'short',
+ year: 'numeric'
+ }).format(date);
+}
+
export function formatLongDate(value: Date): string {
return value.toLocaleDateString(undefined, {
day: 'numeric',