Live CPU chart: 1s polling with 5s x-axis labels

- New useCpuLive hook: dedicated 1s React Query poll for CPU/loadAvg
- CpuChart x-axis labels every 5 data points (= every 5 seconds)
- 60-point visible window (last 60 seconds)
- Removed useCpuHistory in favour of useCpuLive in dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 16:58:24 +05:30
parent a22f7b535a
commit f98bd60c29
3 changed files with 68 additions and 29 deletions

View File

@@ -16,7 +16,7 @@ import { StoragePage } from "@/components/pages/StoragePage"
import { NetworkPage } from "@/components/pages/NetworkPage" import { NetworkPage } from "@/components/pages/NetworkPage"
import { LoginPage } from "@/components/ui/animated-characters-login-page" import { LoginPage } from "@/components/ui/animated-characters-login-page"
import { useServerHealth } from "@/hooks/useServerHealth" import { useServerHealth } from "@/hooks/useServerHealth"
import { useCpuHistory } from "@/hooks/useCpuHistory" import { useCpuLive } from "@/hooks/useCpuLive"
import { formatBytes } from "@/lib/utils" import { formatBytes } from "@/lib/utils"
const TOKEN_KEY = "eventify-auth-token" const TOKEN_KEY = "eventify-auth-token"
@@ -62,13 +62,7 @@ function DashboardContent({ activePage, onLogout }: { activePage: string; onLogo
dataUpdatedAt, dataUpdatedAt,
} = useServerHealth() } = useServerHealth()
const { history, addDataPoint } = useCpuHistory() const { history, loadAvg: liveLoadAvg } = useCpuLive()
useEffect(() => {
if (data?.system) {
addDataPoint(data.system.cpuPercent, data.timestamp)
}
}, [data, addDataPoint])
// If we get a 401, the token is invalid — log out // If we get a 401, the token is invalid — log out
useEffect(() => { useEffect(() => {
@@ -212,7 +206,7 @@ function DashboardContent({ activePage, onLogout }: { activePage: string; onLogo
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<CpuChart <CpuChart
history={history} history={history}
loadAvg={system?.loadAvg ?? [0, 0, 0]} loadAvg={liveLoadAvg}
/> />
</div> </div>
{memory && <MemoryDonut memory={memory} />} {memory && <MemoryDonut memory={memory} />}

View File

@@ -1,4 +1,3 @@
import { useEffect, useRef } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -10,9 +9,10 @@ interface CpuChartProps {
const CHART_HEIGHT = 200 const CHART_HEIGHT = 200
const PADDING = { top: 12, right: 16, bottom: 28, left: 44 } const PADDING = { top: 12, right: 16, bottom: 28, left: 44 }
const INNER_H = CHART_HEIGHT - PADDING.top - PADDING.bottom const INNER_H = CHART_HEIGHT - PADDING.top - PADDING.bottom
const STEP = 18 // px between data points const STEP = 12 // px between data points (1s each)
const VISIBLE = 30 // number of points visible at once const VISIBLE = 60 // show last 60 seconds
const INNER_W = VISIBLE * STEP const INNER_W = VISIBLE * STEP
const X_LABEL_EVERY = 5 // label every 5 seconds on x-axis
// Map a CPU % value to a y coordinate // Map a CPU % value to a y coordinate
function toY(cpu: number) { function toY(cpu: number) {
@@ -32,15 +32,9 @@ function loadColor(load: number) {
} }
export function CpuChart({ history, loadAvg }: CpuChartProps) { export function CpuChart({ history, loadAvg }: CpuChartProps) {
const prevLenRef = useRef(0)
const currentCpu = history.length > 0 ? history[history.length - 1].cpuPercent : 0 const currentCpu = history.length > 0 ? history[history.length - 1].cpuPercent : 0
const color = cpuColor(currentCpu) const color = cpuColor(currentCpu)
// Track history length changes (used to drive CSS transitions via key changes)
useEffect(() => {
prevLenRef.current = history.length
}, [history.length])
// Assign fixed x positions: latest point always at right edge, older ones to the left // Assign fixed x positions: latest point always at right edge, older ones to the left
const pts = history.map((p, i) => ({ const pts = history.map((p, i) => ({
x: PADDING.left + INNER_W - (history.length - 1 - i) * STEP, x: PADDING.left + INNER_W - (history.length - 1 - i) * STEP,
@@ -49,7 +43,7 @@ export function CpuChart({ history, loadAvg }: CpuChartProps) {
time: p.timestamp, time: p.timestamp,
})) }))
// Only keep points that are within or just left of the chart area // Only keep points within or just left of the chart area
const visible = pts.filter(p => p.x >= PADDING.left - STEP) const visible = pts.filter(p => p.x >= PADDING.left - STEP)
const linePath = visible.length > 1 const linePath = visible.length > 1
@@ -62,18 +56,14 @@ export function CpuChart({ history, loadAvg }: CpuChartProps) {
const yTicks = [0, 25, 50, 75, 100] const yTicks = [0, 25, 50, 75, 100]
// X-axis time labels: show 5 evenly spaced from visible points // X-axis: label every 5th point (= every 5 seconds)
const xLabelIndexes = visible.length > 1 const xLabels = visible
? [0, Math.floor(visible.length * 0.25), Math.floor(visible.length * 0.5), Math.floor(visible.length * 0.75), visible.length - 1] .map((p, i) => ({ p, i, globalIdx: history.length - visible.length + i }))
: visible.length === 1 ? [0] : [] .filter(({ globalIdx }) => globalIdx % X_LABEL_EVERY === 0)
const xLabels = [...new Set(xLabelIndexes)].map(i => { .map(({ p }) => ({
const p = visible[i]
if (!p) return null
return {
x: p.x, x: p.x,
label: new Date(p.time).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }), label: new Date(p.time).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }),
} }))
}).filter(Boolean) as { x: number; label: string }[]
// Gradient colors based on CPU status // Gradient colors based on CPU status
const gradientId = `cpuGrad-${color.stroke.replace("#", "")}` const gradientId = `cpuGrad-${color.stroke.replace("#", "")}`

View File

@@ -0,0 +1,55 @@
import { useQuery } from "@tanstack/react-query"
import { useCallback, useRef, useState } from "react"
export interface CpuDataPoint {
cpuPercent: number
timestamp: string
}
const MAX_POINTS = 120 // 2 minutes of 1s data
export function useCpuLive() {
const historyRef = useRef<CpuDataPoint[]>([])
const [history, setHistory] = useState<CpuDataPoint[]>([])
const lastTimestampRef = useRef<string | null>(null)
const addDataPoint = useCallback((cpuPercent: number, timestamp: string) => {
if (timestamp === lastTimestampRef.current) return
lastTimestampRef.current = timestamp
const next = [...historyRef.current, { cpuPercent, timestamp }]
if (next.length > MAX_POINTS) next.shift()
historyRef.current = next
setHistory([...next])
}, [])
const query = useQuery<{ system: { cpuPercent: number; loadAvg: [number, number, number] }; timestamp: string }>({
queryKey: ["cpu-live"],
queryFn: async () => {
const token = localStorage.getItem("eventify-auth-token")
const res = await fetch("/api/overview", {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
if (!res.ok) throw new Error(`${res.status}`)
return res.json()
},
refetchInterval: 1000,
refetchIntervalInBackground: true,
staleTime: 0,
select: (data) => ({
system: { cpuPercent: data.system?.cpuPercent ?? 0, loadAvg: data.system?.loadAvg ?? [0, 0, 0] },
timestamp: data.timestamp,
}),
})
// Feed new points into history whenever data arrives
const data = query.data
if (data && data.timestamp !== lastTimestampRef.current) {
addDataPoint(data.system.cpuPercent, data.timestamp)
}
return {
history,
cpuPercent: data?.system.cpuPercent ?? 0,
loadAvg: (data?.system.loadAvg ?? [0, 0, 0]) as [number, number, number],
}
}