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], + } +}