1// SystemTab.jsx - Enhanced system information display
3import React from "react";
4import { apiFetch } from "../../lib/api";
5import MeshCentralEmbed, { VIEWMODE } from "../../components/MeshCentralEmbed";
7export default function TabSystem({ agentId, agentUuid, agent }) {
8 const [info, setInfo] = React.useState(null);
9 const [agentInfo, setAgentInfo] = React.useState(null);
10 const [loading, setLoading] = React.useState(true);
11 const [error, setError] = React.useState(null);
12 const [useMeshCentral, setUseMeshCentral] = React.useState(false);
13 const [meshInfo, setMeshInfo] = React.useState(null);
14 const [meshLoading, setMeshLoading] = React.useState(false);
16 // Check if MeshCentral is available
17 React.useEffect(() => {
18 if (!agentId && !agentUuid) return;
21 apiFetch(`/agent/${agentUuid || agentId}/meshcentral-url`)
22 .then((res) => res.ok ? res.json() : null)
24 if (data?.meshcentralNodeId) {
29 .finally(() => setMeshLoading(false));
30 }, [agentId, agentUuid]);
32 React.useEffect(() => {
36 // Fetch both hardware info and agent details
38 apiFetch(`/agent/${agentId}/hardware`).then(res => res.ok ? res.json() : null),
39 apiFetch(`/agent/${agentId}/status`).then(res => res.ok ? res.json() : null)
41 .then(([hardwareData, statusData]) => {
42 setInfo(hardwareData);
43 setAgentInfo(statusData);
45 .catch((err) => setError(err.message))
46 .finally(() => setLoading(false));
49 const formatBytes = (bytes) => {
50 if (!bytes) return 'N/A';
51 const gb = (bytes / (1024 * 1024 * 1024)).toFixed(2);
52 return `${gb} GB (${bytes.toLocaleString()} bytes)`;
55 const formatUptime = (seconds) => {
56 if (!seconds) return 'N/A';
57 const days = Math.floor(seconds / 86400);
58 const hours = Math.floor((seconds % 86400) / 3600);
59 const minutes = Math.floor((seconds % 3600) / 60);
60 return `${days}d ${hours}h ${minutes}m`;
63 const formatDate = (dateString) => {
64 if (!dateString) return 'N/A';
66 return new Date(dateString).toLocaleString();
72 if (loading || meshLoading) return <div className="card">Loading system information...</div>;
73 if (error) return <div className="card error">Error: {error}</div>;
74 if (!info && !agentInfo && !meshInfo?.available) return <div className="card">No information available.</div>;
77 <div className="tab-container">
78 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
79 <h2 style={{ margin: 0 }}>System Information</h2>
80 {meshInfo?.available && (
83 onClick={() => setUseMeshCentral(!useMeshCentral)}
84 style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
86 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
87 {useMeshCentral ? 'list' : 'desktop_windows'}
89 {useMeshCentral ? 'Switch to Legacy View' : 'Switch to MeshCentral'}
94 {/* MeshCentral Details View */}
95 {useMeshCentral && meshInfo?.available && (
96 <div className="card" style={{ padding: '16px', marginBottom: '16px' }}>
98 agentUuid={agent?.uuid || agentUuid}
99 viewMode={VIEWMODE.DETAILS}
100 title="System Details (MeshCentral)"
105 {/* Legacy System Information */}
106 {(!useMeshCentral || !meshInfo?.available) && (
108 <div className="card-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '16px' }}>
110 {/* Computer Information */}
111 <div className="card">
112 <h3><span className="material-symbols-outlined">computer</span> Computer</h3>
113 <table style={{ width: '100%', fontSize: '0.9rem' }}>
115 <tr><td><strong>Name:</strong></td><td>{info?.computerName || agentInfo?.hostname || 'N/A'}</td></tr>
116 <tr><td><strong>Manufacturer:</strong></td><td>{info?.manufacturer || 'N/A'}</td></tr>
117 <tr><td><strong>Model:</strong></td><td>{info?.model || 'N/A'}</td></tr>
118 <tr><td><strong>Serial:</strong></td><td>{info?.serial_number || 'N/A'}</td></tr>
119 <tr><td><strong>UUID:</strong></td><td style={{ fontSize: '0.75rem', wordBreak: 'break-all' }}>{info?.uuid || agentInfo?.hardware_uuid || 'N/A'}</td></tr>
124 {/* Operating System */}
125 <div className="card">
126 <h3><span className="material-symbols-outlined">desktop_windows</span> Operating System</h3>
127 <table style={{ width: '100%', fontSize: '0.9rem' }}>
129 <tr><td><strong>OS:</strong></td><td>{agentInfo?.os || info?.platform || 'N/A'}</td></tr>
130 <tr><td><strong>Version:</strong></td><td>{info?.os_version || 'N/A'}</td></tr>
131 <tr><td><strong>Build:</strong></td><td>{info?.os_build || 'N/A'}</td></tr>
132 <tr><td><strong>Architecture:</strong></td><td>{info?.arch || 'N/A'}</td></tr>
133 <tr><td><strong>Install Date:</strong></td><td>{formatDate(info?.os_install_date)}</td></tr>
134 <tr><td><strong>Last Reboot:</strong></td><td>{formatDate(info?.last_reboot)}</td></tr>
140 <div className="card">
141 <h3><span className="material-symbols-outlined">settings</span> Processor</h3>
142 <table style={{ width: '100%', fontSize: '0.9rem' }}>
144 <tr><td><strong>CPU:</strong></td><td>{info?.cpu_model || (info?.cpus && info.cpus[0]) || info?.cpu || 'N/A'}</td></tr>
145 <tr><td><strong>Cores:</strong></td><td>{info?.cpu_cores || (info?.cpus && info.cpus.length) || 'N/A'}</td></tr>
146 <tr><td><strong>Threads:</strong></td><td>{info?.cpu_threads || 'N/A'}</td></tr>
151 {/* Memory - Enhanced like Storage */}
152 <div className="card">
153 <h3><span className="material-symbols-outlined">memory</span> Memory</h3>
155 const totalBytes = info?.ram_bytes || info?.memory || info?.ram;
156 const totalGB = totalBytes ? (totalBytes / (1024 * 1024 * 1024)).toFixed(2) : null;
157 const usedBytes = info?.used_memory;
158 const usedGB = usedBytes ? (usedBytes / (1024 * 1024 * 1024)).toFixed(2) : null;
159 const freeBytes = info?.available_memory || info?.free_memory;
160 const freeGB = freeBytes ? (freeBytes / (1024 * 1024 * 1024)).toFixed(2) : null;
162 // Calculate percentage
163 let usedPercent = info?.memory_usage;
164 if (!usedPercent && usedGB && totalGB) {
165 usedPercent = ((usedGB / totalGB) * 100).toFixed(1);
170 <table style={{ width: '100%', fontSize: '0.9rem', marginBottom: '12px' }}>
172 <tr><td><strong>Total RAM:</strong></td><td>{totalGB} GB</td></tr>
173 <tr><td><strong>Used:</strong></td><td>{usedGB || 'N/A'} GB</td></tr>
174 <tr><td><strong>Available:</strong></td><td>{freeGB || 'N/A'} GB</td></tr>
180 <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '6px', fontSize: '0.9rem' }}>
181 <span>Memory Usage</span>
184 color: usedPercent > 90 ? '#dc3545' : usedPercent > 75 ? '#ffc107' : '#28a745'
192 background: '#e0e0e0',
197 width: `${usedPercent}%`,
199 background: usedPercent > 90 ? '#dc3545' : usedPercent > 75 ? '#ffc107' : '#007bff',
200 transition: 'width 0.3s ease'
207 <table style={{ width: '100%', fontSize: '0.9rem' }}>
209 <tr><td><strong>Total RAM:</strong></td><td>{formatBytes(totalBytes)}</td></tr>
210 <tr><td><strong>Available:</strong></td><td>N/A</td></tr>
211 <tr><td><strong>Used:</strong></td><td>N/A</td></tr>
219 <div className="card" style={{ gridColumn: info?.drives && Array.isArray(info.drives) && info.drives.length > 0 ? 'span 2' : 'span 1' }}>
220 <h3><span className="material-symbols-outlined">storage</span> Storage</h3>
222 {info?.drives && Array.isArray(info.drives) && info.drives.length > 0 ? (
224 {/* Drive Summary */}
225 <table style={{ width: '100%', fontSize: '0.9rem', marginBottom: '16px' }}>
227 <tr><td><strong>Total Drives:</strong></td><td>{info.drives.length}</td></tr>
229 <td><strong>Total Space:</strong></td>
230 <td>{info.drives.reduce((sum, d) => sum + (d.Total || 0), 0).toFixed(2)} GB</td>
233 <td><strong>Total Used:</strong></td>
234 <td>{info.drives.reduce((sum, d) => sum + (d.Used || 0), 0).toFixed(2)} GB</td>
237 <td><strong>Total Free:</strong></td>
238 <td>{info.drives.reduce((sum, d) => sum + (d.Free || 0), 0).toFixed(2)} GB</td>
243 {/* Individual Drives */}
244 <div style={{ borderTop: '1px solid #ddd', paddingTop: '12px' }}>
245 <strong style={{ fontSize: '0.95rem', marginBottom: '8px', display: 'block' }}>Individual Drives:</strong>
246 {info.drives.map((drive, idx) => {
247 // Calculate percentage if not provided
248 const usedPercent = drive.UsedPercent ||
249 (drive.Used && drive.Total ? ((drive.Used / drive.Total) * 100).toFixed(1) : 0);
252 <div key={idx} style={{
253 marginBottom: '12px',
255 background: '#f9f9f9',
257 borderLeft: '3px solid #007bff'
259 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' }}>
261 <strong style={{ fontSize: '1rem' }}>
262 <span className="material-symbols-outlined" style={{ fontSize: '18px', verticalAlign: 'middle', marginRight: '4px' }}>
265 Drive {drive.Drive || drive.Name}:
268 <span style={{ fontSize: '0.85rem', color: '#666', marginLeft: '8px' }}>
276 color: usedPercent > 90 ? '#dc3545' : usedPercent > 75 ? '#ffc107' : '#28a745'
286 background: '#e0e0e0',
292 width: `${usedPercent}%`,
294 background: usedPercent > 90 ? '#dc3545' : usedPercent > 75 ? '#ffc107' : '#007bff',
295 transition: 'width 0.3s ease'
300 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px', fontSize: '0.85rem' }}>
302 <div style={{ color: '#666' }}>Total</div>
303 <div style={{ fontWeight: '500' }}>{drive.Total} GB</div>
306 <div style={{ color: '#666' }}>Used</div>
307 <div style={{ fontWeight: '500', color: '#dc3545' }}>{drive.Used} GB</div>
310 <div style={{ color: '#666' }}>Free</div>
311 <div style={{ fontWeight: '500', color: '#28a745' }}>{drive.Free} GB</div>
320 <table style={{ width: '100%', fontSize: '0.9rem' }}>
322 <tr><td><strong>Drives:</strong></td><td>{info?.drive_count || 'N/A'}</td></tr>
323 <tr><td><strong>Total Space:</strong></td><td>{formatBytes(info?.total_disk)}</td></tr>
324 <tr><td><strong>Used Space:</strong></td><td>{formatBytes(info?.used_disk)}</td></tr>
325 <tr><td><strong>Free Space:</strong></td><td>{formatBytes(info?.free_disk)}</td></tr>
326 <tr><td><strong>Disk Usage:</strong></td><td>{info?.disk_usage ? `${info.disk_usage}%` : 'N/A'}</td></tr>
333 <div className="card">
334 <h3><span className="material-symbols-outlined">videogame_asset</span> Graphics</h3>
335 <table style={{ width: '100%', fontSize: '0.9rem' }}>
337 <tr><td><strong>GPU:</strong></td><td>{info?.gpu || 'Unknown'}</td></tr>
338 <tr><td><strong>Adapter:</strong></td><td>{info?.adapter || 'N/A'}</td></tr>
339 <tr><td><strong>Video RAM:</strong></td><td>{formatBytes(info?.video_memory)}</td></tr>
345 <div className="card">
346 <h3><span className="material-symbols-outlined">lan</span> Network</h3>
347 <table style={{ width: '100%', fontSize: '0.9rem' }}>
349 <tr><td><strong>IP Address:</strong></td><td>{info?.ip_address || agentInfo?.ip_address || 'N/A'}</td></tr>
350 <tr><td><strong>IPv6:</strong></td><td>{info?.ipv6_address || 'N/A'}</td></tr>
351 <tr><td><strong>MAC Address:</strong></td><td>{info?.mac_address || 'N/A'}</td></tr>
352 <tr><td><strong>Domain:</strong></td><td>{info?.domain || 'N/A'}</td></tr>
353 <tr><td><strong>Workgroup:</strong></td><td>{info?.workgroup || 'N/A'}</td></tr>
359 <div className="card">
360 <h3><span className="material-symbols-outlined">shield</span> Security</h3>
361 <table style={{ width: '100%', fontSize: '0.9rem' }}>
363 <tr><td><strong>Antivirus:</strong></td><td>{info?.antivirus || 'Not detected'}</td></tr>
364 <tr><td><strong>AV Version:</strong></td><td>{info?.antivirus_version || 'N/A'}</td></tr>
365 <tr><td><strong>Firewall:</strong></td><td>{info?.firewall_enabled ? 'Enabled' : 'Disabled'}</td></tr>
366 <tr><td><strong>BitLocker:</strong></td><td>{info?.bitlocker_key ? 'Encrypted' : info?.disk_encryption_status || 'Not encrypted'}</td></tr>
367 <tr><td><strong>Windows Updates:</strong></td><td>{info?.windows_update_status || 'N/A'}</td></tr>
372 {/* Agent Information */}
373 <div className="card">
374 <h3><span className="material-symbols-outlined">smart_toy</span> Agent</h3>
375 <table style={{ width: '100%', fontSize: '0.9rem' }}>
377 <tr><td><strong>Version:</strong></td><td>{agentInfo?.version || info?.agent_version || 'N/A'}</td></tr>
378 <tr><td><strong>Status:</strong></td><td>{agentInfo?.status || 'Unknown'}</td></tr>
379 <tr><td><strong>Last Seen:</strong></td><td>{formatDate(agentInfo?.last_seen)}</td></tr>
380 <tr><td><strong>Registered:</strong></td><td>{formatDate(agentInfo?.created_at)}</td></tr>
381 <tr><td><strong>Uptime:</strong></td><td>{formatUptime(info?.uptime)}</td></tr>
386 {/* User Information */}
387 {(info?.username || info?.user_list) && (
388 <div className="card">
389 <h3><span className="material-symbols-outlined">person</span> Users</h3>
390 <table style={{ width: '100%', fontSize: '0.9rem' }}>
392 <tr><td><strong>Current User:</strong></td><td>{info?.username || 'N/A'}</td></tr>
393 {info?.user_list && Array.isArray(info.user_list) && (
396 <strong>All Users:</strong>
397 <ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
398 {info.user_list.map((user, i) => (
399 <li key={i} style={{ fontSize: '0.85rem' }}>{user}</li>
410 {/* BIOS Information */}
411 {info?.bios_version && (
412 <div className="card">
413 <h3><span className="material-symbols-outlined">bolt</span> BIOS</h3>
414 <table style={{ width: '100%', fontSize: '0.9rem' }}>
416 <tr><td><strong>Version:</strong></td><td>{info.bios_version}</td></tr>
417 <tr><td><strong>Vendor:</strong></td><td>{info.bios_vendor || 'N/A'}</td></tr>
418 <tr><td><strong>Date:</strong></td><td>{info.bios_date || 'N/A'}</td></tr>