diff --git a/.env b/.env index a567e68..91479b4 100644 --- a/.env +++ b/.env @@ -1,7 +1,6 @@ -CLIENT_GRAPH_STEPS=150 +SERVER_STEPS=150 CLIENT_PORT=3000 CLIENT_REFETCH_INTERVAL=500 -SERVER_ACTIVE_WINDOW=5000 SERVER_DEPLOY_URL= SERVER_PORT=3001 SERVER_REFRESH_INTERVAL=500 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6864200..27cd949 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -689,18 +689,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -799,6 +799,7 @@ version = "0.1.0" dependencies = [ "axum", "dotenv", + "serde", "serde_json", "sysinfo", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 605fb83..5a5140a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] axum = { version = "0.6.9", features = ["macros", "ws"] } dotenv = "0.15.0" +serde = { version = "1.0.200", features = ["derive"] } serde_json = "1.0.93" sysinfo = "0.30.11" tokio = { version = "1.25.0", features = ["full"] } diff --git a/src/client/App.css b/src/client/App.css index fb0928e..13da33e 100644 --- a/src/client/App.css +++ b/src/client/App.css @@ -4,6 +4,7 @@ display: grid; padding: 8px; gap: 8px; + min-height: 100dvh; > div { border-radius: 16px; diff --git a/src/client/App.tsx b/src/client/App.tsx index c83fe45..3ba1aaa 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,28 +1,55 @@ -import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '@tanstack/react-query'; import './App.css'; -import { fetchDynamicData, fetchStaticData } from './api'; +import { fetchDynamicData, fetchHistoryData, fetchStaticData } from './api'; import { Cpu } from './components/chart-cards/cpu'; import { Disks } from './components/chart-cards/disks'; import { Memory } from './components/chart-cards/memory'; import { Network } from './components/chart-cards/network'; import { Static } from './components/chart-cards/static'; import { Temps } from './components/chart-cards/temps'; +import { useSetTheme } from './hooks/use-set-theme'; + +const serverSteps = Number(import.meta.env.SERVER_STEPS); +const serverRefreshInterval = Number(import.meta.env.SERVER_REFRESH_INTERVAL); +const refetchInterval = Number(import.meta.env.CLIENT_REFETCH_INTERVAL); const queryClient = new QueryClient(); const Main = () => { + const queryClient = useQueryClient(); const staticQuery = useQuery({ queryKey: ['static'], queryFn: fetchStaticData, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }); + + const historyQuery = useQuery({ + queryKey: ['history'], + queryFn: fetchHistoryData, }); const dynamicQuery = useQuery({ queryKey: ['dynamic'], - queryFn: fetchDynamicData, - refetchInterval: Number(import.meta.env.CLIENT_REFETCH_INTERVAL), + queryFn: async () => { + const data = await fetchDynamicData(); + queryClient.setQueryData(['history'], (historyOld: HistorySlice[]) => { + const history = historyOld.slice(); + while (history.length && history[0].timestamp < Date.now() - (serverSteps + 3) * serverRefreshInterval) { + history.shift(); + } + + history.push(data); + return history; + }); + return data; + }, + refetchInterval: refetchInterval, }); - const isLoading = staticQuery.isLoading || dynamicQuery.isLoading; + useSetTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); + + const isLoading = staticQuery.isLoading || dynamicQuery.isLoading || historyQuery.isLoading; return ( !isLoading && ( diff --git a/src/client/api/index.ts b/src/client/api/index.ts index b998a8e..e136882 100644 --- a/src/client/api/index.ts +++ b/src/client/api/index.ts @@ -17,7 +17,12 @@ export const fetchStaticData = async (): Promise => { return await response.json(); }; -export const fetchDynamicData = async (): Promise => { +export const fetchHistoryData = async (): Promise => { + const response = await fetch(getApiUrl('/api/history')); + return await response.json(); +}; + +export const fetchDynamicData = async (): Promise => { const response = await fetch(getApiUrl('/api/dynamic')); return await response.json(); }; diff --git a/src/client/api/types.ts b/src/client/api/types.ts index 01a19eb..697d731 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -15,17 +15,11 @@ type StaticData = { uptime: number; }; -type DynamicData = { - cpu_usage: number[]; - disks: { - read: number; - write: number; - }; - mem_usage: number; - network: { - down: number; - up: number; - }; - swap_usage: number; +type HistorySlice = { + cpu: number[]; + mem: [number, number]; + net: [number, number]; + disks: [number, number]; temps: number[]; + timestamp: number; }; diff --git a/src/client/components/chart-cards/common/card.tsx b/src/client/components/chart-cards/common/card.tsx index 34c5f84..8ae4410 100644 --- a/src/client/components/chart-cards/common/card.tsx +++ b/src/client/components/chart-cards/common/card.tsx @@ -6,25 +6,32 @@ import { CanvasChart } from './chart'; type Props = { title: ReactNode; subtitle?: ReactNode; - legend?: Omit & Partial>; + legend?: LegendProps; formatOptions?: FormatOptions; domain?: [number, number]; hardDomain?: boolean; hueOffset?: number; - data: number[]; + dataKey: Exclude; total: number; }; -export const ChartCard = ({ data, legend, hueOffset = 0, title, subtitle, formatOptions, ...rest }: Props) => { +export const ChartCard = ({ dataKey, legend, hueOffset = 0, title, subtitle, formatOptions, ...rest }: Props) => { return (

{title}

{subtitle} - {legend && } + {legend && ( + + )} - +
); }; diff --git a/src/client/components/chart-cards/common/chart/index.tsx b/src/client/components/chart-cards/common/chart/index.tsx index a4d3c1e..bd0355e 100644 --- a/src/client/components/chart-cards/common/chart/index.tsx +++ b/src/client/components/chart-cards/common/chart/index.tsx @@ -1,17 +1,18 @@ import { useAnimationFrame } from '@/hooks/use-animation-frame'; import { getFillColor, getStrokeColor } from '@/utils/colors'; import type { FormatOptions } from '@/utils/format'; +import { useQuery } from '@tanstack/react-query'; import { useEffect, useMemo, useRef, useState } from 'react'; import './index.css'; import { YAxis } from './y-axis'; -const stepWindow = Number(import.meta.env.CLIENT_GRAPH_STEPS); +const stepWindow = Number(import.meta.env.SERVER_STEPS); const stepPeriod = Number(import.meta.env.CLIENT_REFETCH_INTERVAL); const xMargin = 4; type Props = { total: number; - data: number[]; + dataKey: Exclude; domain?: [number, number]; hardDomain?: boolean; formatOptions?: FormatOptions; @@ -21,39 +22,27 @@ type Props = { const xFromTimestamp = (timestamp: number, width: number) => ((timestamp - Date.now()) / stepPeriod) * (width / stepWindow) + width + (width / stepWindow) * 2; -export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, data, formatOptions }: Props) => { +export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, dataKey, formatOptions }: Props) => { const canvasRef = useRef(null!); - const now = useMemo(() => Date.now(), []); - const history = useRef<[number, number[]][]>([[now, []]]); - const [historyMax, setHistoryMax] = useState(0); + const { data: historyData } = useQuery({ queryKey: ['history'] }); const targetMax = useMemo(() => { if (domain && hardDomain) { return domain[1]; } + const historyMax = (historyData ?? []).reduce((max, slice) => Math.max(max, ...slice[dataKey]), 0); + if (!domain || historyMax > domain[1]) { return historyMax; } return domain[1]; - }, [domain, hardDomain, historyMax]); + }, [domain, hardDomain, historyData, dataKey]); const max = useRef(targetMax); const [width, setWidth] = useState(640); const [height, setHeight] = useState(480); - // Record data changes - useEffect(() => { - if (data) { - while (history.current.length && xFromTimestamp(history.current[0][0], width) < -xMargin) { - history.current.shift(); - } - - history.current.push([Date.now(), data]); - setHistoryMax(history.current.reduce((max, [_, values]) => Math.max(max, ...values), 0)); - } - }, [data, width]); - // Sync canvas size to actual element dimensions useEffect(() => { const onResize = () => { @@ -72,7 +61,7 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, data, fo // Redraw chart useAnimationFrame(() => { const ctx = canvasRef.current.getContext('2d'); - if (!ctx) { + if (!ctx || !historyData) { return; } @@ -107,16 +96,16 @@ export const CanvasChart = ({ total, hueOffset = 0, domain, hardDomain, data, fo ctx.beginPath(); ctx.moveTo(-xMargin, height); - ctx.lineTo(-xMargin, height - (height * (history.current[0][1][i] ?? 0)) / max.current); + ctx.lineTo(-xMargin, height - (height * (historyData[0][dataKey][i] ?? 0)) / max.current); - for (const [timestamp, values] of history.current) { + for (const { timestamp, [dataKey]: values } of historyData) { const x = xFromTimestamp(timestamp, width); const y = height - (height * (values[i] ?? 0)) / max.current; ctx.lineTo(x, y); } - ctx.lineTo(width + xMargin, height - (height * (history.current[0][1][i] ?? 0)) / max.current); + ctx.lineTo(width + xMargin, height - (height * (historyData[0][dataKey][i] ?? 0)) / max.current); ctx.lineTo(width + xMargin, height); if (type === 'fill') ctx.fill(); diff --git a/src/client/components/chart-cards/common/legend/index.tsx b/src/client/components/chart-cards/common/legend/index.tsx index 13e8e02..3b6766e 100644 --- a/src/client/components/chart-cards/common/legend/index.tsx +++ b/src/client/components/chart-cards/common/legend/index.tsx @@ -1,15 +1,26 @@ import { getTextColor } from '@/utils/colors'; import { type FormatOptions, formatValue } from '@/utils/format'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; import './index.css'; -export type LegendProps = { +type LegendCommonProps = { labels: string[]; - values: string[] | number[]; formatOptions?: FormatOptions; hueOffset?: number; }; -export const Legend = ({ labels, values, formatOptions, hueOffset = 0 }: LegendProps) => ( +type LegendByKeyProps = LegendCommonProps & { + dataKey: Exclude; +}; + +type LegendByDataProps = LegendCommonProps & { + values: string[] | number[]; +}; + +export type LegendProps = LegendByDataProps | LegendByKeyProps; + +export const LegendByData = ({ labels, formatOptions, hueOffset = 0, values }: LegendByDataProps) => (
{labels.map((label, index) => (
@@ -24,3 +35,19 @@ export const Legend = ({ labels, values, formatOptions, hueOffset = 0 }: LegendP ))}
); + +export const LegendByKey = ({ dataKey, ...rest }: LegendByKeyProps) => { + const { data: historyData } = useQuery({ queryKey: ['history'] }); + const values = useMemo(() => historyData?.at(-1)?.[dataKey], [historyData, dataKey]); + if (values) { + return ; + } +}; + +export const Legend = (props: LegendProps) => { + if ('values' in props) { + return ; + } + + return ; +}; diff --git a/src/client/components/chart-cards/cpu.tsx b/src/client/components/chart-cards/cpu.tsx index df245db..582ec29 100644 --- a/src/client/components/chart-cards/cpu.tsx +++ b/src/client/components/chart-cards/cpu.tsx @@ -3,13 +3,13 @@ import { ChartCard } from './common/card'; export const Cpu = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); - const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const { data: historyData } = useQuery({ queryKey: ['history'] }); - if (!staticData || !dynamicData) { + if (!staticData || !historyData) { return
; } - const total_cpus = dynamicData.cpu_usage.length; + const total_cpus = historyData[0].cpu.length; return ( !!total_cpus && ( @@ -24,7 +24,7 @@ export const Cpu = () => { domain={[0, 100]} hardDomain formatOptions={{ prefix: false, units: '%' }} - data={dynamicData.cpu_usage} + dataKey={'cpu'} total={total_cpus} /> ) diff --git a/src/client/components/chart-cards/disks.tsx b/src/client/components/chart-cards/disks.tsx index cf3dfd3..1235a45 100644 --- a/src/client/components/chart-cards/disks.tsx +++ b/src/client/components/chart-cards/disks.tsx @@ -4,22 +4,23 @@ import { useAtomValue } from 'jotai'; import { ChartCard } from './common/card'; export const Disks = () => { - const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const { data: historyData } = useQuery({ queryKey: ['history'] }); const isSi = useAtomValue(siAtom); - if (!dynamicData) { + if (!historyData) { return
; } return ( ); diff --git a/src/client/components/chart-cards/memory.tsx b/src/client/components/chart-cards/memory.tsx index 967f2b5..10e0ebe 100644 --- a/src/client/components/chart-cards/memory.tsx +++ b/src/client/components/chart-cards/memory.tsx @@ -7,7 +7,7 @@ import { ChartCard } from './common/card'; export const Memory = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); - const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const { data: historyData } = useQuery({ queryKey: ['history'] }); const isSi = useAtomValue(siAtom); const formatOptions = { units: 'B', ...(isSi && { si: true }) }; @@ -18,7 +18,9 @@ export const Memory = () => { return [formatValue(staticData.total_memory, formatOptions), formatValue(staticData.total_swap, formatOptions)]; }, [staticData, formatOptions]); - if (!staticData || !dynamicData) { + const last = useMemo(() => historyData?.at(-1), [historyData]); + + if (!staticData || !historyData || !last) { return
; } @@ -27,15 +29,15 @@ export const Memory = () => { title='Memory' legend={{ values: [ - `${formatValue(dynamicData.mem_usage, formatOptions)} / ${formatedTotals[0]}`, - `${formatValue(dynamicData.swap_usage, formatOptions)} / ${formatedTotals[1]}`, + `${formatValue(last.mem[0], formatOptions)} / ${formatedTotals[0]}`, + `${formatValue(last.mem[1], formatOptions)} / ${formatedTotals[1]}`, ], labels: ['Memory', 'Swap'], }} domain={[0, Math.max(staticData.total_memory, staticData.total_swap)]} hardDomain formatOptions={formatOptions} - data={[dynamicData.mem_usage, dynamicData.swap_usage]} + dataKey={'mem'} total={2} /> ); diff --git a/src/client/components/chart-cards/network.tsx b/src/client/components/chart-cards/network.tsx index 566653b..7c86102 100644 --- a/src/client/components/chart-cards/network.tsx +++ b/src/client/components/chart-cards/network.tsx @@ -4,22 +4,23 @@ import { useAtomValue } from 'jotai'; import { ChartCard } from './common/card'; export const Network = () => { - const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const { data: historyData } = useQuery({ queryKey: ['history'] }); const isSi = useAtomValue(siAtom); - if (!dynamicData) { + if (!historyData) { return
; } return ( ); diff --git a/src/client/components/chart-cards/static.tsx b/src/client/components/chart-cards/static.tsx index f3737c8..200474b 100644 --- a/src/client/components/chart-cards/static.tsx +++ b/src/client/components/chart-cards/static.tsx @@ -1,9 +1,10 @@ import { highFpsAtom, siAtom } from '@/atoms'; import { Switch } from '@/components/switch'; import { useAnimationFrame } from '@/hooks/use-animation-frame'; +import { useSetTheme } from '@/hooks/use-set-theme'; import { useQuery } from '@tanstack/react-query'; import { useAtom } from 'jotai'; -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; import './static.css'; const formatUptime = (value: number) => { @@ -41,14 +42,11 @@ const Uptime = ({ boot_time }: Pick) => { export const Static = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); - const root = useRef(document.getElementById('root')!); const [dark, setDark] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches); const [highFps, setHighFps] = useAtom(highFpsAtom); const [isSi, setIsSi] = useAtom(siAtom); - useEffect(() => { - root.current.setAttribute('data-theme', dark ? 'dark' : 'light'); - }, [dark]); + useSetTheme(dark); return ( staticData && ( diff --git a/src/client/components/chart-cards/temps.tsx b/src/client/components/chart-cards/temps.tsx index 29620c8..da18777 100644 --- a/src/client/components/chart-cards/temps.tsx +++ b/src/client/components/chart-cards/temps.tsx @@ -3,21 +3,22 @@ import { ChartCard } from './common/card'; export const Temps = () => { const { data: staticData } = useQuery({ queryKey: ['static'] }); - const { data: dynamicData } = useQuery({ queryKey: ['dynamic'] }); + const { data: historyData } = useQuery({ queryKey: ['history'] }); - if (!staticData || !dynamicData) { + if (!staticData || !historyData) { return
; } return ( ); diff --git a/src/client/hooks/use-set-theme.ts b/src/client/hooks/use-set-theme.ts new file mode 100644 index 0000000..3747072 --- /dev/null +++ b/src/client/hooks/use-set-theme.ts @@ -0,0 +1,7 @@ +import { useEffect } from 'react'; + +export const useSetTheme = (dark?: boolean) => { + useEffect(() => { + document.getElementById('root')!.setAttribute('data-theme', dark ? 'dark' : 'light'); + }, [dark]); +}; diff --git a/src/client/vite-env.d.ts b/src/client/vite-env.d.ts index c3ed686..1dde36f 100644 --- a/src/client/vite-env.d.ts +++ b/src/client/vite-env.d.ts @@ -1,10 +1,9 @@ /// interface ImportMetaEnv { - readonly CLIENT_GRAPH_STEPS: string; + readonly SERVER_STEPS: string; readonly CLIENT_PORT: string; readonly CLIENT_REFETCH_INTERVAL: string; - readonly SERVER_ACTIVE_WINDOW: string; readonly SERVER_DEPLOY_URL?: string; readonly SERVER_PORT: string; readonly SERVER_REFRESH_INTERVAL: string; diff --git a/src/stats-server/disks.rs b/src/stats-server/disks.rs index 1cb6235..924b258 100644 --- a/src/stats-server/disks.rs +++ b/src/stats-server/disks.rs @@ -1,13 +1,12 @@ pub mod disks { - use serde_json::json; use std::{ fs::File, io::{Read, Seek, SeekFrom}, }; pub struct DiskUsage { - read: u64, - write: u64, + pub read: u64, + pub write: u64, } impl DiskUsage { @@ -43,12 +42,12 @@ pub mod disks { curr } - pub fn diff(&mut self) -> serde_json::Value { + pub fn diff(&mut self) -> DiskUsage { let curr = self.refresh(); - let diff = json!({ - "read": curr.read - self.prev.read, - "write": curr.write - self.prev.write, - }); + let diff = DiskUsage { + read: curr.read - self.prev.read, + write: curr.write - self.prev.write, + }; self.prev = curr; diff } diff --git a/src/stats-server/main.rs b/src/stats-server/main.rs index d8e356a..9e11ee3 100644 --- a/src/stats-server/main.rs +++ b/src/stats-server/main.rs @@ -1,9 +1,9 @@ use axum::{extract::State, http::Response, response::IntoResponse, routing::get, Router, Server}; use dotenv::dotenv; -use serde_json::{json, Value}; +use serde_json::json; use std::{ sync::{Arc, Mutex}, - time::{Duration, Instant}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use sysinfo::{Components, CpuRefreshKind, MemoryRefreshKind, Networks, RefreshKind, System}; @@ -11,17 +11,25 @@ use crate::disks::disks::DiskStats; mod disks; +#[derive(Clone, serde::Serialize)] +struct HistorySlice { + cpu: Vec, + mem: (u64, u64), + net: (u64, u64), + disks: (u64, u64), + temps: Vec, + timestamp: u128, +} + #[derive(Clone)] struct AppState { - latest: Arc>, - data: Arc>, + data: Arc>>, } impl Default for AppState { fn default() -> Self { AppState { - latest: Arc::new(Mutex::new(Instant::now())), - data: Arc::new(Mutex::new(json!({}))), + data: Arc::new(Mutex::new(Vec::new())), } } } @@ -29,22 +37,15 @@ impl Default for AppState { #[tokio::main] async fn main() { dotenv().ok(); - let refresh_interval = Duration::from_millis( - std::env::var("SERVER_REFRESH_INTERVAL") - .unwrap() - .parse::() - .unwrap(), - ); - let active_window = std::env::var("SERVER_ACTIVE_WINDOW") - .unwrap() - .parse::() - .unwrap(); + let refresh_interval = std::env::var("SERVER_REFRESH_INTERVAL").unwrap().parse::().unwrap(); + let steps = std::env::var("SERVER_STEPS").unwrap().parse::().unwrap(); let port = std::env::var("SERVER_PORT").unwrap(); let app_state = AppState::default(); let router = Router::new() - .route("/api/dynamic", get(dynamic_sysinfo_get)) .route("/api/static", get(static_sysinfo_get)) + .route("/api/history", get(history_sysinfo_get)) + .route("/api/dynamic", get(dynamic_sysinfo_get)) .with_state(app_state.clone()); // Update system usage in the background @@ -55,47 +56,59 @@ async fn main() { let mut disk_stats = DiskStats::new(); loop { - let now = Instant::now(); - let latest = app_state.latest.lock().unwrap().clone(); + sys.refresh_cpu(); + sys.refresh_memory(); + components.refresh(); + networks.refresh(); + disk_stats.refresh(); - if (now - latest).as_millis() < active_window { - sys.refresh_cpu(); - sys.refresh_memory(); - components.refresh(); - networks.refresh(); - disk_stats.refresh(); - - let cpu_usage: Vec<_> = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(); - let mem_usage = sys.used_memory(); - let swap_usage = sys.used_swap(); - let temps: Vec<_> = components + let cpu_usage: Vec<_> = sys.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(); + let mem_usage = sys.used_memory(); + let swap_usage = sys.used_swap(); + let temps: Vec<_> = components + .iter() + .map(|component| component.temperature()) + .collect(); + let (net_down, net_up) = + networks .iter() - .map(|component| component.temperature()) - .collect(); - let (net_down, net_up) = - networks - .iter() - .fold((0, 0), |(down, up), (_name, network)| { - (down + network.received(), up + network.transmitted()) - }); - - { - let mut data = app_state.data.lock().unwrap(); - *data = json!({ - "cpu_usage": cpu_usage, - "mem_usage": mem_usage, - "swap_usage": swap_usage, - "temps": temps, - "network": { - "down": net_down, - "up": net_up - }, - "disks": disk_stats.diff() + .fold((0, 0), |(down, up), (_name, network)| { + (down + network.received(), up + network.transmitted()) }); - } + + { + let disks_diff = disk_stats.diff(); + let mut data = app_state.data.lock().unwrap(); + + // Get the current time + let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + + // Find the index of the first element within the last 60 seconds + let first_index_to_keep = match data.iter().position(|item| { + item.timestamp >= current_time - (steps * refresh_interval) as u128 + }) { + Some(index) => index, + None => 0, + }; + + // Remove elements before the first index to keep + data.drain(..first_index_to_keep); + let slice = HistorySlice { + cpu: cpu_usage, + mem: (mem_usage, swap_usage), + net: (net_down, net_up), + disks: (disks_diff.read, disks_diff.write), + temps, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + }; + + (*data).push(slice); } - std::thread::sleep(refresh_interval); + std::thread::sleep(Duration::from_millis(refresh_interval)); } }); @@ -111,16 +124,22 @@ async fn main() { async fn dynamic_sysinfo_get(State(state): State) -> impl IntoResponse { let data = state.data.lock().unwrap().clone(); - { - let mut latest = state.latest.lock().unwrap(); - *latest = Instant::now() - } + Response::builder() + .header(axum::http::header::ORIGIN, "*") + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .header("content-type", "application/json") + .body(serde_json::to_string(&(data.last())).unwrap()) + .unwrap() +} +#[axum::debug_handler] +async fn history_sysinfo_get(State(state): State) -> impl IntoResponse { + let data = state.data.lock().unwrap().clone(); Response::builder() .header(axum::http::header::ORIGIN, "*") .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") .header("content-type", "application/json") - .body(data.to_string()) + .body(serde_json::to_string(&data).unwrap()) .unwrap() }