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) => (
-
- ))}
+ {Object.entries(COL_LABELS)
+ .filter(([id]) => id !== 'status')
+ .map(([id, label]) => (
+
+ ))}