diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b5f30..b51f94f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 sequence numbers, followed by a lightweight `FETCH (UID)` to resolve them to stable UIDs; all subsequent operations (`FETCH`, `STORE`) continue to use the UID form. +- **Dashboard: fix UTC hour-shift on "Last check" timestamps** — the dashboard + page was using `new Date(iso)` which treats timezone-naive ISO strings from + the backend as local time, shifting relative labels (e.g. "1h ago" instead + of "Just now") for users outside UTC. The dashboard now uses the same + `parseUTC()` helper that the logs page already applied. The helper functions + `formatRelative`, `formatDate`, and `formatDuration` have been consolidated + into `src/lib/date-utils.ts` and are imported by all pages. ## v0.4.2 (2026-03-28) diff --git a/frontend/src/app/admin/logs/page.tsx b/frontend/src/app/admin/logs/page.tsx index a614da9..80832e0 100644 --- a/frontend/src/app/admin/logs/page.tsx +++ b/frontend/src/app/admin/logs/page.tsx @@ -8,7 +8,7 @@ import { useAuthStore } from '@/store/authStore'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; import { adminApi, AdminProcessingRun } from '@/lib/api'; -import { parseUTC } from '@/lib/date-utils'; +import { formatDate, formatDuration } from '@/lib/date-utils'; import { Activity, ChevronLeft, @@ -25,19 +25,6 @@ const STATUS_STYLES: Record = { running: 'bg-blue-100 text-blue-800', }; -function formatDuration(seconds?: number | null): string { - if (seconds == null) return '—'; - if (seconds < 60) return `${seconds.toFixed(1)}s`; - return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; -} - -function formatDate(iso: string): string { - return parseUTC(iso).toLocaleString(undefined, { - dateStyle: 'short', - timeStyle: 'medium', - }); -} - export default function AdminLogsPage() { const { user } = useAuthStore(); const router = useRouter(); diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 5e5f74e..47c6c8e 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -4,6 +4,7 @@ import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; import { useQuery } from '@tanstack/react-query'; import { mailAccountsApi, MailAccount } from '@/lib/api'; +import { formatRelative } from '@/lib/date-utils'; import Link from 'next/link'; import { Mail, @@ -16,17 +17,6 @@ import { Inbox, } from 'lucide-react'; -function formatRelative(iso?: string | null): string { - if (!iso) return 'Never'; - const diff = Date.now() - new Date(iso).getTime(); - const minutes = Math.floor(diff / 60000); - if (minutes < 1) return 'Just now'; - if (minutes < 60) return `${minutes}m ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; -} - interface StatCardProps { title: string; value: string | number; diff --git a/frontend/src/app/logs/page.tsx b/frontend/src/app/logs/page.tsx index 9a47161..749685b 100644 --- a/frontend/src/app/logs/page.tsx +++ b/frontend/src/app/logs/page.tsx @@ -5,7 +5,7 @@ import { AuthGuard } from '@/components/AuthGuard'; import { DashboardLayout } from '@/components/DashboardLayout'; import { useQuery } from '@tanstack/react-query'; import { processingRunsApi, mailAccountsApi, MailAccount, ProcessingRun, ProcessingLog } from '@/lib/api'; -import { parseUTC } from '@/lib/date-utils'; +import { formatRelative, formatDate, formatDuration } from '@/lib/date-utils'; import { Inbox, ChevronLeft, @@ -19,31 +19,6 @@ import { AlertTriangle, } from 'lucide-react'; -function formatRelative(iso?: string | null): string { - if (!iso) return 'Never'; - const diff = Date.now() - parseUTC(iso).getTime(); - const minutes = Math.floor(diff / 60000); - if (minutes < 1) return 'Just now'; - if (minutes < 60) return `${minutes}m ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; -} - -function formatDate(iso: string): string { - return parseUTC(iso).toLocaleString(undefined, { - dateStyle: 'short', - timeStyle: 'medium', - }); -} - -function formatDuration(seconds?: number | null): string { - if (seconds == null) return '—'; - if (seconds < 60) return `${seconds.toFixed(1)}s`; - const totalSecs = Math.floor(seconds); - return `${Math.floor(totalSecs / 60)}m ${totalSecs % 60}s`; -} - function RunDetailRow({ run }: { run: ProcessingRun }) { const [expanded, setExpanded] = useState(false); const [logsPage, setLogsPage] = useState(1); diff --git a/frontend/src/lib/date-utils.ts b/frontend/src/lib/date-utils.ts index f41868e..d21716a 100644 --- a/frontend/src/lib/date-utils.ts +++ b/frontend/src/lib/date-utils.ts @@ -14,3 +14,40 @@ export function parseUTC(iso: string): Date { return new Date(/Z|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : iso + 'Z'); } + +/** + * Format an ISO-8601 UTC timestamp as a human-readable relative time string + * (e.g. "3m ago", "2h ago", "1d ago"). Returns "Never" for falsy input. + */ +export function formatRelative(iso?: string | null): string { + if (!iso) return 'Never'; + const diff = Date.now() - parseUTC(iso).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +/** + * Format an ISO-8601 UTC timestamp as a locale-aware short date + medium time + * string, e.g. "28.03.26, 22:19:44". + */ +export function formatDate(iso: string): string { + return parseUTC(iso).toLocaleString(undefined, { + dateStyle: 'short', + timeStyle: 'medium', + }); +} + +/** + * Format a duration in seconds as a human-readable string, + * e.g. "3.1s" or "2m 5s". Returns "—" for null/undefined input. + */ +export function formatDuration(seconds?: number | null): string { + if (seconds == null) return '—'; + if (seconds < 60) return `${seconds.toFixed(1)}s`; + const totalSecs = Math.floor(seconds); + return `${Math.floor(totalSecs / 60)}m ${totalSecs % 60}s`; +}