[PULSE-54] Analytics chart improvements and live dashboard #24

Merged
uz1mani merged 10 commits from staging into main 2026-02-12 07:55:21 +00:00
7 changed files with 358 additions and 127 deletions
Showing only changes of commit 213e337940 - Show all commits

View File

@@ -1,6 +1,6 @@
'use client'
import { useEffect, useState } from 'react'
import { useCallback, 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 { toast } from '@ciphera-net/ui'
@@ -80,29 +80,13 @@ export default function PublicDashboardPage() {
}
greptile-apps[bot] commented 2026-02-12 07:46:58 +00:00 (Migrated from github.com)
Review

Missing loadDashboard and loadRealtime in dependency array causes stale closure. The interval callback captures old function references, which may reference outdated state or props.

  }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime])
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/share/[id]/page.tsx
Line: 84:93

Comment:
Missing `loadDashboard` and `loadRealtime` in dependency array causes stale closure. The interval callback captures old function references, which may reference outdated state or props.

```suggestion
  }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime])
```

How can I resolve this? If you propose a fix, please make it concise.
Missing `loadDashboard` and `loadRealtime` in dependency array causes stale closure. The interval callback captures old function references, which may reference outdated state or props. ```suggestion }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime]) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/share/[id]/page.tsx Line: 84:93 Comment: Missing `loadDashboard` and `loadRealtime` in dependency array causes stale closure. The interval callback captures old function references, which may reference outdated state or props. ```suggestion }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime]) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-12 07:49:54 +00:00 (Migrated from github.com)
Review

Issue: The interval callback captured old loadDashboard and loadRealtime references, causing stale closures as state/props changed.
Fix: Wrapped both functions in useCallback and added them to the effect dependency array.
Why: Ensures the interval always calls the latest functions that reflect current state and props.

Issue: The interval callback captured old loadDashboard and loadRealtime references, causing stale closures as state/props changed. Fix: Wrapped both functions in useCallback and added them to the effect dependency array. Why: Ensures the interval always calls the latest functions that reflect current state and props.
}
// * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
if (data && !isPasswordProtected) {
loadDashboard(true)
loadRealtime()
}
}, 30000)
return () => clearInterval(interval)
}, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password])
// * Tick every 1s so "Live · Xs ago" counts in real time
useEffect(() => {
const interval = setInterval(() => setTick((t) => t + 1), 1000)
return () => clearInterval(interval)
}, [])
useEffect(() => {
loadDashboard()
}, [siteId, dateRange, todayInterval, multiDayInterval])
const loadRealtime = async () => {
const loadRealtime = useCallback(async () => {
try {
const auth = {
password,
@@ -122,9 +106,9 @@ export default function PublicDashboardPage() {
} catch (error) {
// Silently fail for realtime updates
}
}
}, [siteId, password, captchaId, captchaSolution, captchaToken, data])
const loadDashboard = async (silent = false) => {
const loadDashboard = useCallback(async (silent = false) => {
try {
if (!silent) setLoading(true)
@@ -186,7 +170,22 @@ export default function PublicDashboardPage() {
} finally {
if (!silent) setLoading(false)
}
}
}, [siteId, dateRange, todayInterval, multiDayInterval, password, captchaId, captchaSolution, captchaToken])
// * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds
useEffect(() => {
if (data && !isPasswordProtected) {
const interval = setInterval(() => {
loadDashboard(true)
loadRealtime()
}, 30000)
return () => clearInterval(interval)
}
}, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime])
useEffect(() => {
loadDashboard()
}, [siteId, dateRange, todayInterval, multiDayInterval, loadDashboard])
const handlePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault()

View File

@@ -1,7 +1,7 @@
'use client'
import { useAuth } from '@/lib/auth/context'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
greptile-apps[bot] commented 2026-02-12 07:53:23 +00:00 (Migrated from github.com)
Review

Import useCallback from react to fix stale closure issues in interval callbacks.

import { useEffect, useState, useCallback } from 'react'
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/sites/[id]/page.tsx
Line: 4:4

Comment:
Import `useCallback` from react to fix stale closure issues in interval callbacks.

```suggestion
import { useEffect, useState, useCallback } from 'react'
```

How can I resolve this? If you propose a fix, please make it concise.
Import `useCallback` from react to fix stale closure issues in interval callbacks. ```suggestion import { useEffect, useState, useCallback } from 'react' ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/sites/[id]/page.tsx Line: 4:4 Comment: Import `useCallback` from react to fix stale closure issues in interval callbacks. ```suggestion import { useEffect, useState, useCallback } from 'react' ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-12 07:55:14 +00:00 (Migrated from github.com)
Review

Already addressed in the current code. Fixed.

Already addressed in the current code. Fixed.
import { motion } from 'framer-motion'
import { getSite, type Site } from '@/lib/api/sites'
@@ -128,15 +128,13 @@ export default function SiteDashboardPage() {
}, [todayInterval, multiDayInterval, isSettingsLoaded]) // dateRange is handled in saveSettings/onChange
useEffect(() => {
if (isSettingsLoaded) {
loadData()
}
if (isSettingsLoaded) loadData()
const interval = setInterval(() => {
greptile-apps[bot] commented 2026-02-12 07:46:59 +00:00 (Migrated from github.com)
Review

Missing loadData and loadRealtime in dependency array causes stale closure. The interval callback captures old function references.

    }, 30000) // * Chart, KPIs, and realtime count update every 30 seconds
    return () => clearInterval(interval)
  }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/sites/[id]/page.tsx
Line: 134:139

Comment:
Missing `loadData` and `loadRealtime` in dependency array causes stale closure. The interval callback captures old function references.

```suggestion
    }, 30000) // * Chart, KPIs, and realtime count update every 30 seconds
    return () => clearInterval(interval)
  }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
```

How can I resolve this? If you propose a fix, please make it concise.
Missing `loadData` and `loadRealtime` in dependency array causes stale closure. The interval callback captures old function references. ```suggestion }, 30000) // * Chart, KPIs, and realtime count update every 30 seconds return () => clearInterval(interval) }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime]) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/sites/[id]/page.tsx Line: 134:139 Comment: Missing `loadData` and `loadRealtime` in dependency array causes stale closure. The interval callback captures old function references. ```suggestion }, 30000) // * Chart, KPIs, and realtime count update every 30 seconds return () => clearInterval(interval) }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime]) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-12 07:50:04 +00:00 (Migrated from github.com)
Review

Issue: The interval callback captured old loadData and loadRealtime references.
Fix: Both loadData and loadRealtime are memoized with useCallback and included in the effect dependency array.
Why: Keeps the interval callback in sync with the latest loader implementations and current dependencies.

Issue: The interval callback captured old loadData and loadRealtime references. Fix: Both loadData and loadRealtime are memoized with useCallback and included in the effect dependency array. Why: Keeps the interval callback in sync with the latest loader implementations and current dependencies.
loadData(true)
loadRealtime()
}, 30000) // * Chart, KPIs, and realtime count update every 30 seconds
}, 30000)
return () => clearInterval(interval)
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded])
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
// * Tick every 1s so "Live · Xs ago" counts in real time
useEffect(() => {
@@ -144,31 +142,20 @@ export default function SiteDashboardPage() {
return () => clearInterval(interval)
}, [])
const getPreviousDateRange = (start: string, end: string) => {
const getPreviousDateRange = useCallback((start: string, end: string) => {
const startDate = new Date(start)
const endDate = new Date(end)
const duration = endDate.getTime() - startDate.getTime()
// * If duration is 0 (Today), set previous range to yesterday
if (duration === 0) {
const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000)
const prevStart = prevEnd
return {
start: prevStart.toISOString().split('T')[0],
end: prevEnd.toISOString().split('T')[0]
}
return { start: prevEnd.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] }
}
const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000)
const prevStart = new Date(prevEnd.getTime() - duration)
return {
start: prevStart.toISOString().split('T')[0],
end: prevEnd.toISOString().split('T')[0]
}
}
return { start: prevStart.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] }
}, [])
const loadData = async (silent = false) => {
const loadData = useCallback(async (silent = false) => {
try {
if (!silent) setLoading(true)
const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval
@@ -217,16 +204,16 @@ export default function SiteDashboardPage() {
} finally {
if (!silent) setLoading(false)
}
}
}, [siteId, dateRange, todayInterval, multiDayInterval])
const loadRealtime = async () => {
const loadRealtime = useCallback(async () => {
try {
const data = await getRealtime(siteId)
setRealtime(data.visitors)
} catch (error) {
// Silently fail for realtime updates
}
}
}, [siteId])
if (loading) {
return <LoadingOverlay logoSrc="/pulse_icon_no_margins.png" title="Pulse" />

View File

@@ -650,8 +650,8 @@ export default function Chart({
strokeDasharray="4 4"
strokeOpacity={0.7}
label={{
value: `Avg: ${metric === 'bounce_rate' ? `${Math.round(avg)}%` : metric === 'avg_duration' ? formatDuration(avg) : formatAxisValue(avg)}`,
position: 'right',
value: `Avg: ${metric === 'bounce_rate' ? `${Math.round(avg)}%` : metric === 'avg_duration' ? formatAxisDuration(avg) : formatAxisValue(avg)}`,
position: 'insideTopRight',
fill: colors.axis,
fontSize: 11,
}}