From 4aefca7118cb2262ee2de7fb107a40431da8d668 Mon Sep 17 00:00:00 2001
From: Usman Baig
Date: Wed, 11 Feb 2026 20:49:09 +0100
Subject: [PATCH] feat: add "Updated X ago" display for realtime indicators and
implement auto-refresh tick functionality
---
app/share/[id]/page.tsx | 37 +++++++++++++++++++-------
app/sites/[id]/page.tsx | 58 +++++++++++++++++++++++++++--------------
lib/utils/format.ts | 12 +++++++++
3 files changed, 78 insertions(+), 29 deletions(-)
diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx
index 56359e8..087f558 100644
--- a/app/share/[id]/page.tsx
+++ b/app/share/[id]/page.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react'
import { useParams, useSearchParams, useRouter } from 'next/navigation'
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
+import { formatUpdatedAgo } from '@/lib/utils/format'
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import { LoadingOverlay, Button } from '@ciphera-net/ui'
@@ -53,6 +54,8 @@ export default function PublicDashboardPage() {
// Previous period data
const [prevStats, setPrevStats] = useState(undefined)
const [prevDailyStats, setPrevDailyStats] = useState(undefined)
+ const [lastUpdatedAt, setLastUpdatedAt] = useState(null)
+ const [, setTick] = useState(0)
const getPreviousDateRange = (start: string, end: string) => {
const startDate = new Date(start)
@@ -78,17 +81,23 @@ export default function PublicDashboardPage() {
}
}
- // Auto-refresh interval (for realtime)
+ // * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
- // Only refresh realtime count if we have data
if (data && !isPasswordProtected) {
+ loadDashboard(true)
loadRealtime()
}
- }, 30000) // 30 seconds
+ }, 30000)
return () => clearInterval(interval)
- }, [data, isPasswordProtected, dateRange, password])
+ }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password])
+
+ // * Tick every 5s to refresh "Updated X ago" display
+ useEffect(() => {
+ const interval = setInterval(() => setTick((t) => t + 1), 5000)
+ return () => clearInterval(interval)
+ }, [])
useEffect(() => {
loadDashboard()
@@ -153,6 +162,7 @@ export default function PublicDashboardPage() {
setData(dashboardData)
setPrevStats(prevStatsData)
setPrevDailyStats(prevDailyStatsData)
+ setLastUpdatedAt(Date.now())
setIsPasswordProtected(false)
// Reset captcha
@@ -283,15 +293,22 @@ export default function PublicDashboardPage() {
- {/* Realtime Indicator - Desktop */}
-
-
+ {/* Realtime Indicator & Polling - Desktop */}
+
+
+
-
-
+
+
{realtime_visitors} current visitors
-
+
+
+ {lastUpdatedAt !== null && (
+
+ Updated {formatUpdatedAgo(lastUpdatedAt)}
+
+ )}
diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx
index 7d5a319..676f9d4 100644
--- a/app/sites/[id]/page.tsx
+++ b/app/sites/[id]/page.tsx
@@ -6,7 +6,7 @@ import { useParams, useRouter } from 'next/navigation'
import { motion } from 'framer-motion'
import { getSite, type Site } from '@/lib/api/sites'
import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard, getCampaigns, getPerformanceByPage, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
-import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
+import { formatNumber, formatDuration, formatUpdatedAgo, getDateRange } from '@/lib/utils/format'
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@/lib/utils/authErrors'
import { LoadingOverlay, Button } from '@ciphera-net/ui'
@@ -57,6 +57,8 @@ export default function SiteDashboardPage() {
const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>('hour')
const [multiDayInterval, setMultiDayInterval] = useState<'hour' | 'day'>('day')
const [isSettingsLoaded, setIsSettingsLoaded] = useState(false)
+ const [lastUpdatedAt, setLastUpdatedAt] = useState(null)
+ const [, setTick] = useState(0)
// Load settings from localStorage
useEffect(() => {
@@ -130,11 +132,18 @@ export default function SiteDashboardPage() {
loadData()
}
const interval = setInterval(() => {
+ loadData(true)
loadRealtime()
- }, 30000) // Update every 30 seconds
+ }, 30000) // * Chart, KPIs, and realtime count update every 30 seconds
return () => clearInterval(interval)
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded])
+ // * Tick every 5s to refresh "Updated X ago" display
+ useEffect(() => {
+ const interval = setInterval(() => setTick((t) => t + 1), 5000)
+ return () => clearInterval(interval)
+ }, [])
+
const getPreviousDateRange = (start: string, end: string) => {
const startDate = new Date(start)
const endDate = new Date(end)
@@ -159,9 +168,9 @@ export default function SiteDashboardPage() {
}
}
- const loadData = async () => {
+ const loadData = async (silent = false) => {
try {
- setLoading(true)
+ if (!silent) setLoading(true)
const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval
const [data, prevStatsData, prevDailyStatsData, campaignsData] = await Promise.all([
@@ -200,10 +209,13 @@ export default function SiteDashboardPage() {
setPerformanceByPage(data.performance_by_page ?? null)
setGoalCounts(Array.isArray(data.goal_counts) ? data.goal_counts : [])
setCampaigns(Array.isArray(campaignsData) ? campaignsData : [])
+ setLastUpdatedAt(Date.now())
} catch (error: unknown) {
- toast.error(getAuthErrorMessage(error) || 'Failed to load data: ' + ((error as Error)?.message || 'Unknown error'))
+ if (!silent) {
+ toast.error(getAuthErrorMessage(error) || 'Failed to load data: ' + ((error as Error)?.message || 'Unknown error'))
+ }
} finally {
- setLoading(false)
+ if (!silent) setLoading(false)
}
}
@@ -247,19 +259,27 @@ export default function SiteDashboardPage() {
- {/* Realtime Indicator */}
-
+
+ {/* Realtime Indicator */}
+
+ {/* Polling indicator */}
+ {lastUpdatedAt !== null && (
+
+ Updated {formatUpdatedAgo(lastUpdatedAt)}
+
+ )}
+
diff --git a/lib/utils/format.ts b/lib/utils/format.ts
index fb0a200..485efa6 100644
--- a/lib/utils/format.ts
+++ b/lib/utils/format.ts
@@ -25,6 +25,18 @@ export function getDateRange(days: number): { start: string; end: string } {
}
}
+/**
+ * Format "updated X ago" for polling indicators (e.g. "Just now", "12 seconds ago")
+ */
+export function formatUpdatedAgo(timestamp: number): string {
+ const diff = Math.floor((Date.now() - timestamp) / 1000)
+ if (diff < 5) return 'Just now'
+ if (diff < 60) return `${diff} seconds ago`
+ if (diff < 120) return '1 minute ago'
+ const minutes = Math.floor(diff / 60)
+ return `${minutes} minutes ago`
+}
+
/**
* Format relative time (e.g., "2 hours ago")
*/