diff --git a/client/src/App.tsx b/client/src/App.tsx
index b9d8122..a44ff26 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -16,7 +16,7 @@ import { StoragePage } from "@/components/pages/StoragePage"
import { NetworkPage } from "@/components/pages/NetworkPage"
import { LoginPage } from "@/components/ui/animated-characters-login-page"
import { useServerHealth } from "@/hooks/useServerHealth"
-import { useCpuHistory } from "@/hooks/useCpuHistory"
+import { useCpuLive } from "@/hooks/useCpuLive"
import { formatBytes } from "@/lib/utils"
const TOKEN_KEY = "eventify-auth-token"
@@ -62,13 +62,7 @@ function DashboardContent({ activePage, onLogout }: { activePage: string; onLogo
dataUpdatedAt,
} = useServerHealth()
- const { history, addDataPoint } = useCpuHistory()
-
- useEffect(() => {
- if (data?.system) {
- addDataPoint(data.system.cpuPercent, data.timestamp)
- }
- }, [data, addDataPoint])
+ const { history, loadAvg: liveLoadAvg } = useCpuLive()
// If we get a 401, the token is invalid — log out
useEffect(() => {
@@ -212,7 +206,7 @@ function DashboardContent({ activePage, onLogout }: { activePage: string; onLogo
{memory && }
diff --git a/client/src/components/dashboard/CpuChart.tsx b/client/src/components/dashboard/CpuChart.tsx
index c88457e..7a3051e 100644
--- a/client/src/components/dashboard/CpuChart.tsx
+++ b/client/src/components/dashboard/CpuChart.tsx
@@ -1,4 +1,3 @@
-import { useEffect, useRef } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
@@ -10,9 +9,10 @@ interface CpuChartProps {
const CHART_HEIGHT = 200
const PADDING = { top: 12, right: 16, bottom: 28, left: 44 }
const INNER_H = CHART_HEIGHT - PADDING.top - PADDING.bottom
-const STEP = 18 // px between data points
-const VISIBLE = 30 // number of points visible at once
+const STEP = 12 // px between data points (1s each)
+const VISIBLE = 60 // show last 60 seconds
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
function toY(cpu: number) {
@@ -32,15 +32,9 @@ function loadColor(load: number) {
}
export function CpuChart({ history, loadAvg }: CpuChartProps) {
- const prevLenRef = useRef(0)
const currentCpu = history.length > 0 ? history[history.length - 1].cpuPercent : 0
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
const pts = history.map((p, i) => ({
x: PADDING.left + INNER_W - (history.length - 1 - i) * STEP,
@@ -49,7 +43,7 @@ export function CpuChart({ history, loadAvg }: CpuChartProps) {
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 linePath = visible.length > 1
@@ -62,18 +56,14 @@ export function CpuChart({ history, loadAvg }: CpuChartProps) {
const yTicks = [0, 25, 50, 75, 100]
- // X-axis time labels: show 5 evenly spaced from visible points
- const xLabelIndexes = visible.length > 1
- ? [0, Math.floor(visible.length * 0.25), Math.floor(visible.length * 0.5), Math.floor(visible.length * 0.75), visible.length - 1]
- : visible.length === 1 ? [0] : []
- const xLabels = [...new Set(xLabelIndexes)].map(i => {
- const p = visible[i]
- if (!p) return null
- return {
+ // X-axis: label every 5th point (= every 5 seconds)
+ const xLabels = visible
+ .map((p, i) => ({ p, i, globalIdx: history.length - visible.length + i }))
+ .filter(({ globalIdx }) => globalIdx % X_LABEL_EVERY === 0)
+ .map(({ p }) => ({
x: p.x,
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
const gradientId = `cpuGrad-${color.stroke.replace("#", "")}`
diff --git a/client/src/hooks/useCpuLive.ts b/client/src/hooks/useCpuLive.ts
new file mode 100644
index 0000000..4b8480e
--- /dev/null
+++ b/client/src/hooks/useCpuLive.ts
@@ -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([])
+ const [history, setHistory] = useState([])
+ const lastTimestampRef = useRef(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],
+ }
+}