////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Device_Details.js import React, { useState, useEffect, useMemo } from "react"; import { Paper, Box, Tabs, Tab, Typography, Table, TableHead, TableRow, TableCell, TableBody, Button, LinearProgress, TableSortLabel, TextField } from "@mui/material"; export default function DeviceDetails({ device, onBack }) { const [tab, setTab] = useState(0); const [agent, setAgent] = useState(device || {}); const [details, setDetails] = useState({}); const [softwareOrderBy, setSoftwareOrderBy] = useState("name"); const [softwareOrder, setSoftwareOrder] = useState("asc"); const [softwareSearch, setSoftwareSearch] = useState(""); const [description, setDescription] = useState(""); // Snapshotted status for the lifetime of this page const [lockedStatus, setLockedStatus] = useState(() => { // Prefer status provided by the device list row if available if (device?.status) return device.status; // Fallback: compute once from the provided lastSeen timestamp const tsSec = device?.lastSeen; if (!tsSec) return "Offline"; const now = Date.now() / 1000; return now - tsSec <= 120 ? "Online" : "Offline"; }); const statusFromHeartbeat = (tsSec, offlineAfter = 120) => { if (!tsSec) return "Offline"; const now = Date.now() / 1000; return now - tsSec <= offlineAfter ? "Online" : "Offline"; }; const statusColor = (s) => (s === "Online" ? "#00d18c" : "#ff4f4f"); const formatLastSeen = (tsSec, offlineAfter = 120) => { if (!tsSec) return "unknown"; const now = Date.now() / 1000; if (now - tsSec <= offlineAfter) return "Currently Online"; const d = new Date(tsSec * 1000); const date = d.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric", }); const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", }); return `${date} @ ${time}`; }; useEffect(() => { // When navigating to a different device, take a fresh snapshot of its status if (device) { setLockedStatus(device.status || statusFromHeartbeat(device.lastSeen)); } if (!device || !device.hostname) return; const load = async () => { try { const [agentsRes, detailsRes] = await Promise.all([ fetch("/api/agents"), fetch(`/api/device/details/${device.hostname}`) ]); const agentsData = await agentsRes.json(); if (agentsData && agentsData[device.id]) { setAgent({ id: device.id, ...agentsData[device.id] }); } const detailData = await detailsRes.json(); setDetails(detailData || {}); setDescription(detailData?.summary?.description || ""); } catch (e) { console.warn("Failed to load device info", e); } }; load(); }, [device]); const saveDescription = async () => { if (!details.summary?.hostname) return; try { await fetch(`/api/device/description/${details.summary.hostname}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ description }) }); setDetails((d) => ({ ...d, summary: { ...(d.summary || {}), description } })); } catch (e) { console.warn("Failed to save description", e); } }; const formatDateTime = (str) => { if (!str) return "unknown"; try { const [datePart, timePart] = str.split(" "); const [y, m, d] = datePart.split("-").map(Number); let [hh, mm, ss] = timePart.split(":").map(Number); const ampm = hh >= 12 ? "PM" : "AM"; hh = hh % 12 || 12; return `${m.toString().padStart(2, "0")}/${d.toString().padStart(2, "0")}/${y} @ ${hh}:${mm .toString() .padStart(2, "0")} ${ampm}`; } catch { return str; } }; const formatMac = (mac) => (mac ? mac.replace(/-/g, ":").toUpperCase() : "unknown"); const formatBytes = (val) => { if (val === undefined || val === null || val === "unknown") return "unknown"; let num = Number(val); const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; while (num >= 1024 && i < units.length - 1) { num /= 1024; i++; } return `${num.toFixed(1)} ${units[i]}`; }; const handleSoftwareSort = (col) => { if (softwareOrderBy === col) { setSoftwareOrder(softwareOrder === "asc" ? "desc" : "asc"); } else { setSoftwareOrderBy(col); setSoftwareOrder("asc"); } }; const softwareRows = useMemo(() => { const rows = details.software || []; const filtered = rows.filter((s) => s.name.toLowerCase().includes(softwareSearch.toLowerCase()) ); const dir = softwareOrder === "asc" ? 1 : -1; return [...filtered].sort((a, b) => { const A = a[softwareOrderBy] || ""; const B = b[softwareOrderBy] || ""; return String(A).localeCompare(String(B)) * dir; }); }, [details.software, softwareSearch, softwareOrderBy, softwareOrder]); const summary = details.summary || {}; const summaryItems = [ { label: "Device Name", value: summary.hostname || agent.hostname || device?.hostname || "unknown" }, { label: "Operating System", value: summary.operating_system || agent.agent_operating_system || "unknown" }, { label: "Last User", value: summary.last_user || "unknown" }, { label: "Internal IP", value: summary.internal_ip || "unknown" }, { label: "External IP", value: summary.external_ip || "unknown" }, { label: "Last Reboot", value: summary.last_reboot ? formatDateTime(summary.last_reboot) : "unknown" }, { label: "Created", value: summary.created ? formatDateTime(summary.created) : "unknown" }, { label: "Last Seen", value: formatLastSeen(agent.last_seen || device?.lastSeen) } ]; const renderSummary = () => ( Description setDescription(e.target.value)} onBlur={saveDescription} placeholder="Enter description" sx={{ input: { color: "#fff" }, "& .MuiOutlinedInput-root": { "& fieldset": { borderColor: "#555" }, "&:hover fieldset": { borderColor: "#888" } } }} /> {summaryItems.map((item) => ( {item.label} {item.value} ))}
); const placeholderTable = (headers) => ( {headers.map((h) => ( {h} ))} No data available.
); const renderSoftware = () => { if (!softwareRows.length) return placeholderTable(["Software Name", "Version", "Action"]); return ( setSoftwareSearch(e.target.value)} sx={{ input: { color: "#fff" }, "& .MuiOutlinedInput-root": { "& fieldset": { borderColor: "#555" }, "&:hover fieldset": { borderColor: "#888" } } }} /> handleSoftwareSort("name")} > Software Name handleSoftwareSort("version")} > Version Action {softwareRows.map((s, i) => ( {s.name} {s.version} ))}
); }; const renderMemory = () => { const rows = details.memory || []; if (!rows.length) return placeholderTable(["Slot", "Speed", "Serial Number", "Capacity"]); return ( Slot Speed Serial Number Capacity {rows.map((m, i) => ( {m.slot} {m.speed} {m.serial} {formatBytes(m.capacity)} ))}
); }; const renderStorage = () => { const toNum = (val) => { if (val === undefined || val === null) return undefined; if (typeof val === "number") { return Number.isNaN(val) ? undefined : val; } const n = parseFloat(String(val).replace(/[^0-9.]+/g, "")); return Number.isNaN(n) ? undefined : n; }; const rows = (details.storage || []).map((d) => { const total = toNum(d.total); let usagePct = toNum(d.usage); let usedBytes = toNum(d.used); let freeBytes = toNum(d.free); let freePct; if (usagePct !== undefined) { if (usagePct <= 1) usagePct *= 100; freePct = 100 - usagePct; } if (usedBytes === undefined && total !== undefined && usagePct !== undefined) { usedBytes = (usagePct / 100) * total; } if (freeBytes === undefined && total !== undefined && usedBytes !== undefined) { freeBytes = total - usedBytes; } if (freePct === undefined && total !== undefined && freeBytes !== undefined) { freePct = (freeBytes / total) * 100; } if (usagePct === undefined && freePct !== undefined) { usagePct = 100 - freePct; } return { drive: d.drive, disk_type: d.disk_type, used: usedBytes, freePct, freeBytes, total, usage: usagePct, }; }); if (!rows.length) return placeholderTable([ "Drive Letter", "Disk Type", "Used", "Free %", "Free GB", "Total Size", "Usage", ]); return ( Drive Letter Disk Type Used Free % Free GB Total Size Usage {rows.map((d, i) => ( {d.drive} {d.disk_type} {d.used !== undefined && !Number.isNaN(d.used) ? formatBytes(d.used) : "unknown"} {d.freePct !== undefined && !Number.isNaN(d.freePct) ? `${d.freePct.toFixed(1)}%` : "unknown"} {d.freeBytes !== undefined && !Number.isNaN(d.freeBytes) ? formatBytes(d.freeBytes) : "unknown"} {d.total !== undefined && !Number.isNaN(d.total) ? formatBytes(d.total) : "unknown"} {d.usage !== undefined && !Number.isNaN(d.usage) ? `${d.usage.toFixed(1)}%` : "unknown"} ))}
); }; const renderNetwork = () => { const rows = details.network || []; if (!rows.length) return placeholderTable(["Adapter", "IP Address", "MAC Address"]); return ( Adapter IP Address MAC Address {rows.map((n, i) => ( {n.adapter} {(n.ips || []).join(", ")} {formatMac(n.mac)} ))}
); }; const tabs = [ { label: "Summary", content: renderSummary() }, { label: "Software", content: renderSoftware() }, { label: "Memory", content: renderMemory() }, { label: "Storage", content: renderStorage() }, { label: "Network", content: renderNetwork() } ]; // Use the snapshotted status so it stays static while on this page const status = lockedStatus || statusFromHeartbeat(agent.last_seen || device?.lastSeen); return ( {onBack && ( )} {agent.hostname || "Device Details"} setTab(v)} sx={{ borderBottom: 1, borderColor: "#333" }} > {tabs.map((t) => ( ))} {tabs[tab].content} ); }