fix: improve session management and UI highlights
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -443,7 +443,7 @@ function AppSettingsSection() {
|
||||
<div>
|
||||
<SectionHeader
|
||||
expanded={expanded.has('account')}
|
||||
active={active.section === 'account'}
|
||||
active={active.section === 'account' || active.section === 'devices' || active.section === 'activity'}
|
||||
onToggle={() => {
|
||||
toggleSection('account')
|
||||
if (!expanded.has('account')) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
30
lib/utils/formatDate.ts
Normal file
30
lib/utils/formatDate.ts
Normal file
@@ -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',
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user