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:
@@ -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} />}
|
||||||
|
|||||||
@@ -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("#", "")}`
|
||||||
|
|||||||
55
client/src/hooks/useCpuLive.ts
Normal file
55
client/src/hooks/useCpuLive.ts
Normal 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],
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user