diff --git a/Data/Server/WebUI/src/Devices/Device_Details.jsx b/Data/Server/WebUI/src/Devices/Device_Details.jsx index 47bcbe59..9eed9e28 100644 --- a/Data/Server/WebUI/src/Devices/Device_Details.jsx +++ b/Data/Server/WebUI/src/Devices/Device_Details.jsx @@ -43,6 +43,7 @@ export default function DeviceDetails({ device, onBack }) { const [tab, setTab] = useState(0); const [agent, setAgent] = useState(device || {}); const [details, setDetails] = useState({}); + const [meta, setMeta] = useState({}); const [softwareOrderBy, setSoftwareOrderBy] = useState("name"); const [softwareOrder, setSoftwareOrder] = useState("asc"); const [softwareSearch, setSoftwareSearch] = useState(""); @@ -94,27 +95,137 @@ export default function DeviceDetails({ device, onBack }) { }; 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 guid = device?.agent_guid || device?.guid || device?.agentGuid || device?.summary?.agent_guid; + const agentId = device?.agentId || device?.summary?.agent_id || device?.id; + const hostname = device?.hostname || device?.summary?.hostname; + if (!device || (!guid && !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 agentsPromise = fetch("/api/agents").catch(() => null); + let detailResponse = null; + if (guid) { + try { + detailResponse = await fetch(`/api/devices/${encodeURIComponent(guid)}`); + } catch (err) { + detailResponse = null; + } + } + if ((!detailResponse || !detailResponse.ok) && hostname) { + try { + detailResponse = await fetch(`/api/device/details/${encodeURIComponent(hostname)}`); + } catch (err) { + detailResponse = null; + } + } + if (!detailResponse || !detailResponse.ok) { + throw new Error(`Failed to load device record (${detailResponse ? detailResponse.status : 'no response'})`); + } + + const [agentsData, detailData] = await Promise.all([ + agentsPromise?.then((r) => (r ? r.json() : {})).catch(() => ({})), + detailResponse.json(), + ]); + + if (agentsData && agentId && agentsData[agentId]) { + setAgent({ id: agentId, ...agentsData[agentId] }); + } + + const summary = + detailData?.summary && typeof detailData.summary === "object" + ? detailData.summary + : (detailData?.details?.summary || {}); + const normalizedSummary = { ...(summary || {}) }; + if (detailData?.description) { + normalizedSummary.description = detailData.description; + } + + const normalized = { + summary: normalizedSummary, + memory: Array.isArray(detailData?.memory) + ? detailData.memory + : Array.isArray(detailData?.details?.memory) + ? detailData.details.memory + : [], + network: Array.isArray(detailData?.network) + ? detailData.network + : Array.isArray(detailData?.details?.network) + ? detailData.details.network + : [], + software: Array.isArray(detailData?.software) + ? detailData.software + : Array.isArray(detailData?.details?.software) + ? detailData.details.software + : [], + storage: Array.isArray(detailData?.storage) + ? detailData.storage + : Array.isArray(detailData?.details?.storage) + ? detailData.details.storage + : [], + cpu: detailData?.cpu || detailData?.details?.cpu || {}, + }; + setDetails(normalized); + + const toYmdHms = (dateObj) => { + if (!dateObj || Number.isNaN(dateObj.getTime())) return ''; + const pad = (v) => String(v).padStart(2, '0'); + return `${dateObj.getUTCFullYear()}-${pad(dateObj.getUTCMonth() + 1)}-${pad(dateObj.getUTCDate())} ${pad(dateObj.getUTCHours())}:${pad(dateObj.getUTCMinutes())}:${pad(dateObj.getUTCSeconds())}`; + }; + + let createdDisplay = normalizedSummary.created || ''; + if (!createdDisplay) { + if (detailData?.created_at && Number(detailData.created_at)) { + createdDisplay = toYmdHms(new Date(Number(detailData.created_at) * 1000)); + } else if (detailData?.created_at_iso) { + createdDisplay = toYmdHms(new Date(detailData.created_at_iso)); + } + } + + const metaPayload = { + hostname: detailData?.hostname || normalizedSummary.hostname || hostname || "", + lastUser: detailData?.last_user || normalizedSummary.last_user || "", + deviceType: detailData?.device_type || normalizedSummary.device_type || "", + created: createdDisplay, + createdAtIso: detailData?.created_at_iso || "", + lastSeen: detailData?.last_seen || normalizedSummary.last_seen || 0, + lastReboot: detailData?.last_reboot || normalizedSummary.last_reboot || "", + operatingSystem: + detailData?.operating_system || normalizedSummary.operating_system || normalizedSummary.agent_operating_system || "", + agentId: detailData?.agent_id || normalizedSummary.agent_id || agentId || "", + agentGuid: detailData?.agent_guid || normalizedSummary.agent_guid || guid || "", + agentHash: detailData?.agent_hash || normalizedSummary.agent_hash || "", + internalIp: detailData?.internal_ip || normalizedSummary.internal_ip || "", + externalIp: detailData?.external_ip || normalizedSummary.external_ip || "", + siteId: detailData?.site_id, + siteName: detailData?.site_name || "", + siteDescription: detailData?.site_description || "", + status: detailData?.status || "", + }; + setMeta(metaPayload); + setDescription(normalizedSummary.description || detailData?.description || ""); + + setAgent((prev) => ({ + ...(prev || {}), + id: agentId || prev?.id, + hostname: metaPayload.hostname || prev?.hostname, + agent_hash: metaPayload.agentHash || prev?.agent_hash, + agent_operating_system: metaPayload.operatingSystem || prev?.agent_operating_system, + device_type: metaPayload.deviceType || prev?.device_type, + last_seen: metaPayload.lastSeen || prev?.last_seen, + })); + + if (metaPayload.status) { + setLockedStatus(metaPayload.status); + } else if (metaPayload.lastSeen) { + setLockedStatus(statusFromHeartbeat(metaPayload.lastSeen)); } - const detailData = await detailsRes.json(); - setDetails(detailData || {}); - setDescription(detailData?.summary?.description || ""); } catch (e) { console.warn("Failed to load device info", e); + setMeta({}); } }; load(); @@ -151,9 +262,10 @@ export default function DeviceDetails({ device, onBack }) { }; const saveDescription = async () => { - if (!details.summary?.hostname) return; + const targetHost = meta.hostname || details.summary?.hostname; + if (!targetHost) return; try { - await fetch(`/api/device/description/${details.summary.hostname}`, { + await fetch(`/api/device/description/${targetHost}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ description }) @@ -162,6 +274,7 @@ export default function DeviceDetails({ device, onBack }) { ...d, summary: { ...(d.summary || {}), description } })); + setMeta((m) => ({ ...(m || {}), hostname: targetHost })); } catch (e) { console.warn("Failed to save description", e); } @@ -236,7 +349,7 @@ export default function DeviceDetails({ device, onBack }) { const summary = details.summary || {}; // Build a best-effort CPU display from summary fields const cpuInfo = useMemo(() => { - const cpu = summary.cpu || {}; + const cpu = details.cpu || summary.cpu || {}; const cores = cpu.logical_cores || cpu.cores || cpu.physical_cores; let ghz = cpu.base_clock_ghz; if (!ghz && typeof (summary.processor || '') === 'string') { @@ -253,24 +366,42 @@ export default function DeviceDetails({ device, onBack }) { }, [summary]); const summaryItems = [ - { label: "Hostname", value: summary.hostname || agent.hostname || device?.hostname || "unknown" }, - { label: "Operating System", value: summary.operating_system || agent.agent_operating_system || "unknown" }, - { label: "Processor", value: cpuInfo.display || "unknown" }, - { label: "Device Type", value: summary.device_type || "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) } + { label: "Hostname", value: meta.hostname || summary.hostname || agent.hostname || device?.hostname || "unknown" }, + { + label: "Last User", + value: ( + + + {meta.lastUser || summary.last_user || 'unknown'} + + ) + }, + { label: "Device Type", value: meta.deviceType || summary.device_type || 'unknown' }, + { + label: "Created", + value: meta.created ? formatDateTime(meta.created) : summary.created ? formatDateTime(summary.created) : 'unknown' + }, + { + label: "Last Seen", + value: formatLastSeen(meta.lastSeen || agent.last_seen || device?.lastSeen) + }, + { + label: "Last Reboot", + value: meta.lastReboot ? formatDateTime(meta.lastReboot) : summary.last_reboot ? formatDateTime(summary.last_reboot) : 'unknown' + }, + { label: "Operating System", value: meta.operatingSystem || summary.operating_system || agent.agent_operating_system || 'unknown' }, + { label: "Agent ID", value: meta.agentId || summary.agent_id || 'unknown' }, + { label: "Agent GUID", value: meta.agentGuid || summary.agent_guid || 'unknown' }, + { label: "Agent Hash", value: meta.agentHash || summary.agent_hash || 'unknown' }, ]; const MetricCard = ({ icon, title, main, sub, color }) => { @@ -693,9 +824,29 @@ export default function DeviceDetails({ device, onBack }) { const renderNetwork = () => { const rows = details.network || []; - if (!rows.length) return placeholderTable(["Adapter", "IP Address", "MAC Address"]); + const internalIp = meta.internalIp || summary.internal_ip || "unknown"; + const externalIp = meta.externalIp || summary.external_ip || "unknown"; + const ipHeader = ( + + + Internal IP: {internalIp || 'unknown'} + + + External IP: {externalIp || 'unknown'} + + + ); + if (!rows.length) { + return ( + + {ipHeader} + {placeholderTable(["Adapter", "IP Address", "MAC Address"])} + + ); + } return ( + {ipHeader} diff --git a/Data/Server/WebUI/src/Devices/Device_List.jsx b/Data/Server/WebUI/src/Devices/Device_List.jsx index fdbe35ae..a7ad04fe 100644 --- a/Data/Server/WebUI/src/Devices/Device_List.jsx +++ b/Data/Server/WebUI/src/Devices/Device_List.jsx @@ -51,6 +51,21 @@ function statusFromHeartbeat(tsSec, offlineAfter = 300) { return now - tsSec <= offlineAfter ? "Online" : "Offline"; } +function formatUptime(seconds) { + const total = Number(seconds); + if (!Number.isFinite(total) || total <= 0) return ""; + const parts = []; + const days = Math.floor(total / 86400); + if (days) parts.push(`${days}d`); + const hours = Math.floor((total % 86400) / 3600); + if (hours) parts.push(`${hours}h`); + const minutes = Math.floor((total % 3600) / 60); + if (minutes) parts.push(`${minutes}m`); + const secondsPart = Math.floor(total % 60); + if (!parts.length && secondsPart) parts.push(`${secondsPart}s`); + return parts.join(' '); +} + export default function DeviceList({ onSelectDevice }) { const [rows, setRows] = useState([]); const [orderBy, setOrderBy] = useState("status"); @@ -89,6 +104,17 @@ export default function DeviceList({ onSelectDevice }) { lastReboot: "Last Reboot", created: "Created", lastSeen: "Last Seen", + agentId: "Agent ID", + agentHash: "Agent Hash", + agentGuid: "Agent GUID", + domain: "Domain", + uptime: "Uptime", + memory: "Memory", + network: "Network", + software: "Software", + storage: "Storage", + cpu: "CPU", + siteDescription: "Site Description", }), [] ); @@ -114,19 +140,26 @@ export default function DeviceList({ onSelectDevice }) { const [filters, setFilters] = useState({}); const [filterAnchor, setFilterAnchor] = useState(null); // { id, anchorEl } - // Cache device details to avoid re-fetching every refresh - const [detailsByHost, setDetailsByHost] = useState({}); // hostname -> cached fields - const [siteMapping, setSiteMapping] = useState({}); // hostname -> { site_id, site_name } const [sites, setSites] = useState([]); // sites list for assignment const [assignDialogOpen, setAssignDialogOpen] = useState(false); const [assignSiteId, setAssignSiteId] = useState(null); const [assignTargets, setAssignTargets] = useState([]); // hostnames const [repoHash, setRepoHash] = useState(null); + const lastRepoFetchRef = useRef(0); - const fetchLatestRepoHash = useCallback(async () => { + const fetchLatestRepoHash = useCallback(async (options = {}) => { + const { force = false } = options || {}; + const now = Date.now(); + const elapsed = now - lastRepoFetchRef.current; + if (!force && repoHash && elapsed >= 0 && elapsed < 60_000) { + return repoHash; + } try { const params = new URLSearchParams({ repo: "bunny-lab-io/Borealis", branch: "main" }); + if (force) { + params.set("refresh", "1"); + } const resp = await fetch(`/api/repo/current_hash?${params.toString()}`); const json = await resp.json(); const sha = (json?.sha || "").trim(); @@ -135,14 +168,19 @@ export default function DeviceList({ onSelectDevice }) { err.response = json; throw err; } - setRepoHash((prev) => sha || prev || null); + lastRepoFetchRef.current = now; + setRepoHash((prev) => (sha ? sha : prev || null)); return sha || null; } catch (err) { console.warn("Failed to fetch repository hash", err); + if (!force && repoHash) { + return repoHash; + } + lastRepoFetchRef.current = now; setRepoHash((prev) => prev || null); return null; } - }, []); + }, [repoHash]); const computeAgentVersion = useCallback((agentHashValue, repoHashValue) => { const agentHash = (agentHashValue || "").trim(); @@ -152,195 +190,182 @@ export default function DeviceList({ onSelectDevice }) { return agentHash === repo ? "Up-to-Date" : "Needs Updated"; }, []); - const fetchAgents = useCallback(async (options = {}) => { + const fetchDevices = useCallback(async (options = {}) => { const { refreshRepo = false } = options || {}; let repoSha = repoHash; if (refreshRepo || !repoSha) { - const fetched = await fetchLatestRepoHash(); + const fetched = await fetchLatestRepoHash({ force: refreshRepo }); if (fetched) repoSha = fetched; } + + const hashById = new Map(); + const hashByGuid = new Map(); + const hashByHost = new Map(); try { - const hashById = new Map(); - const hashByGuid = new Map(); - const hashByHost = new Map(); - try { - const hashResp = await fetch('/api/agent/hash_list'); - if (hashResp.ok) { - const hashJson = await hashResp.json(); - const list = Array.isArray(hashJson?.agents) ? hashJson.agents : []; - list.forEach((rec) => { - if (!rec || typeof rec !== 'object') return; - const hash = (rec.agent_hash || '').trim(); - if (!hash) return; - const agentId = (rec.agent_id || '').trim(); - const guidRaw = (rec.agent_guid || '').trim().toLowerCase(); - const hostKey = (rec.hostname || '').trim().toLowerCase(); - const isMemory = (rec.source || '').trim() === 'memory'; - if (agentId && (!hashById.has(agentId) || isMemory)) { - hashById.set(agentId, hash); - } - if (guidRaw && (!hashByGuid.has(guidRaw) || isMemory)) { - hashByGuid.set(guidRaw, hash); - } - if (hostKey && (!hashByHost.has(hostKey) || isMemory)) { - hashByHost.set(hostKey, hash); - } - }); - } else { + const hashResp = await fetch('/api/agent/hash_list'); + if (hashResp.ok) { + const hashJson = await hashResp.json(); + const list = Array.isArray(hashJson?.agents) ? hashJson.agents : []; + list.forEach((rec) => { + if (!rec || typeof rec !== 'object') return; + const hash = (rec.agent_hash || '').trim(); + if (!hash) return; + const agentId = (rec.agent_id || '').trim(); + const guidRaw = (rec.agent_guid || '').trim().toLowerCase(); + const hostKey = (rec.hostname || '').trim().toLowerCase(); + const isMemory = (rec.source || '').trim() === 'memory'; + if (agentId && (!hashById.has(agentId) || isMemory)) { + hashById.set(agentId, hash); + } + if (guidRaw && (!hashByGuid.has(guidRaw) || isMemory)) { + hashByGuid.set(guidRaw, hash); + } + if (hostKey && (!hashByHost.has(hostKey) || isMemory)) { + hashByHost.set(hostKey, hash); + } + }); + } + } catch (err) { + console.warn('Failed to fetch agent hash list', err); + } + + try { + const res = await fetch('/api/devices'); + if (!res.ok) { + const err = new Error(`Failed to fetch devices (${res.status})`); + try { + err.response = await res.json(); + } catch {} + throw err; + } + const payload = await res.json(); + const list = Array.isArray(payload?.devices) ? payload.devices : []; + + const normalizeJson = (value) => { + if (!value) return ''; + try { + return JSON.stringify(value); + } catch { + return ''; + } + }; + + const normalized = list.map((device, index) => { + const summary = device && typeof device.summary === 'object' ? { ...device.summary } : {}; + const rawHostname = (device.hostname || summary.hostname || '').trim(); + const hostname = rawHostname || `device-${index + 1}`; + const agentId = (device.agent_id || summary.agent_id || '').trim(); + const guidRaw = (device.agent_guid || summary.agent_guid || '').trim(); + const guidLookupKey = guidRaw.toLowerCase(); + const rowKey = guidRaw || agentId || hostname || `device-${index + 1}`; + let agentHash = (device.agent_hash || summary.agent_hash || '').trim(); + if (agentId && hashById.has(agentId)) agentHash = hashById.get(agentId) || agentHash; + if (!agentHash && guidLookupKey && hashByGuid.has(guidLookupKey)) { + agentHash = hashByGuid.get(guidLookupKey) || agentHash; + } + const hostKey = hostname.trim().toLowerCase(); + if (!agentHash && hostKey && hashByHost.has(hostKey)) { + agentHash = hashByHost.get(hostKey) || agentHash; + } + const lastSeen = Number(device.last_seen || summary.last_seen || 0) || 0; + const status = device.status || statusFromHeartbeat(lastSeen); + + if (guidRaw && !summary.agent_guid) { + summary.agent_guid = guidRaw; + } + + let createdTs = Number(device.created_at || 0) || 0; + let createdDisplay = summary.created || ''; + if (!createdTs && createdDisplay) { + const parsed = Date.parse(createdDisplay.replace(' ', 'T')); + if (!Number.isNaN(parsed)) createdTs = Math.floor(parsed / 1000); + } + if (!createdDisplay && device.created_at_iso) { try { - const errPayload = await hashResp.json(); - if (errPayload?.error) { - console.warn('Failed to fetch agent hash list', errPayload.error); - } + createdDisplay = new Date(device.created_at_iso).toLocaleString(); } catch {} } - } catch (err) { - console.warn('Failed to fetch agent hash list', err); - } - const res = await fetch("/api/agents"); - const data = await res.json(); - const arr = Object.entries(data || {}).map(([id, a]) => { - const agentId = (id || '').trim(); - const hostname = a.hostname || agentId || "unknown"; - const normalizedHostKey = (hostname || '').trim().toLowerCase(); - const details = detailsByHost[hostname] || {}; - const rawGuid = (a.agent_guid || a.agentGuid || a.guid || '').trim(); - const detailGuid = (details.agentGuid || details.agent_guid || '').trim(); - const guidCandidates = [rawGuid, detailGuid].filter((g) => g && g.trim()); - let agentHash = ''; - if (agentId && hashById.has(agentId)) { - agentHash = hashById.get(agentId) || ''; - } - if (!agentHash) { - for (const candidate of guidCandidates) { - const key = (candidate || '').trim().toLowerCase(); - if (key && hashByGuid.has(key)) { - agentHash = hashByGuid.get(key) || ''; - break; - } - } - } - if (!agentHash && normalizedHostKey && hashByHost.has(normalizedHostKey)) { - agentHash = hashByHost.get(normalizedHostKey) || ''; - } - if (!agentHash) { - agentHash = (a.agent_hash || details.agentHash || '').trim(); - } - const agentGuidValue = rawGuid || detailGuid || ''; + const osName = + device.operating_system || + summary.operating_system || + summary.agent_operating_system || + "-"; + const type = (device.device_type || summary.device_type || '').trim(); + const lastUser = (device.last_user || summary.last_user || '').trim(); + const domain = (device.domain || summary.domain || '').trim(); + const internalIp = (device.internal_ip || summary.internal_ip || '').trim(); + const externalIp = (device.external_ip || summary.external_ip || '').trim(); + const lastReboot = (device.last_reboot || summary.last_reboot || '').trim(); + const uptimeSeconds = Number( + device.uptime || + summary.uptime_sec || + summary.uptime_seconds || + summary.uptime || + 0 + ) || 0; + + const memoryList = Array.isArray(device.memory) ? device.memory : []; + const networkList = Array.isArray(device.network) ? device.network : []; + const softwareList = Array.isArray(device.software) ? device.software : []; + const storageList = Array.isArray(device.storage) ? device.storage : []; + const cpuObj = + (device.cpu && typeof device.cpu === 'object' && device.cpu) || + (summary.cpu && typeof summary.cpu === 'object' ? summary.cpu : {}); + + const memoryDisplay = memoryList.length ? `${memoryList.length} module(s)` : ''; + const networkDisplay = networkList.length ? networkList.map((n) => n.adapter || n.name || '').filter(Boolean).join(', ') : ''; + const softwareDisplay = softwareList.length ? `${softwareList.length} item(s)` : ''; + const storageDisplay = storageList.length ? `${storageList.length} volume(s)` : ''; + const cpuDisplay = cpuObj.name || summary.processor || ''; + return { - id, + id: rowKey, hostname, - status: statusFromHeartbeat(a.last_seen), - lastSeen: a.last_seen || 0, - os: a.agent_operating_system || a.os || "-", - // Enriched fields from details cache - lastUser: details.lastUser || "", - type: a.device_type || details.type || "", - created: details.created || "", - createdTs: details.createdTs || 0, - internalIp: details.internalIp || "", - externalIp: details.externalIp || "", - lastReboot: details.lastReboot || "", - description: details.description || "", - agentGuid: agentGuidValue, + status, + lastSeen, + lastSeenDisplay: formatLastSeen(lastSeen), + os: osName, + lastUser, + type, + site: device.site_name || 'Not Configured', + siteId: device.site_id || null, + siteDescription: device.site_description || '', + description: (device.description || summary.description || '').trim(), + created: createdDisplay, + createdTs, + createdIso: device.created_at_iso || '', + agentGuid: guidRaw, agentHash, agentVersion: computeAgentVersion(agentHash, repoSha), + agentId, + domain, + internalIp, + externalIp, + lastReboot, + uptime: uptimeSeconds, + uptimeDisplay: formatUptime(uptimeSeconds), + memory: memoryDisplay, + memoryRaw: normalizeJson(memoryList), + network: networkDisplay, + networkRaw: normalizeJson(networkList), + software: softwareDisplay, + softwareRaw: normalizeJson(softwareList), + storage: storageDisplay, + storageRaw: normalizeJson(storageList), + cpu: cpuDisplay, + cpuRaw: normalizeJson(cpuObj), + summary, + details: device.details || {}, }; }); - setRows(arr); - // Fetch site mapping for these hostnames - try { - const hostCsv = arr.map((r) => r.hostname).filter(Boolean).map(encodeURIComponent).join(','); - const resp = await fetch(`/api/sites/device_map?hostnames=${hostCsv}`); - const mapData = await resp.json(); - const mapping = mapData?.mapping || {}; - setSiteMapping(mapping); - setRows((prev) => prev.map((r) => ({ ...r, site: mapping[r.hostname]?.site_name || "Not Configured" }))); - } catch {} - - // Fetch missing details (last_user, created) for hosts not cached yet - const hostsToFetch = arr - .map((r) => r.hostname) - .filter((h) => h && !detailsByHost[h]); - if (hostsToFetch.length) { - // Limit concurrency a bit - const chunks = []; - const size = 6; - for (let i = 0; i < hostsToFetch.length; i += size) { - chunks.push(hostsToFetch.slice(i, i + size)); - } - for (const chunk of chunks) { - await Promise.all( - chunk.map(async (h) => { - try { - const resp = await fetch(`/api/device/details/${encodeURIComponent(h)}`); - const det = await resp.json(); - const summary = det?.summary || {}; - const lastUser = summary.last_user || ""; - const createdRaw = summary.created || ""; - // Try to parse created to epoch seconds for sorting; fallback to 0 - let createdTs = 0; - if (createdRaw) { - const parsed = Date.parse(createdRaw.replace(" ", "T")); - createdTs = isNaN(parsed) ? 0 : Math.floor(parsed / 1000); - } - const deviceType = (summary.device_type || "").trim(); - const internalIp = summary.internal_ip || ""; - const externalIp = summary.external_ip || ""; - const lastReboot = summary.last_reboot || ""; - const description = summary.description || ""; - const agentHashValue = (summary.agent_hash || "").trim(); - const agentGuidValue = (summary.agent_guid || summary.agentGuid || "").trim(); - const enriched = { - lastUser, - created: createdRaw, - createdTs, - type: deviceType, - internalIp, - externalIp, - lastReboot, - description, - agentHash: agentHashValue, - agentGuid: agentGuidValue, - }; - setDetailsByHost((prev) => ({ - ...prev, - [h]: enriched, - })); - setRows((prev) => - prev.map((r) => { - if (r.hostname !== h) return r; - const nextHash = agentHashValue || r.agentHash; - return { - ...r, - lastUser: enriched.lastUser || r.lastUser, - type: enriched.type || r.type, - created: enriched.created || r.created, - createdTs: enriched.createdTs || r.createdTs, - internalIp: enriched.internalIp || r.internalIp, - externalIp: enriched.externalIp || r.externalIp, - lastReboot: enriched.lastReboot || r.lastReboot, - description: enriched.description || r.description, - agentGuid: agentGuidValue || r.agentGuid || "", - agentHash: nextHash, - agentVersion: computeAgentVersion(nextHash, repoSha), - }; - }) - ); - } catch { - // ignore per-host failure - } - }) - ); - } - } + setRows(normalized); } catch (e) { - console.warn("Failed to load agents:", e); + console.warn('Failed to load devices:', e); setRows([]); } - }, [detailsByHost, repoHash, fetchLatestRepoHash, computeAgentVersion]); + }, [repoHash, fetchLatestRepoHash, computeAgentVersion]); const fetchViews = useCallback(async () => { try { @@ -355,8 +380,8 @@ export default function DeviceList({ onSelectDevice }) { useEffect(() => { // Initial load only; removed auto-refresh interval - fetchAgents({ refreshRepo: true }); - }, [fetchAgents]); + fetchDevices({ refreshRepo: true }); + }, [fetchDevices]); useEffect(() => { fetchViews(); @@ -471,6 +496,28 @@ export default function DeviceList({ onSelectDevice }) { return formatCreated(row.created, row.createdTs); case "lastSeen": return formatLastSeen(row.lastSeen); + case "agentId": + return row.agentId || ""; + case "agentHash": + return row.agentHash || ""; + case "agentGuid": + return row.agentGuid || ""; + case "domain": + return row.domain || ""; + case "uptime": + return row.uptimeDisplay || (row.uptime ? String(row.uptime) : ""); + case "memory": + return row.memoryRaw || row.memory || ""; + case "network": + return row.networkRaw || row.network || ""; + case "software": + return row.softwareRaw || row.software || ""; + case "storage": + return row.storageRaw || row.storage || ""; + case "cpu": + return row.cpuRaw || row.cpu || ""; + case "siteDescription": + return row.siteDescription || ""; default: return ""; } @@ -485,9 +532,10 @@ export default function DeviceList({ onSelectDevice }) { const sorted = useMemo(() => { const dir = order === "asc" ? 1 : -1; return [...filtered].sort((a, b) => { - // Support numeric sort for created/lastSeen + // Support numeric sort for created/lastSeen/uptime if (orderBy === "lastSeen") return ((a.lastSeen || 0) - (b.lastSeen || 0)) * dir; if (orderBy === "created") return ((a.createdTs || 0) - (b.createdTs || 0)) * dir; + if (orderBy === "uptime") return ((a.uptime || 0) - (b.uptime || 0)) * dir; const A = a[orderBy]; const B = b[orderBy]; return String(A || "").localeCompare(String(B || "")) * dir; @@ -518,8 +566,11 @@ export default function DeviceList({ onSelectDevice }) { const handleDelete = async () => { if (!selected) return; + const targetAgentId = selected.agentId || selected.summary?.agent_id || selected.id; try { - await fetch(`/api/agent/${selected.id}`, { method: "DELETE" }); + if (targetAgentId) { + await fetch(`/api/agent/${encodeURIComponent(targetAgentId)}`, { method: "DELETE" }); + } } catch (e) { console.warn("Failed to remove agent", e); } @@ -682,7 +733,7 @@ export default function DeviceList({ onSelectDevice }) { fetchAgents({ refreshRepo: true })} + onClick={() => fetchDevices({ refreshRepo: true })} sx={{ color: "#bbb", mr: 1 }} > @@ -830,8 +881,30 @@ export default function DeviceList({ onSelectDevice }) { return ( {formatLastSeen(r.lastSeen)} ); + case "agentId": + return {r.agentId || ""}; + case "agentHash": + return {r.agentHash || ""}; + case "agentGuid": + return {r.agentGuid || ""}; + case "domain": + return {r.domain || ""}; + case "uptime": + return {r.uptimeDisplay || ''}; + case "memory": + return {r.memory || ""}; + case "network": + return {r.network || ""}; + case "software": + return {r.software || ""}; + case "storage": + return {r.storage || ""}; + case "cpu": + return {r.cpu || ""}; + case "siteDescription": + return {r.siteDescription || ""}; default: - return ; + return {String(r[col.id] || "")}; } })} @@ -952,44 +1025,30 @@ export default function DeviceList({ onSelectDevice }) { PaperProps={{ sx: { bgcolor: "#1e1e1e", color: '#fff', p: 1 } }} > - {[ - { id: 'agentVersion', label: 'Agent Version' }, - { id: 'site', label: 'Site' }, - { id: 'hostname', label: 'Hostname' }, - { id: 'os', label: 'Operating System' }, - { id: 'type', label: 'Device Type' }, - { id: 'lastUser', label: 'Last User' }, - { id: 'internalIp', label: 'Internal IP' }, - { id: 'externalIp', label: 'External IP' }, - { id: 'lastReboot', label: 'Last Reboot' }, - { id: 'created', label: 'Created' }, - { id: 'lastSeen', label: 'Last Seen' }, - { id: 'description', label: 'Description' }, - ].map((opt) => ( - e.stopPropagation()} sx={{ gap: 1 }}> - c.id === opt.id)} - onChange={(e) => { - const checked = e.target.checked; - setColumns((prev) => { - // Keep 'status' always present; manage others per toggle - const exists = prev.some((c) => c.id === opt.id); - if (checked) { - if (exists) return prev; - // Append new column at the end with canonical label - const label = COL_LABELS[opt.id] || opt.label || opt.id; - return [...prev, { id: opt.id, label }]; - } - // Remove column - return prev.filter((c) => c.id !== opt.id); - }); - }} - sx={{ p: 0.3, color: '#bbb' }} - /> - {opt.label} - - ))} + {Object.entries(COL_LABELS) + .filter(([id]) => id !== 'status') + .map(([id, label]) => ( + e.stopPropagation()} sx={{ gap: 1 }}> + c.id === id)} + onChange={(e) => { + const checked = e.target.checked; + setColumns((prev) => { + const exists = prev.some((c) => c.id === id); + if (checked) { + if (exists) return prev; + const nextLabel = COL_LABELS[id] || label || id; + return [...prev, { id, label: nextLabel }]; + } + return prev.filter((c) => c.id !== id); + }); + }} + sx={{ p: 0.3, color: '#bbb' }} + /> + {label || id} + + ))}