fix: improve session management and UI highlights

This commit is contained in:
Usman Baig
2026-03-01 13:53:54 +01:00
parent ac1ed58127
commit b3a303d6df
7 changed files with 44 additions and 64 deletions

View File

@@ -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

View File

@@ -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')) {

View File

@@ -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'

View File

@@ -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()

View File

@@ -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
View 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',
})
}

View File

@@ -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