mirror of
				https://github.com/csehviktor/status-monitor.git
				synced 2025-08-08 18:06:14 +02:00 
			
		
		
		
	improve ui
This commit is contained in:
		| @@ -16,7 +16,7 @@ impl WebsocketRoutes { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn routes(self: Arc<Self>) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { |     pub fn routes(self: Arc<Self>) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { | ||||||
|         warp::path("agent") |         warp::path("ws") | ||||||
|             .and(warp::path::param::<String>()) |             .and(warp::path::param::<String>()) | ||||||
|             .and(warp::get()) |             .and(warp::get()) | ||||||
|             .and(warp::ws()) |             .and(warp::ws()) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@eslint/js": "^9.29.0", |         "@eslint/js": "^9.29.0", | ||||||
|  |         "@nanostores/react": "^1.0.0", | ||||||
|         "@tailwindcss/vite": "^4.1.11", |         "@tailwindcss/vite": "^4.1.11", | ||||||
|         "@types/react": "^19.1.8", |         "@types/react": "^19.1.8", | ||||||
|         "@types/react-dom": "^19.1.6", |         "@types/react-dom": "^19.1.6", | ||||||
| @@ -18,6 +19,7 @@ | |||||||
|         "eslint-plugin-react-hooks": "^5.2.0", |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|         "eslint-plugin-react-refresh": "^0.4.20", |         "eslint-plugin-react-refresh": "^0.4.20", | ||||||
|         "globals": "^16.2.0", |         "globals": "^16.2.0", | ||||||
|  |         "nanostores": "^1.0.1", | ||||||
|         "react-router-dom": "^7.6.3", |         "react-router-dom": "^7.6.3", | ||||||
|         "tailwindcss": "^4.1.11", |         "tailwindcss": "^4.1.11", | ||||||
|         "typescript": "~5.8.3", |         "typescript": "~5.8.3", | ||||||
| @@ -153,6 +155,8 @@ | |||||||
|  |  | ||||||
|     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], |     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], | ||||||
|  |  | ||||||
|  |     "@nanostores/react": ["@nanostores/react@1.0.0", "", { "peerDependencies": { "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0", "react": ">=18.0.0" } }, "sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A=="], | ||||||
|  |  | ||||||
|     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], |     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], | ||||||
|  |  | ||||||
|     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], |     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], | ||||||
| @@ -463,6 +467,8 @@ | |||||||
|  |  | ||||||
|     "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |     "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | ||||||
|  |  | ||||||
|  |     "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], | ||||||
|  |  | ||||||
|     "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], |     "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], | ||||||
|  |  | ||||||
|     "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], |     "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ | |||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@eslint/js": "^9.29.0", |         "@eslint/js": "^9.29.0", | ||||||
|  |         "@nanostores/react": "^1.0.0", | ||||||
|         "@tailwindcss/vite": "^4.1.11", |         "@tailwindcss/vite": "^4.1.11", | ||||||
|         "@types/react": "^19.1.8", |         "@types/react": "^19.1.8", | ||||||
|         "@types/react-dom": "^19.1.6", |         "@types/react-dom": "^19.1.6", | ||||||
| @@ -24,6 +25,7 @@ | |||||||
|         "eslint-plugin-react-hooks": "^5.2.0", |         "eslint-plugin-react-hooks": "^5.2.0", | ||||||
|         "eslint-plugin-react-refresh": "^0.4.20", |         "eslint-plugin-react-refresh": "^0.4.20", | ||||||
|         "globals": "^16.2.0", |         "globals": "^16.2.0", | ||||||
|  |         "nanostores": "^1.0.1", | ||||||
|         "react-router-dom": "^7.6.3", |         "react-router-dom": "^7.6.3", | ||||||
|         "tailwindcss": "^4.1.11", |         "tailwindcss": "^4.1.11", | ||||||
|         "typescript": "~5.8.3", |         "typescript": "~5.8.3", | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import { BrowserRouter, Route, Routes } from "react-router-dom"; | import { BrowserRouter, Route, Routes } from "react-router-dom"; | ||||||
| import { HomePage } from "@/pages/home"; | import { HomePage } from "@/pages/home"; | ||||||
|  | import { AgentPage } from "@/pages/agent"; | ||||||
|  |  | ||||||
| export default function App() { | export default function App() { | ||||||
|     return ( |     return ( | ||||||
|         <BrowserRouter> |         <BrowserRouter> | ||||||
|             <Routes> |             <Routes> | ||||||
|                 <Route path="/" element={<HomePage />} /> |                 <Route path="/" element={<HomePage />} /> | ||||||
|  |                 <Route path="/agents/:agent" element={<AgentPage />} /> | ||||||
|             </Routes> |             </Routes> | ||||||
|         </BrowserRouter> |         </BrowserRouter> | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { UptimeMessage } from "@/services/types"; | |||||||
| import { formatRelativeTime, isAgentOnline } from "@/services/utils"; | import { formatRelativeTime, isAgentOnline } from "@/services/utils"; | ||||||
| import { Progressbar } from "./Progressbar"; | import { Progressbar } from "./Progressbar"; | ||||||
|  |  | ||||||
| export type AgentOverviewCardProps = { | type AgentOverviewCardProps = { | ||||||
|     count: number; |     count: number; | ||||||
|     title: string; |     title: string; | ||||||
|     icon: { |     icon: { | ||||||
| @@ -42,13 +42,13 @@ export function AgentOverviewCard({ | |||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  |  | ||||||
| export type AgentCardProps = { | type AgentCardProps = { | ||||||
|     data: UptimeMessage; |     data: UptimeMessage; | ||||||
|     onClick?: () => void; |     onClick: () => void; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export function AgentCard({ props }: { props: AgentCardProps }) { | export function AgentCard({ data, onClick }: AgentCardProps) { | ||||||
|     const status = isAgentOnline(props.data) |     const status = isAgentOnline(data) | ||||||
|         ? { |         ? { | ||||||
|               name: "online", |               name: "online", | ||||||
|               color: "bg-green-500", |               color: "bg-green-500", | ||||||
| @@ -66,7 +66,7 @@ export function AgentCard({ props }: { props: AgentCardProps }) { | |||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <div |         <div | ||||||
|             onClick={() => props.onClick?.()} |             onClick={() => onClick()} | ||||||
|             className="bg-[#111111] border border-[#262626] rounded-lg p-6 hover:bg-[#151515] transition-all cursor-pointer group" |             className="bg-[#111111] border border-[#262626] rounded-lg p-6 hover:bg-[#151515] transition-all cursor-pointer group" | ||||||
|         > |         > | ||||||
|             <div className="flex items-center justify-between space-x-10"> |             <div className="flex items-center justify-between space-x-10"> | ||||||
| @@ -77,7 +77,7 @@ export function AgentCard({ props }: { props: AgentCardProps }) { | |||||||
|                         {status.name.toUpperCase()} |                         {status.name.toUpperCase()} | ||||||
|                     </div> |                     </div> | ||||||
|                     <h3 className="text-sm md:text-md lg:text-lg font-semibold text-white transition-colors"> |                     <h3 className="text-sm md:text-md lg:text-lg font-semibold text-white transition-colors"> | ||||||
|                         {props.data.agent} |                         {data.agent} | ||||||
|                     </h3> |                     </h3> | ||||||
|                 </div> |                 </div> | ||||||
|  |  | ||||||
| @@ -85,13 +85,11 @@ export function AgentCard({ props }: { props: AgentCardProps }) { | |||||||
|                     <div className="flex flex-col max-w-lg gap-y-2"> |                     <div className="flex flex-col max-w-lg gap-y-2"> | ||||||
|                         <Progressbar |                         <Progressbar | ||||||
|                             color={status.color} |                             color={status.color} | ||||||
|                             percentage={props.data.uptime} |                             percentage={data.uptime} | ||||||
|                         /> |                         /> | ||||||
|                         <div className="flex justify-between items-center text-xs"> |                         <div className="flex justify-between items-center text-xs"> | ||||||
|                             <span> |                             <span>{formatRelativeTime(data.last_seen)}</span> | ||||||
|                                 {formatRelativeTime(props.data.last_seen)} |                             <span>{data.uptime.toFixed(2)}%</span> | ||||||
|                             </span> |  | ||||||
|                             <span>{props.data.uptime.toFixed(2)}%</span> |  | ||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								ui/src/components/ConnectionStatus.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								ui/src/components/ConnectionStatus.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | export type WebsocketStatus = | ||||||
|  |     | "connected" | ||||||
|  |     | "connecting" | ||||||
|  |     | "disconnected" | ||||||
|  |     | "error"; | ||||||
|  |  | ||||||
|  | type ConnectionStatusProps = { | ||||||
|  |     status: WebsocketStatus; | ||||||
|  |     timestamp: string | undefined; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function ConnectionStatus({ status, timestamp }: ConnectionStatusProps) { | ||||||
|  |     const getConnectionStatus = (status: WebsocketStatus) => { | ||||||
|  |         switch (status) { | ||||||
|  |             case "connected": | ||||||
|  |                 return { | ||||||
|  |                     color: "bg-green-500", | ||||||
|  |                     text: "connected", | ||||||
|  |                     animation: "animate-pulse-slow", | ||||||
|  |                 }; | ||||||
|  |             case "connecting": | ||||||
|  |                 return { | ||||||
|  |                     color: "bg-yellow-500", | ||||||
|  |                     text: "connecting", | ||||||
|  |                     animation: "animate-pulse", | ||||||
|  |                 }; | ||||||
|  |             case "disconnected": | ||||||
|  |                 return { | ||||||
|  |                     color: "bg-gray-500", | ||||||
|  |                     text: "disconnected", | ||||||
|  |                     animation: "", | ||||||
|  |                 }; | ||||||
|  |             case "error": | ||||||
|  |                 return { | ||||||
|  |                     color: "bg-red-500", | ||||||
|  |                     text: "error", | ||||||
|  |                     animation: "animate-pulse", | ||||||
|  |                 }; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const config = getConnectionStatus(status); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className="flex items-center space-x-3 bg-[#141414] p-2 rounded-lg"> | ||||||
|  |             <div | ||||||
|  |                 className={`w-2 h-2 rounded-full mt-[3px] ${config.color} ${config.animation}`} | ||||||
|  |             /> | ||||||
|  |             <span className="text-sm text-gray-300 font-semibold"> | ||||||
|  |                 {config.text} | ||||||
|  |             </span> | ||||||
|  |             {timestamp && ( | ||||||
|  |                 <span className="text-xs text-gray-500"> | ||||||
|  |                     {new Date(timestamp).toLocaleTimeString()} | ||||||
|  |                 </span> | ||||||
|  |             )} | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { Zap } from "lucide-react"; | import { ChevronLeft, Zap } from "lucide-react"; | ||||||
|  | import { useNavigate } from "react-router-dom"; | ||||||
|  |  | ||||||
| export type HeaderProps = { | type HeaderProps = { | ||||||
|     title: string; |     title: string; | ||||||
|     subtitle: string; |     subtitle: string; | ||||||
|     hasBackButton: boolean; |     hasBackButton: boolean; | ||||||
| @@ -8,10 +9,21 @@ export type HeaderProps = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export function Header({ props }: { props: HeaderProps }) { | export function Header({ props }: { props: HeaderProps }) { | ||||||
|  |     const navigate = useNavigate(); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <header className="bg-[#0d0d0d] border-b border-[#191919]"> |         <header className="bg-[#0d0d0d] border-b border-[#191919]"> | ||||||
|             <div className="max-w-7xl mx-auto py-4 flex justify-between items-center"> |             <div className="max-w-7xl mx-auto py-4 flex justify-between items-center"> | ||||||
|                 <div className="flex items-center space-x-3"> |                 <div className="flex items-center space-x-3"> | ||||||
|  |                     {props.hasBackButton && ( | ||||||
|  |                         <button | ||||||
|  |                             onClick={() => navigate(-1)} | ||||||
|  |                             className="w-8 h-8 bg-[#262626] hover:bg-[#404040] rounded-lg flex items-center justify-center transition-colors" | ||||||
|  |                         > | ||||||
|  |                             <ChevronLeft /> | ||||||
|  |                         </button> | ||||||
|  |                     )} | ||||||
|  |  | ||||||
|                     <div className="w-11 h-11 text-yellow-500 flex items-center justify-center"> |                     <div className="w-11 h-11 text-yellow-500 flex items-center justify-center"> | ||||||
|                         <Zap className="w-9 h-9" /> |                         <Zap className="w-9 h-9" /> | ||||||
|                     </div> |                     </div> | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								ui/src/components/MetricCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ui/src/components/MetricCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | export type MetricStatus = "nil" | "good" | "warning" | "critical"; | ||||||
|  |  | ||||||
|  | type MetricCardProps = { | ||||||
|  |     title: string; | ||||||
|  |     value: string; | ||||||
|  |     status: MetricStatus; | ||||||
|  |     subtitle?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function MetricCard({ | ||||||
|  |     props, | ||||||
|  |     children, | ||||||
|  | }: { | ||||||
|  |     props: MetricCardProps; | ||||||
|  |     children: React.ReactNode; | ||||||
|  | }) { | ||||||
|  |     const indicateColor = getStatusColor(props.status); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className="bg-[#0d0d0d] rounded-lg p-6 shadow-lg hover:shadow-xl border border-[#191919]"> | ||||||
|  |             <div className="flex items-center justify-between mb-4"> | ||||||
|  |                 <h3 className="text-gray-300 text-sm font-medium uppercase tracking-wider"> | ||||||
|  |                     {props.title} | ||||||
|  |                 </h3> | ||||||
|  |                 <div | ||||||
|  |                     className={`w-3 h-3 rounded-full animate-pulse-slow ${indicateColor}`} | ||||||
|  |                 /> | ||||||
|  |             </div> | ||||||
|  |             <div className="space-y-2"> | ||||||
|  |                 <div className="text-2xl font-bold text-white"> | ||||||
|  |                     {props.value} | ||||||
|  |                 </div> | ||||||
|  |                 <div className="text-sm text-gray-500">{props.subtitle}</div> | ||||||
|  |                 <div>{children}</div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getStatusColor = (status: MetricStatus) => { | ||||||
|  |     switch (status) { | ||||||
|  |         case "nil": | ||||||
|  |             return "bg-gray-500"; | ||||||
|  |         case "good": | ||||||
|  |             return "bg-green-500"; | ||||||
|  |         case "warning": | ||||||
|  |             return "bg-yellow-500"; | ||||||
|  |         case "critical": | ||||||
|  |             return "bg-red-500"; | ||||||
|  |     } | ||||||
|  | }; | ||||||
							
								
								
									
										140
									
								
								ui/src/pages/agent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								ui/src/pages/agent.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | import { | ||||||
|  |     ConnectionStatus, | ||||||
|  |     WebsocketStatus, | ||||||
|  | } from "@/components/ConnectionStatus"; | ||||||
|  | import { Header } from "@/components/Header"; | ||||||
|  | import { MetricCard } from "@/components/MetricCard"; | ||||||
|  | import { getLastMessage, setLastMessage } from "@/services/store"; | ||||||
|  | import { StatusMessage } from "@/services/types"; | ||||||
|  | import { | ||||||
|  |     formatBytes, | ||||||
|  |     calcPercentage, | ||||||
|  |     formatPercentage, | ||||||
|  | } from "@/services/utils"; | ||||||
|  | import { initializeConnection } from "@/services/websocket"; | ||||||
|  | import { useEffect, useState } from "react"; | ||||||
|  | import { useParams } from "react-router-dom"; | ||||||
|  |  | ||||||
|  | export function AgentPage() { | ||||||
|  |     const { agent } = useParams(); | ||||||
|  |     const [status, setStatus] = useState<WebsocketStatus>(); | ||||||
|  |     const [message, setMessage] = useState<StatusMessage | null>( | ||||||
|  |         getLastMessage(), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (!agent) return; | ||||||
|  |  | ||||||
|  |         initializeConnection(agent, { | ||||||
|  |             onMessage: (data) => { | ||||||
|  |                 const parsed = JSON.parse(data); | ||||||
|  |  | ||||||
|  |                 setStatus("connected"); | ||||||
|  |                 setMessage(parsed); | ||||||
|  |                 setLastMessage(parsed); | ||||||
|  |             }, | ||||||
|  |             onOpen: () => setStatus("connecting"), | ||||||
|  |             onClose: () => setStatus("disconnected"), | ||||||
|  |             onError: () => setStatus("error"), | ||||||
|  |         }); | ||||||
|  |     }, [agent]); | ||||||
|  |  | ||||||
|  |     const { metrics } = message || {}; | ||||||
|  |  | ||||||
|  |     const getMetricsStatus = (percentage: number | undefined) => { | ||||||
|  |         if (!percentage) return "nil"; | ||||||
|  |  | ||||||
|  |         if (percentage < 50) return "good"; | ||||||
|  |         if (percentage < 80) return "warning"; | ||||||
|  |         return "critical"; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const cpuUsage = metrics?.cpu.usage ?? 0; | ||||||
|  |     const memoryUsage = calcPercentage( | ||||||
|  |         metrics?.memory.used, | ||||||
|  |         metrics?.memory.total, | ||||||
|  |     ); | ||||||
|  |     const diskUsage = calcPercentage( | ||||||
|  |         (metrics?.disk.total ?? 0) - (metrics?.disk.free ?? 0), | ||||||
|  |         metrics?.disk.total, | ||||||
|  |     ); | ||||||
|  |     const networkUsage = | ||||||
|  |         (metrics?.network.up ?? 0) + (metrics?.network.down ?? 0); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div> | ||||||
|  |             <Header | ||||||
|  |                 props={{ | ||||||
|  |                     title: "Status Monitor", | ||||||
|  |                     subtitle: agent!, | ||||||
|  |                     hasBackButton: true, | ||||||
|  |                     rightComponent: ( | ||||||
|  |                         <ConnectionStatus | ||||||
|  |                             status={status ?? "disconnected"} | ||||||
|  |                             timestamp={message?.timestamp} | ||||||
|  |                         /> | ||||||
|  |                     ), | ||||||
|  |                 }} | ||||||
|  |             /> | ||||||
|  |  | ||||||
|  |             <main className="max-w-7xl mx-auto py-8"> | ||||||
|  |                 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> | ||||||
|  |                     <MetricCard | ||||||
|  |                         props={{ | ||||||
|  |                             title: "CPU USAGE", | ||||||
|  |                             value: formatPercentage(cpuUsage), | ||||||
|  |                             status: getMetricsStatus(cpuUsage), | ||||||
|  |                             subtitle: `${metrics?.cpu.threads ?? 0} threads`, | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <h1>cpu chart</h1> | ||||||
|  |                     </MetricCard> | ||||||
|  |  | ||||||
|  |                     <MetricCard | ||||||
|  |                         props={{ | ||||||
|  |                             title: "MEMORY USAGE", | ||||||
|  |                             value: formatPercentage(memoryUsage), | ||||||
|  |                             status: getMetricsStatus(memoryUsage), | ||||||
|  |                             subtitle: `${formatBytes(metrics?.memory.used)} / ${formatBytes(metrics?.memory.total)}`, | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <h1>memory chart</h1> | ||||||
|  |                     </MetricCard> | ||||||
|  |  | ||||||
|  |                     <MetricCard | ||||||
|  |                         props={{ | ||||||
|  |                             title: "NETWORK ACTIVITY", | ||||||
|  |                             value: formatBytes(networkUsage), | ||||||
|  |                             status: getMetricsStatus(networkUsage), | ||||||
|  |                             subtitle: `↑ ${formatBytes(metrics?.network.up)}/s ↓ ${formatBytes(metrics?.network.down)}/s`, | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <h1>network chart</h1> | ||||||
|  |                     </MetricCard> | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 <MetricCard | ||||||
|  |                     props={{ | ||||||
|  |                         title: "DISK USAGE", | ||||||
|  |                         value: formatPercentage(diskUsage), | ||||||
|  |                         status: getMetricsStatus(diskUsage), | ||||||
|  |                         subtitle: `${formatBytes(metrics?.disk.free)} free of ${formatBytes(metrics?.disk.total)}`, | ||||||
|  |                     }} | ||||||
|  |                 > | ||||||
|  |                     <div className="w-full bg-[#141414] rounded-full h-4"> | ||||||
|  |                         <div | ||||||
|  |                             className={`h-4 rounded-full transition-all duration-500 ${ | ||||||
|  |                                 getMetricsStatus(diskUsage) == "good" | ||||||
|  |                                     ? "bg-green-500" | ||||||
|  |                                     : getMetricsStatus(diskUsage) == "warning" | ||||||
|  |                                       ? "bg-yellow-500" | ||||||
|  |                                       : "bg-red-500" | ||||||
|  |                             }`} | ||||||
|  |                             style={{ width: `${diskUsage}%` }} | ||||||
|  |                         /> | ||||||
|  |                     </div> | ||||||
|  |                 </MetricCard> | ||||||
|  |             </main> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | } | ||||||
| @@ -6,8 +6,10 @@ import { UptimeMessage } from "@/services/types"; | |||||||
| import { isAgentOnline } from "@/services/utils"; | import { isAgentOnline } from "@/services/utils"; | ||||||
| import { Box } from "lucide-react"; | import { Box } from "lucide-react"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
|  | import { useNavigate } from "react-router-dom"; | ||||||
|  |  | ||||||
| export function HomePage() { | export function HomePage() { | ||||||
|  |     const navigate = useNavigate(); | ||||||
|     const [agents, setAgents] = useState<UptimeMessage[]>([]); |     const [agents, setAgents] = useState<UptimeMessage[]>([]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
| @@ -82,7 +84,10 @@ export function HomePage() { | |||||||
|                             {agents.map((agent, index) => ( |                             {agents.map((agent, index) => ( | ||||||
|                                 <AgentCard |                                 <AgentCard | ||||||
|                                     key={index} |                                     key={index} | ||||||
|                                     props={{ data: agent }} |                                     data={agent} | ||||||
|  |                                     onClick={() => | ||||||
|  |                                         navigate(`/agents/${agent.agent}`) | ||||||
|  |                                     } | ||||||
|                                 /> |                                 /> | ||||||
|                             ))} |                             ))} | ||||||
|                         </div> |                         </div> | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								ui/src/services/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								ui/src/services/store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import type { StatusMessage } from "@/services/types"; | ||||||
|  | import { atom } from "nanostores"; | ||||||
|  |  | ||||||
|  | const store = atom<StatusMessage | null>(null); | ||||||
|  |  | ||||||
|  | export function getLastMessage(): StatusMessage | null { | ||||||
|  |     return store.get(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function setLastMessage(message: StatusMessage) { | ||||||
|  |     store.set(message); | ||||||
|  | } | ||||||
| @@ -6,7 +6,7 @@ export function isAgentOnline(data: UptimeMessage): boolean { | |||||||
|     return timeDiff < 10000; |     return timeDiff < 10000; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const formatRelativeTime = (timestamp: string): string => { | export function formatRelativeTime(timestamp: string): string { | ||||||
|     const now = new Date(); |     const now = new Date(); | ||||||
|     const past = new Date(timestamp); |     const past = new Date(timestamp); | ||||||
|     const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000); |     const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000); | ||||||
| @@ -25,4 +25,26 @@ export const formatRelativeTime = (timestamp: string): string => { | |||||||
|         const days = Math.floor(diffInSeconds / 86400); |         const days = Math.floor(diffInSeconds / 86400); | ||||||
|         return `${days}d ago`; |         return `${days}d ago`; | ||||||
|     } |     } | ||||||
| }; | } | ||||||
|  |  | ||||||
|  | export function formatBytes(bytes: number | undefined): string { | ||||||
|  |     if (!bytes || bytes === 0) return "0 B"; | ||||||
|  |  | ||||||
|  |     const k = 1024; | ||||||
|  |     const sizes = ["B", "KB", "MB", "GB", "TB"]; | ||||||
|  |     const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||||
|  |  | ||||||
|  |     return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function calcPercentage( | ||||||
|  |     value: number | undefined, | ||||||
|  |     total: number | undefined, | ||||||
|  | ): number { | ||||||
|  |     if (value === undefined || total === undefined) return 0; | ||||||
|  |     return (value / total) * 100; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function formatPercentage(val: number | undefined) { | ||||||
|  |     return `${(val ?? 0).toFixed(2)}%`; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | type Callbacks = { | ||||||
|  |     onMessage: (message: string) => void; | ||||||
|  |     onError?: (error: Event) => void; | ||||||
|  |     onOpen?: () => void; | ||||||
|  |     onClose?: () => void; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function initializeConnection(agent: string, callbacks: Callbacks) { | ||||||
|  |     const ws = new WebSocket(`ws://localhost:3000/ws/${agent}`); | ||||||
|  |  | ||||||
|  |     ws.onopen = () => callbacks.onOpen?.(); | ||||||
|  |     ws.onmessage = (event) => callbacks.onMessage(event.data); | ||||||
|  |     ws.onerror = (error) => callbacks.onError?.(error); | ||||||
|  |     ws.onclose = () => callbacks.onClose?.(); | ||||||
|  |  | ||||||
|  |     return () => ws.close(); | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user