diff --git a/Cargo.lock b/Cargo.lock
index c2eb6a7..feb4b1f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2528,9 +2528,21 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.1",
+ "tokio-macros",
"windows-sys 0.61.2",
]
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
diff --git a/Cargo.toml b/Cargo.toml
index 6aa4ce8..eada969 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,4 +17,4 @@ serde = { version = "1.0.228", features = ["serde_derive", "derive"] }
serde_derive = "1.0.228"
serde_json = "1.0.147"
sqlx = { version = "0.8", features = [ "runtime-tokio", "tls-native-tls", "sqlite" ] }
-tokio = "1.48.0"
+tokio = { version = "1.48.0", features = ["full"] }
diff --git a/frontend/src/components/server_stats.tsx b/frontend/src/components/server_stats.tsx
new file mode 100644
index 0000000..7c4ba94
--- /dev/null
+++ b/frontend/src/components/server_stats.tsx
@@ -0,0 +1,174 @@
+import type { Metrics } from "@/models/metrics";
+import type { MinimalServer } from "@/models/minimal_servers";
+import {
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "./ui/chart";
+import { Line, LineChart, YAxis } from "recharts";
+import { colors64 } from "@/lib/colors";
+import { Card } from "./ui/card";
+
+export function ServerStats({
+ server,
+ metrics,
+}: {
+ server: MinimalServer;
+ metrics: Metrics;
+}) {
+ function getServerName() {
+ if (server.nickname) {
+ return server.nickname;
+ }
+ if (server.hostname) {
+ return server.hostname;
+ }
+ if (server.name) {
+ return server.name;
+ }
+
+ return server.id.toString();
+ }
+
+ function getCputColor(cpu: string) {
+ const cpuIndex = parseInt(cpu.replace("CPU", ""));
+ if (cpuIndex >= 0 && cpuIndex < colors64.length) {
+ return colors64[cpuIndex];
+ }
+ return "#8884d8";
+ }
+
+ function getInterfaceColor(interfaceName: string) {
+ if (interfaceName.includes("RX")) {
+ return "#008000";
+ }
+ if (interfaceName.includes("TX")) {
+ return "#FF0000";
+ }
+ return "#8884d8";
+ }
+
+ function getDiskColor(diskName: string) {
+ if (diskName.includes("Read")) {
+ return "#0000FF";
+ }
+ if (diskName.includes("Write")) {
+ return "#FF00FF";
+ }
+ return "#8884d8";
+ }
+
+ return (
+
+
+
+
+ CPU Stats
+
+
+ } />
+ } />
+
+
+
+ {Object.keys(metrics.cpu[Object.keys(metrics.cpu)[0]]).map(
+ (cpu) => {
+ return (
+
+ );
+ },
+ )}
+
+
+
+
+ Network Stats
+
+
+ } />
+ } />
+
+
+
+ {Object.keys(
+ metrics.network[Object.keys(metrics.network)[0]],
+ ).map((interf) => {
+ return (
+
+ );
+ })}
+
+
+
+
+ Disk Stats
+
+
+ } />
+ } />
+
+
+
+ {Object.keys(metrics.disk[Object.keys(metrics.disk)[0]]).map(
+ (disk) => {
+ return (
+
+ );
+ },
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/lib/colors.ts b/frontend/src/lib/colors.ts
new file mode 100644
index 0000000..daedaf2
--- /dev/null
+++ b/frontend/src/lib/colors.ts
@@ -0,0 +1,67 @@
+export const colors64: string[] = [
+ "#0000F0",
+ "#FFFFFF",
+ "#FF0000",
+ "#00FF00",
+ "#0000FF",
+ "#FFFF00",
+ "#00FFFF",
+ "#FF00FF",
+ "#800000",
+ "#008000",
+ "#000080",
+ "#808000",
+ "#008080",
+ "#800080",
+ "#C0C0C0",
+ "#808080",
+ "#FFA500",
+ "#A52A2A",
+ "#8B4513",
+ "#DEB887",
+ "#D2691E",
+ "#CD853F",
+ "#F4A460",
+ "#DAA520",
+ "#B8860B",
+ "#FFD700",
+ "#F0E68C",
+ "#EEE8AA",
+ "#98FB98",
+ "#90EE90",
+ "#00FA9A",
+ "#2E8B57",
+ "#3CB371",
+ "#66CDAA",
+ "#20B2AA",
+ "#5F9EA0",
+ "#4682B4",
+ "#1E90FF",
+ "#6495ED",
+ "#87CEEB",
+ "#87CEFA",
+ "#00BFFF",
+ "#4169E1",
+ "#00008B",
+ "#191970",
+ "#6A5ACD",
+ "#7B68EE",
+ "#8A2BE2",
+ "#4B0082",
+ "#9400D3",
+ "#9932CC",
+ "#BA55D3",
+ "#DDA0DD",
+ "#FFC0CB",
+ "#FF69B4",
+ "#FF1493",
+ "#DB7093",
+ "#DC143C",
+ "#B22222",
+ "#FA8072",
+ "#E9967A",
+ "#F08080",
+ "#CD5C5C",
+ "#FF6347",
+ "#FF7F50",
+];
diff --git a/frontend/src/models/metrics.ts b/frontend/src/models/metrics.ts
new file mode 100644
index 0000000..ae51980
--- /dev/null
+++ b/frontend/src/models/metrics.ts
@@ -0,0 +1,6 @@
+export interface Metrics {
+ server_id: number;
+ cpu: any;
+ disk: any;
+ network: any;
+}
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index e1ecc12..82085c3 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -1,9 +1,51 @@
+import { ServerStats } from "@/components/server_stats";
+import { Card } from "@/components/ui/card";
+import type { Metrics } from "@/models/metrics";
+import type { MinimalServers } from "@/models/minimal_servers";
+import { useQueries, useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
+import { useEffect, useState } from "react";
export const Route = createFileRoute("/")({
component: App,
});
function App() {
- return Dashboard coming soon!
;
+ const { data: servers = [] } = useQuery({
+ queryKey: ["servers"],
+ queryFn: async () => {
+ const res = await fetch("/api/servers");
+ if (!res.ok) throw new Error("Failed to fetch servers");
+ return (await res.json()) as MinimalServers;
+ },
+ refetchInterval: 40000,
+ });
+
+ const { data: metrics = [] } = useQuery({
+ queryKey: ["serversMetrics", servers.map((s) => s.id).join("-")],
+ enabled: servers.length > 0,
+ queryFn: async () => {
+ const results = await Promise.all(
+ servers.map(async (server) => {
+ const res = await fetch(`/api/servers/${server.id}/metrics`);
+ if (!res.ok) throw new Error(`Failed metrics for ${server.id}`);
+ return (await res.json()) as Metrics;
+ }),
+ );
+ return results;
+ },
+ refetchInterval: 30000,
+ });
+
+ return (
+
+ {metrics.map((metric, index) => (
+
+
+ {metric && }
+
+
+ ))}
+
+ );
}
diff --git a/src/main.rs b/src/main.rs
index a8f4a36..ebee468 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -57,6 +57,7 @@ async fn main() -> std::io::Result<()> {
.service(servers::list_servers)
.service(servers::get_server)
.service(servers::update_server_hostname)
+ .service(servers::get_server_metrics)
.service(
Files::new("/", "./frontend/dist")
.index_file("index.html")
diff --git a/src/servers.rs b/src/servers.rs
index 4eb1a6b..4be7ce4 100644
--- a/src/servers.rs
+++ b/src/servers.rs
@@ -1,5 +1,6 @@
use actix_web::{Responder, get, patch, web};
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
use crate::helper;
@@ -55,3 +56,41 @@ pub async fn update_server_hostname(
serde_json::to_string(&response).unwrap()
}
+
+#[derive(Deserialize, Serialize)]
+struct Metrics {
+ server_id: i32,
+ cpu: Value,
+ disk: Value,
+ network: Value,
+}
+
+#[get("/api/servers/{id}/metrics")]
+pub async fn get_server_metrics(id: web::Path) -> impl Responder {
+ let config = helper::get_authed_api_config().await.unwrap();
+ let cpu_future = scp_core::apis::default_api::api_v1_servers_server_id_metrics_cpu_get(
+ &config,
+ id.clone(),
+ Some(48),
+ );
+
+ let disk_future = scp_core::apis::default_api::api_v1_servers_server_id_metrics_disk_get(
+ &config,
+ id.clone(),
+ Some(48),
+ );
+ let network_future = scp_core::apis::default_api::api_v1_servers_server_id_metrics_network_get(
+ &config,
+ id.clone(),
+ Some(48),
+ );
+
+ let result = Metrics {
+ server_id: id.into_inner(),
+ cpu: cpu_future.await.unwrap(),
+ disk: disk_future.await.unwrap(),
+ network: network_future.await.unwrap(),
+ };
+
+ serde_json::to_string(&result).unwrap()
+}