From b3a303d6df22b30ecc04ac89c3e7a0db6be53d86 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sun, 1 Mar 2026 13:53:54 +0100 Subject: [PATCH] fix: improve session management and UI highlights --- CHANGELOG.md | 2 ++ app/settings/SettingsPageClient.tsx | 2 +- components/settings/SecurityActivityCard.tsx | 32 +------------------- components/settings/TrustedDevicesCard.tsx | 32 +------------------- lib/auth/context.tsx | 2 ++ lib/utils/formatDate.ts | 30 ++++++++++++++++++ lib/utils/requestId.ts | 8 ++++- 7 files changed, 44 insertions(+), 64 deletions(-) create mode 100644 lib/utils/formatDate.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e58fb..673cf89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed - **More reliable background processing.** When multiple Pulse servers are running, background tasks like daily analytics calculations and data cleanup now coordinate more safely. Previously, under rare timing conditions, two servers could accidentally run the same task at the same time, which could lead to slightly inaccurate stats. Each server now holds a unique token that prevents one from interfering with another's work. +- **Cross-tab sign-out cleanup.** Signing out in one tab now fully clears your session data in all other tabs. Previously, some session-related entries were left behind, which could briefly show stale state before the redirect completed. +- **Settings sidebar highlight.** The "Manage Account" section in Settings now stays highlighted when you're viewing Trusted Devices or Security Activity. Previously, navigating to a sub-page removed the highlight from the parent section, making it unclear which group you were in. ## [0.12.0-alpha] - 2026-03-01 diff --git a/app/settings/SettingsPageClient.tsx b/app/settings/SettingsPageClient.tsx index a98fa56..5cb7464 100644 --- a/app/settings/SettingsPageClient.tsx +++ b/app/settings/SettingsPageClient.tsx @@ -443,7 +443,7 @@ function AppSettingsSection() {
{ toggleSection('account') if (!expanded.has('account')) { diff --git a/components/settings/SecurityActivityCard.tsx b/components/settings/SecurityActivityCard.tsx index 47b1ba9..7e5ca6b 100644 --- a/components/settings/SecurityActivityCard.tsx +++ b/components/settings/SecurityActivityCard.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react' import { useAuth } from '@/lib/auth/context' import { getUserActivity, type AuditLogEntry } from '@/lib/api/activity' import { Spinner } from '@ciphera-net/ui' +import { formatRelativeTime, formatFullDate } from '@/lib/utils/formatDate' const PAGE_SIZE = 20 @@ -62,37 +63,6 @@ function getFailureReason(entry: AuditLogEntry): string | null { return labels[reason as string] || (reason as string).replace(/_/g, ' ') } -function formatRelativeTime(dateStr: string): string { - const date = new Date(dateStr) - const now = new Date() - const diffMs = now.getTime() - date.getTime() - const diffMin = Math.floor(diffMs / 60000) - const diffHr = Math.floor(diffMin / 60) - const diffDay = Math.floor(diffHr / 24) - - if (diffMin < 1) return 'Just now' - if (diffMin < 60) return `${diffMin}m ago` - if (diffHr < 24) return `${diffHr}h ago` - if (diffDay < 7) return `${diffDay}d ago` - - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, - }) -} - -function formatFullDate(dateStr: string): string { - return new Date(dateStr).toLocaleString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - }) -} - function parseBrowserName(ua: string): string { if (!ua) return 'Unknown' if (ua.includes('Firefox')) return 'Firefox' diff --git a/components/settings/TrustedDevicesCard.tsx b/components/settings/TrustedDevicesCard.tsx index 21343c4..9441721 100644 --- a/components/settings/TrustedDevicesCard.tsx +++ b/components/settings/TrustedDevicesCard.tsx @@ -4,37 +4,7 @@ import { useEffect, useState, useCallback } from 'react' import { useAuth } from '@/lib/auth/context' import { getUserDevices, removeDevice, type TrustedDevice } from '@/lib/api/devices' import { Spinner, toast } from '@ciphera-net/ui' - -function formatRelativeTime(dateStr: string): string { - const date = new Date(dateStr) - const now = new Date() - const diffMs = now.getTime() - date.getTime() - const diffMin = Math.floor(diffMs / 60000) - const diffHr = Math.floor(diffMin / 60) - const diffDay = Math.floor(diffHr / 24) - - if (diffMin < 1) return 'Just now' - if (diffMin < 60) return `${diffMin}m ago` - if (diffHr < 24) return `${diffHr}h ago` - if (diffDay < 7) return `${diffDay}d ago` - - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, - }) -} - -function formatFullDate(dateStr: string): string { - return new Date(dateStr).toLocaleString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - }) -} +import { formatRelativeTime, formatFullDate } from '@/lib/utils/formatDate' function getDeviceIcon(hint: string): string { const h = hint.toLowerCase() diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index 7b1920a..1ebc65d 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -174,6 +174,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { useSessionSync({ onLogout: () => { localStorage.removeItem('user') + localStorage.removeItem('ciphera_token_refreshed_at') + localStorage.removeItem('ciphera_last_activity') window.location.href = '/' }, onLogin: (userData) => { diff --git a/lib/utils/formatDate.ts b/lib/utils/formatDate.ts new file mode 100644 index 0000000..aa2cbaa --- /dev/null +++ b/lib/utils/formatDate.ts @@ -0,0 +1,30 @@ +export function formatRelativeTime(dateStr: string): string { + const date = new Date(dateStr) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffMin = Math.floor(diffMs / 60000) + const diffHr = Math.floor(diffMin / 60) + const diffDay = Math.floor(diffHr / 24) + + if (diffMin < 1) return 'Just now' + if (diffMin < 60) return `${diffMin}m ago` + if (diffHr < 24) return `${diffHr}h ago` + if (diffDay < 7) return `${diffDay}d ago` + + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, + }) +} + +export function formatFullDate(dateStr: string): string { + return new Date(dateStr).toLocaleString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + }) +} diff --git a/lib/utils/requestId.ts b/lib/utils/requestId.ts index de6f3bf..1251fd8 100644 --- a/lib/utils/requestId.ts +++ b/lib/utils/requestId.ts @@ -1,6 +1,11 @@ /** * Request ID utilities for tracing API calls across services * Request IDs help debug issues by correlating logs across frontend and backends + * + * IMPORTANT: This module stores mutable state (lastRequestId) at module scope. + * This is safe because apiRequest (the only caller) runs exclusively in the + * browser where JS is single-threaded. If this ever needs server-side use, + * replace the module variable with AsyncLocalStorage. */ const REQUEST_ID_HEADER = 'X-Request-ID' @@ -23,7 +28,8 @@ export function getRequestIdHeader(): string { } /** - * Store the last request ID for error reporting + * Store the last request ID for error reporting. + * Browser-only — single-threaded, no concurrency risk. */ let lastRequestId: string | null = null