1// =============================================
2// Agent.jsx — Clean Rebuild (Header + Sidebar + Content)
3// =============================================
4import React, { useState, useEffect, Suspense, lazy } from "react";
5import { apiFetch } from "../lib/api";
6import { AgentSidebar } from "./AgentSidebar";
7import MainLayout from "../components/Layout/MainLayout";
8import { useParams } from "react-router-dom";
9import dashboardWS from "../services/dashboardWS";
12// Lazy-loaded tab components
13const TabSystem = lazy(() => import("./tabs/TabSystem"));
14const TabMetrics = lazy(() => import("./tabs/TabMetrics"));
15const TabRds = lazy(() => import("./tabs/TabRds"));
16const TabPrograms = lazy(() => import("./tabs/TabPrograms"));
17const TabTasks = lazy(() => import("./tabs/TabTasks"));
18const TabTerminal = lazy(() => import("./tabs/TabTerminal"));
19const TabServices = lazy(() => import("./tabs/TabServices"));
20const TabEvents = lazy(() => import("./tabs/TabEvents"));
21const TabLogs = lazy(() => import("./tabs/TabLogs"));
22const TabFiles = lazy(() => import("./tabs/TabFiles"));
23const TabScriptRunner = lazy(() => import("./tabs/TabScriptRunner"));
24const TabScriptIse = lazy(() => import("./tabs/TabScriptIse"));
26export default function Agent() {
27 const { agentId } = useParams();
28 const [activeTab, setActiveTab] = useState("system");
29 const [agentData, setAgentData] = useState(null);
30 const [wsData, setWsData] = useState(null);
31 const [statusLoading, setStatusLoading] = useState(true);
35 setStatusLoading(true);
37 // Fetch status, hardware info, and MeshCentral info
39 apiFetch(`/agent/${agentId}/status`).then(r => r.ok ? r.json() : null),
40 apiFetch(`/agent/${agentId}/hardware`).then(r => r.ok ? r.json() : null),
41 apiFetch(`/agent/${agentId}/meshcentral-info`).then(r => r.ok ? r.json() : null)
43 .then(([status, hardware, meshInfo]) => {
44 // Merge all data sources, prioritizing our agent data but filling in gaps with MeshCentral
47 hardware: hardware || {}
50 // If we have MeshCentral info, merge it intelligently
51 if (meshInfo && !meshInfo.error) {
52 // Use MeshCentral last seen if our agent hasn't been seen recently
53 if (meshInfo.lastSeen && (!mergedData.last_seen || new Date(meshInfo.lastSeen) > new Date(mergedData.last_seen))) {
54 mergedData.last_seen = meshInfo.lastSeen;
57 // Use MeshCentral hostname if ours is missing
58 if (!mergedData.hostname && meshInfo.hostname) {
59 mergedData.hostname = meshInfo.hostname;
62 // Use MeshCentral platform/OS if ours is missing
63 if (!mergedData.os && meshInfo.platform) {
64 mergedData.os = meshInfo.platform;
67 // Use MeshCentral IP if ours is missing
68 if (!mergedData.ip_address && meshInfo.lastAddress) {
69 mergedData.ip_address = meshInfo.lastAddress;
72 // Merge hardware info from MeshCentral if we don't have it locally
73 if (meshInfo.hardware) {
74 if (!mergedData.hardware.model && meshInfo.hardware.model) {
75 mergedData.hardware.model = `${meshInfo.hardware.manufacturer || ''} ${meshInfo.hardware.model}`.trim();
78 if (!mergedData.hardware.cpu_model && meshInfo.hardware.cpu_model) {
79 mergedData.hardware.cpu_model = meshInfo.hardware.cpu_model;
82 if (!mergedData.hardware.ram_bytes && meshInfo.hardware.ram_bytes) {
83 mergedData.hardware.ram_bytes = meshInfo.hardware.ram_bytes;
86 if (!mergedData.hardware.ipv4_address && meshInfo.hardware.ipv4_address) {
87 mergedData.hardware.ipv4_address = meshInfo.hardware.ipv4_address;
90 if ((!mergedData.hardware.drives || mergedData.hardware.drives.length === 0) && meshInfo.hardware.drives) {
91 mergedData.hardware.drives = meshInfo.hardware.drives;
95 // Store MeshCentral connection status
96 mergedData.meshcentral_connected = meshInfo.connected;
97 mergedData.meshcentral_nodeid = meshInfo.meshcentralNodeId;
100 setAgentData(mergedData);
103 console.error('Failed to load agent data:', err);
106 .finally(() => setStatusLoading(false));
109 // WebSocket integration for real-time data
111 if (!agentData?.agent_uuid) return;
113 // Subscribe to this agent's metrics
114 dashboardWS.subscribe([agentData.agent_uuid]);
116 const handleMetrics = (uuid, data) => {
117 if (uuid === agentData.agent_uuid) {
122 dashboardWS.on('metrics', handleMetrics);
125 dashboardWS.off('metrics', handleMetrics);
126 dashboardWS.unsubscribe([agentData.agent_uuid]);
128 }, [agentData?.agent_uuid]);
130 // Status helpers - Use meshcentral_connected from backend
131 const getStatusClass = (agent) => {
132 if (!agent) return "status-offline";
133 return agent.meshcentral_connected ? "status-online" : "status-offline";
135 const getStatusText = (agent) => {
136 if (!agent) return "Offline";
137 return agent.meshcentral_connected ? "Online" : formatLastSeen(agent.last_seen);
139 const formatLastSeen = (lastSeen) => {
140 if (!lastSeen) return "Never";
141 const date = new Date(lastSeen);
142 const now = new Date();
143 const diffMs = now - date;
144 const diffMinutes = Math.floor(diffMs / 1000 / 60);
145 const diffHours = Math.floor(diffMinutes / 60);
146 const diffDays = Math.floor(diffHours / 24);
147 if (diffMinutes < 1) return "Just now";
148 if (diffMinutes < 60) return `${diffMinutes}m ago`;
149 if (diffHours < 24) return `${diffHours}h ago`;
150 if (diffDays < 7) return `${diffDays}d ago`;
151 return date.toLocaleDateString();
154 // Shorthand formatters for header stats
155 const formatModelShort = (model) => {
156 if (!model) return "Unknown";
157 // Remove everything in parentheses and extra words
158 return model.replace(/\(.*?\)/g, "").replace(/Standard PC|PC/gi, "").trim() || model;
161 const formatCpuShort = (cpu) => {
162 if (!cpu) return "Unknown";
163 // Extract main CPU name: "Intel Core Ultra 7 265K" -> "Core Ultra 7 265K"
164 let cleaned = cpu.replace(/\(R\)|\(TM\)|\r|\n|Intel\(R\)|AMD/gi, "").trim();
165 // Remove "Processor" suffix
166 cleaned = cleaned.replace(/Processor$/i, "").trim();
168 return cleaned.substring(0, 30);
171 const formatBytesShort = (bytes) => {
172 if (!bytes || bytes === 0) return "0 GB";
173 const gb = bytes / (1024 * 1024 * 1024);
174 return `${gb.toFixed(0)} GB`;
177 const formatBytes = (bytes) => {
178 if (!bytes || bytes === 0) return "0 GB";
179 const gb = bytes / (1024 * 1024 * 1024);
180 return `${gb.toFixed(2)} GB`;
183 const renderContent = () => {
186 return <TabSystem key="tab-system" agentId={agentId} agentUuid={agentData?.agent_uuid} agent={agentData} />;
188 return <TabMetrics key="tab-metrics" agentId={agentId} wsData={wsData} />;
190 return <TabRds key="tab-rds" agentId={agentId} agentUuid={agentData?.agent_uuid} />;
192 return <TabPrograms key="tab-programs" agentId={agentId} />;
194 return <TabTasks key="tab-task" agentId={agentId} />;
196 return <TabTerminal key="tab-terminal" agentId={agentId} />;
198 return <TabServices key="tab-services" agentId={agentId} />;
200 return <TabEvents key="tab-events" agentId={agentId} agentUuid={agentData?.agent_uuid} agent={agentData} />;
202 return <TabLogs key="tab-logs" agentId={agentId} />;
204 return <TabFiles key="tab-file" agentId={agentId} />;
206 return <TabScriptRunner key="tab-script" agentId={agentId} agentUuid={agentData?.agent_uuid} agent={agentData} />;
208 return <TabScriptIse key="tab-ise" agentId={agentId} />;
210 return <div>Select a tab.</div>;
216 <div className="agent-page-layout">
218 <header className="agent-header-enhanced">
220 <div className="agent-header-loading">
221 <span className="status-indicator status-loading">Loading agent information...</span>
225 <div className="agent-header-top">
226 <div className="agent-title-section">
228 className={`status-dot ${getStatusClass(agentData)}`}
229 title={getStatusText(agentData)}
232 <h1>{agentData.hostname || "Unknown Agent"}</h1>
233 <div className="agent-subtitle">
234 <span>{agentData.os || "Unknown OS"}</span>
236 <span>v{agentData.version || "Unknown"}</span>
238 <span className={getStatusClass(agentData)}>
239 {getStatusText(agentData)}
246 <div className="agent-header-stats" style={{
248 gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
251 <div className="stat-card">
252 <span className="material-symbols-outlined stat-icon">computer</span>
253 <div className="stat-content">
254 <div className="stat-label">Computer</div>
255 <div className="stat-value">
256 {formatModelShort(agentData.hardware?.model)}
261 <div className="stat-card">
262 <span className="material-symbols-outlined stat-icon">settings</span>
263 <div className="stat-content">
264 <div className="stat-label">Processor</div>
265 <div className="stat-value">
266 {formatCpuShort(agentData.hardware?.cpu_model || agentData.hardware?.cpu)}
271 <div className="stat-card">
272 <span className="material-symbols-outlined stat-icon">memory</span>
273 <div className="stat-content">
274 <div className="stat-label">Memory</div>
275 <div className="stat-value">
276 {formatBytesShort(agentData.hardware?.ram_bytes || agentData.hardware?.memory)}
281 <div className="stat-card">
282 <span className="material-symbols-outlined stat-icon">storage</span>
283 <div className="stat-content">
284 <div className="stat-label">Storage</div>
285 <div className="stat-value">
287 // Use WebSocket disk data if available
288 if (wsData?.disk && Array.isArray(wsData.disk)) {
289 const totalBytes = wsData.disk.reduce((sum, d) => sum + (d.total || 0), 0);
290 return formatBytes(totalBytes);
292 // Fall back to API drives data
293 if (agentData.hardware?.drives && Array.isArray(agentData.hardware.drives)) {
294 const totalGB = agentData.hardware.drives.reduce((sum, d) => sum + (d.Total || 0), 0);
295 return formatBytes(totalGB * 1024 * 1024 * 1024);
303 <div className="stat-card">
304 <span className="material-symbols-outlined stat-icon">lan</span>
305 <div className="stat-content">
306 <div className="stat-label">IP Address</div>
307 <div className="stat-value">
308 {wsData?.network?.ipv4 ||
309 agentData.hardware?.ipv4_address ||
310 agentData.hardware?.ip_address ||
311 agentData.ip_address ||
317 <div className="stat-card">
318 <span className="material-symbols-outlined stat-icon">schedule</span>
319 <div className="stat-content">
320 <div className="stat-label">Last Seen</div>
321 <div className="stat-value">
322 {formatLastSeen(agentData.last_seen)}
329 <div className="agent-header-error">
330 <span>⚠️ Failed to load agent information</span>
335 {/* Sidebar + Content */}
336 <div className="agent-body">
337 <AgentSidebar selected={activeTab} onSelect={setActiveTab} />
339 <main className="agent-content">
340 <Suspense fallback={<div>Loading...</div>}>