mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 14:05:48 -07:00
Ensure device views use GUID identifiers
This commit is contained in:
@@ -43,6 +43,7 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
const [agent, setAgent] = useState(device || {});
|
const [agent, setAgent] = useState(device || {});
|
||||||
const [details, setDetails] = useState({});
|
const [details, setDetails] = useState({});
|
||||||
|
const [meta, setMeta] = useState({});
|
||||||
const [softwareOrderBy, setSoftwareOrderBy] = useState("name");
|
const [softwareOrderBy, setSoftwareOrderBy] = useState("name");
|
||||||
const [softwareOrder, setSoftwareOrder] = useState("asc");
|
const [softwareOrder, setSoftwareOrder] = useState("asc");
|
||||||
const [softwareSearch, setSoftwareSearch] = useState("");
|
const [softwareSearch, setSoftwareSearch] = useState("");
|
||||||
@@ -94,54 +95,137 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// When navigating to a different device, take a fresh snapshot of its status
|
|
||||||
if (device) {
|
if (device) {
|
||||||
setLockedStatus(device.status || statusFromHeartbeat(device.lastSeen));
|
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 () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const [agentsRes, detailsRes] = await Promise.all([
|
const agentsPromise = fetch("/api/agents").catch(() => null);
|
||||||
fetch("/api/agents"),
|
let detailResponse = null;
|
||||||
fetch(`/api/device/details/${device.hostname}`)
|
if (guid) {
|
||||||
]);
|
try {
|
||||||
const agentsData = await agentsRes.json();
|
detailResponse = await fetch(`/api/devices/${encodeURIComponent(guid)}`);
|
||||||
if (agentsData && agentsData[device.id]) {
|
} catch (err) {
|
||||||
setAgent({ id: device.id, ...agentsData[device.id] });
|
detailResponse = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const detailData = await detailsRes.json();
|
if ((!detailResponse || !detailResponse.ok) && hostname) {
|
||||||
const summary = detailData?.summary && typeof detailData.summary === 'object'
|
try {
|
||||||
? detailData.summary
|
detailResponse = await fetch(`/api/device/details/${encodeURIComponent(hostname)}`);
|
||||||
: (detailData?.details?.summary || {});
|
} 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 = {
|
const normalized = {
|
||||||
...(detailData?.details || {}),
|
summary: normalizedSummary,
|
||||||
summary: summary || {},
|
memory: Array.isArray(detailData?.memory)
|
||||||
memory: Array.isArray(detailData?.memory) ? detailData.memory : (detailData?.details?.memory || []),
|
? detailData.memory
|
||||||
network: Array.isArray(detailData?.network) ? detailData.network : (detailData?.details?.network || []),
|
: Array.isArray(detailData?.details?.memory)
|
||||||
software: Array.isArray(detailData?.software) ? detailData.software : (detailData?.details?.software || []),
|
? detailData.details.memory
|
||||||
storage: Array.isArray(detailData?.storage) ? detailData.storage : (detailData?.details?.storage || []),
|
: [],
|
||||||
|
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 || {},
|
cpu: detailData?.cpu || detailData?.details?.cpu || {},
|
||||||
};
|
};
|
||||||
if (detailData?.description) {
|
|
||||||
normalized.summary = { ...normalized.summary, description: detailData.description };
|
|
||||||
}
|
|
||||||
setDetails(normalized);
|
setDetails(normalized);
|
||||||
setDescription(normalized.summary?.description || detailData?.description || "");
|
|
||||||
|
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) => ({
|
setAgent((prev) => ({
|
||||||
...(prev || {}),
|
...(prev || {}),
|
||||||
id: device?.id || prev?.id,
|
id: agentId || prev?.id,
|
||||||
hostname: device?.hostname || normalized.summary?.hostname || prev?.hostname,
|
hostname: metaPayload.hostname || prev?.hostname,
|
||||||
agent_hash: detailData?.agent_hash || normalized.summary?.agent_hash || prev?.agent_hash,
|
agent_hash: metaPayload.agentHash || prev?.agent_hash,
|
||||||
agent_operating_system:
|
agent_operating_system: metaPayload.operatingSystem || prev?.agent_operating_system,
|
||||||
detailData?.operating_system ||
|
device_type: metaPayload.deviceType || prev?.device_type,
|
||||||
normalized.summary?.operating_system ||
|
last_seen: metaPayload.lastSeen || prev?.last_seen,
|
||||||
prev?.agent_operating_system,
|
|
||||||
device_type: detailData?.device_type || normalized.summary?.device_type || prev?.device_type,
|
|
||||||
last_seen: detailData?.last_seen || normalized.summary?.last_seen || prev?.last_seen,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (metaPayload.status) {
|
||||||
|
setLockedStatus(metaPayload.status);
|
||||||
|
} else if (metaPayload.lastSeen) {
|
||||||
|
setLockedStatus(statusFromHeartbeat(metaPayload.lastSeen));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load device info", e);
|
console.warn("Failed to load device info", e);
|
||||||
|
setMeta({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
load();
|
load();
|
||||||
@@ -178,9 +262,10 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveDescription = async () => {
|
const saveDescription = async () => {
|
||||||
if (!details.summary?.hostname) return;
|
const targetHost = meta.hostname || details.summary?.hostname;
|
||||||
|
if (!targetHost) return;
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/device/description/${details.summary.hostname}`, {
|
await fetch(`/api/device/description/${targetHost}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ description })
|
body: JSON.stringify({ description })
|
||||||
@@ -189,6 +274,7 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
...d,
|
...d,
|
||||||
summary: { ...(d.summary || {}), description }
|
summary: { ...(d.summary || {}), description }
|
||||||
}));
|
}));
|
||||||
|
setMeta((m) => ({ ...(m || {}), hostname: targetHost }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to save description", e);
|
console.warn("Failed to save description", e);
|
||||||
}
|
}
|
||||||
@@ -263,7 +349,7 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
const summary = details.summary || {};
|
const summary = details.summary || {};
|
||||||
// Build a best-effort CPU display from summary fields
|
// Build a best-effort CPU display from summary fields
|
||||||
const cpuInfo = useMemo(() => {
|
const cpuInfo = useMemo(() => {
|
||||||
const cpu = summary.cpu || {};
|
const cpu = details.cpu || summary.cpu || {};
|
||||||
const cores = cpu.logical_cores || cpu.cores || cpu.physical_cores;
|
const cores = cpu.logical_cores || cpu.cores || cpu.physical_cores;
|
||||||
let ghz = cpu.base_clock_ghz;
|
let ghz = cpu.base_clock_ghz;
|
||||||
if (!ghz && typeof (summary.processor || '') === 'string') {
|
if (!ghz && typeof (summary.processor || '') === 'string') {
|
||||||
@@ -280,24 +366,42 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
}, [summary]);
|
}, [summary]);
|
||||||
|
|
||||||
const summaryItems = [
|
const summaryItems = [
|
||||||
{ label: "Hostname", value: summary.hostname || agent.hostname || device?.hostname || "unknown" },
|
{ label: "Hostname", value: meta.hostname || 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: "Last User",
|
||||||
{ label: "Device Type", value: summary.device_type || "unknown" },
|
value: (
|
||||||
{ label: "Last User", value: (
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box
|
||||||
<Box component="span" sx={{
|
component="span"
|
||||||
display: 'inline-block', width: 10, height: 10, borderRadius: 10,
|
sx={{
|
||||||
bgcolor: agent?.collector_active ? '#00d18c' : '#ff4f4f'
|
display: 'inline-block',
|
||||||
}} />
|
width: 10,
|
||||||
<span>{summary.last_user || 'unknown'}</span>
|
height: 10,
|
||||||
</Box>
|
borderRadius: 10,
|
||||||
) },
|
bgcolor: agent?.collector_active ? '#00d18c' : '#ff4f4f'
|
||||||
{ 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" },
|
<span>{meta.lastUser || summary.last_user || 'unknown'}</span>
|
||||||
{ label: "Created", value: summary.created ? formatDateTime(summary.created) : "unknown" },
|
</Box>
|
||||||
{ label: "Last Seen", value: formatLastSeen(agent.last_seen || device?.lastSeen) }
|
)
|
||||||
|
},
|
||||||
|
{ 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 }) => {
|
const MetricCard = ({ icon, title, main, sub, color }) => {
|
||||||
@@ -720,9 +824,29 @@ export default function DeviceDetails({ device, onBack }) {
|
|||||||
|
|
||||||
const renderNetwork = () => {
|
const renderNetwork = () => {
|
||||||
const rows = details.network || [];
|
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 = (
|
||||||
|
<Box sx={{ mb: rows.length ? 1.5 : 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ display: 'block', opacity: 0.9 }}>
|
||||||
|
Internal IP: {internalIp || 'unknown'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ display: 'block', opacity: 0.9 }}>
|
||||||
|
External IP: {externalIp || 'unknown'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
if (!rows.length) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{ipHeader}
|
||||||
|
{placeholderTable(["Adapter", "IP Address", "MAC Address"])}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
{ipHeader}
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -240,9 +240,9 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
const rawHostname = (device.hostname || summary.hostname || '').trim();
|
const rawHostname = (device.hostname || summary.hostname || '').trim();
|
||||||
const hostname = rawHostname || `device-${index + 1}`;
|
const hostname = rawHostname || `device-${index + 1}`;
|
||||||
const agentId = (device.agent_id || summary.agent_id || '').trim();
|
const agentId = (device.agent_id || summary.agent_id || '').trim();
|
||||||
const id = agentId || hostname || `device-${index + 1}`;
|
|
||||||
const guidRaw = (device.agent_guid || summary.agent_guid || '').trim();
|
const guidRaw = (device.agent_guid || summary.agent_guid || '').trim();
|
||||||
const guidLookupKey = guidRaw.toLowerCase();
|
const guidLookupKey = guidRaw.toLowerCase();
|
||||||
|
const rowKey = guidRaw || agentId || hostname || `device-${index + 1}`;
|
||||||
let agentHash = (device.agent_hash || summary.agent_hash || '').trim();
|
let agentHash = (device.agent_hash || summary.agent_hash || '').trim();
|
||||||
if (agentId && hashById.has(agentId)) agentHash = hashById.get(agentId) || agentHash;
|
if (agentId && hashById.has(agentId)) agentHash = hashById.get(agentId) || agentHash;
|
||||||
if (!agentHash && guidLookupKey && hashByGuid.has(guidLookupKey)) {
|
if (!agentHash && guidLookupKey && hashByGuid.has(guidLookupKey)) {
|
||||||
@@ -305,7 +305,7 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
const cpuDisplay = cpuObj.name || summary.processor || '';
|
const cpuDisplay = cpuObj.name || summary.processor || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: rowKey,
|
||||||
hostname,
|
hostname,
|
||||||
status,
|
status,
|
||||||
lastSeen,
|
lastSeen,
|
||||||
@@ -551,8 +551,11 @@ export default function DeviceList({ onSelectDevice }) {
|
|||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
const targetAgentId = selected.agentId || selected.summary?.agent_id || selected.id;
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/agent/${selected.id}`, { method: "DELETE" });
|
if (targetAgentId) {
|
||||||
|
await fetch(`/api/agent/${encodeURIComponent(targetAgentId)}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to remove agent", e);
|
console.warn("Failed to remove agent", e);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user