mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:21:58 -06:00
Refactor device inventory storage and API
This commit is contained in:
@@ -111,8 +111,35 @@ export default function DeviceDetails({ device, onBack }) {
|
||||
setAgent({ id: device.id, ...agentsData[device.id] });
|
||||
}
|
||||
const detailData = await detailsRes.json();
|
||||
setDetails(detailData || {});
|
||||
setDescription(detailData?.summary?.description || "");
|
||||
const summary = detailData?.summary && typeof detailData.summary === 'object'
|
||||
? detailData.summary
|
||||
: (detailData?.details?.summary || {});
|
||||
const normalized = {
|
||||
...(detailData?.details || {}),
|
||||
summary: summary || {},
|
||||
memory: Array.isArray(detailData?.memory) ? detailData.memory : (detailData?.details?.memory || []),
|
||||
network: Array.isArray(detailData?.network) ? detailData.network : (detailData?.details?.network || []),
|
||||
software: Array.isArray(detailData?.software) ? detailData.software : (detailData?.details?.software || []),
|
||||
storage: Array.isArray(detailData?.storage) ? detailData.storage : (detailData?.details?.storage || []),
|
||||
cpu: detailData?.cpu || detailData?.details?.cpu || {},
|
||||
};
|
||||
if (detailData?.description) {
|
||||
normalized.summary = { ...normalized.summary, description: detailData.description };
|
||||
}
|
||||
setDetails(normalized);
|
||||
setDescription(normalized.summary?.description || detailData?.description || "");
|
||||
setAgent((prev) => ({
|
||||
...(prev || {}),
|
||||
id: device?.id || prev?.id,
|
||||
hostname: device?.hostname || normalized.summary?.hostname || prev?.hostname,
|
||||
agent_hash: detailData?.agent_hash || normalized.summary?.agent_hash || prev?.agent_hash,
|
||||
agent_operating_system:
|
||||
detailData?.operating_system ||
|
||||
normalized.summary?.operating_system ||
|
||||
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,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn("Failed to load device info", e);
|
||||
}
|
||||
|
||||
@@ -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,9 +140,6 @@ 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);
|
||||
@@ -152,195 +175,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();
|
||||
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 id = agentId || hostname || `device-${index + 1}`;
|
||||
const guidRaw = (device.agent_guid || summary.agent_guid || '').trim();
|
||||
const guidLookupKey = guidRaw.toLowerCase();
|
||||
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,
|
||||
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 +365,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 +481,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 +517,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;
|
||||
@@ -682,7 +715,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
<Tooltip title="Refresh Devices to Detect Changes">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => fetchAgents({ refreshRepo: true })}
|
||||
onClick={() => fetchDevices({ refreshRepo: true })}
|
||||
sx={{ color: "#bbb", mr: 1 }}
|
||||
>
|
||||
<CachedIcon fontSize="small" />
|
||||
@@ -830,8 +863,30 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
return (
|
||||
<TableCell key={col.id}>{formatLastSeen(r.lastSeen)}</TableCell>
|
||||
);
|
||||
case "agentId":
|
||||
return <TableCell key={col.id}>{r.agentId || ""}</TableCell>;
|
||||
case "agentHash":
|
||||
return <TableCell key={col.id}>{r.agentHash || ""}</TableCell>;
|
||||
case "agentGuid":
|
||||
return <TableCell key={col.id}>{r.agentGuid || ""}</TableCell>;
|
||||
case "domain":
|
||||
return <TableCell key={col.id}>{r.domain || ""}</TableCell>;
|
||||
case "uptime":
|
||||
return <TableCell key={col.id}>{r.uptimeDisplay || ''}</TableCell>;
|
||||
case "memory":
|
||||
return <TableCell key={col.id}>{r.memory || ""}</TableCell>;
|
||||
case "network":
|
||||
return <TableCell key={col.id}>{r.network || ""}</TableCell>;
|
||||
case "software":
|
||||
return <TableCell key={col.id}>{r.software || ""}</TableCell>;
|
||||
case "storage":
|
||||
return <TableCell key={col.id}>{r.storage || ""}</TableCell>;
|
||||
case "cpu":
|
||||
return <TableCell key={col.id}>{r.cpu || ""}</TableCell>;
|
||||
case "siteDescription":
|
||||
return <TableCell key={col.id}>{r.siteDescription || ""}</TableCell>;
|
||||
default:
|
||||
return <TableCell key={col.id} />;
|
||||
return <TableCell key={col.id}>{String(r[col.id] || "")}</TableCell>;
|
||||
}
|
||||
})}
|
||||
<TableCell align="right">
|
||||
@@ -952,44 +1007,30 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: '#fff', p: 1 } }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, 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) => (
|
||||
<MenuItem key={opt.id} disableRipple onClick={(e) => e.stopPropagation()} sx={{ gap: 1 }}>
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={columns.some((c) => 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' }}
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: '#ddd' }}>{opt.label}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
{Object.entries(COL_LABELS)
|
||||
.filter(([id]) => id !== 'status')
|
||||
.map(([id, label]) => (
|
||||
<MenuItem key={id} disableRipple onClick={(e) => e.stopPropagation()} sx={{ gap: 1 }}>
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={columns.some((c) => 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' }}
|
||||
/>
|
||||
<Typography variant="body2" sx={{ color: '#ddd' }}>{label || id}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Box sx={{ display: 'flex', gap: 1, pt: 0.5 }}>
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -463,7 +463,7 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
if row_guid:
|
||||
payload['agent_guid'] = row_guid
|
||||
return payload
|
||||
cur.execute('SELECT hostname, agent_hash, details, guid FROM device_details')
|
||||
cur.execute(f'SELECT hostname, agent_hash, details, guid FROM {DEVICE_TABLE}')
|
||||
for host, db_hash, details_json, row_guid in cur.fetchall():
|
||||
try:
|
||||
data = json.loads(details_json or '{}')
|
||||
@@ -528,7 +528,7 @@ def _lookup_agent_hash_by_guid(agent_guid: str) -> Optional[Dict[str, Any]]:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
'SELECT hostname, agent_hash, details FROM device_details WHERE guid = ?',
|
||||
f'SELECT hostname, agent_hash, details FROM {DEVICE_TABLE} WHERE guid = ?',
|
||||
(normalized_guid,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -638,7 +638,7 @@ def _collect_agent_hash_records() -> List[Dict[str, Any]]:
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT hostname, agent_hash, details, guid FROM device_details')
|
||||
cur.execute(f'SELECT hostname, agent_hash, details, guid FROM {DEVICE_TABLE}')
|
||||
for hostname, stored_hash, details_json, row_guid in cur.fetchall():
|
||||
try:
|
||||
details = json.loads(details_json or '{}')
|
||||
@@ -719,15 +719,16 @@ def _apply_agent_hash_update(agent_id: str, agent_hash: str, agent_guid: Optiona
|
||||
updated_via_guid = False
|
||||
if normalized_guid:
|
||||
cur.execute(
|
||||
'SELECT hostname, details FROM device_details WHERE guid = ?',
|
||||
f'SELECT hostname, description, details, created_at, agent_hash FROM {DEVICE_TABLE} WHERE guid = ?',
|
||||
(normalized_guid,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
updated_via_guid = True
|
||||
hostname = row[0]
|
||||
description = row[1]
|
||||
try:
|
||||
details = json.loads(row[1] or '{}')
|
||||
details = json.loads(row[2] or '{}')
|
||||
except Exception:
|
||||
details = {}
|
||||
summary = details.setdefault('summary', {})
|
||||
@@ -737,9 +738,16 @@ def _apply_agent_hash_update(agent_id: str, agent_hash: str, agent_guid: Optiona
|
||||
summary['agent_id'] = resolved_agent_id
|
||||
summary['agent_hash'] = agent_hash
|
||||
summary['agent_guid'] = normalized_guid
|
||||
cur.execute(
|
||||
'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?',
|
||||
(agent_hash, json.dumps(details), hostname),
|
||||
existing_created_at = row[3] if len(row) > 3 else None
|
||||
existing_hash = row[4] if len(row) > 4 else None
|
||||
_device_upsert(
|
||||
cur,
|
||||
hostname,
|
||||
description,
|
||||
details,
|
||||
existing_created_at,
|
||||
agent_hash=agent_hash or existing_hash,
|
||||
guid=normalized_guid,
|
||||
)
|
||||
conn.commit()
|
||||
elif not agent_id:
|
||||
@@ -774,8 +782,22 @@ def _apply_agent_hash_update(agent_id: str, agent_hash: str, agent_guid: Optiona
|
||||
if resolved_agent_id:
|
||||
summary['agent_id'] = resolved_agent_id
|
||||
cur.execute(
|
||||
'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?',
|
||||
(agent_hash, json.dumps(details), hostname),
|
||||
f'SELECT description, created_at, agent_hash FROM {DEVICE_TABLE} WHERE hostname = ?',
|
||||
(hostname,),
|
||||
)
|
||||
info_row = cur.fetchone()
|
||||
description = info_row[0] if info_row else None
|
||||
existing_created_at = info_row[1] if info_row else None
|
||||
existing_hash = info_row[2] if info_row else None
|
||||
effective_guid = normalized_guid or target.get('guid')
|
||||
_device_upsert(
|
||||
cur,
|
||||
hostname,
|
||||
description,
|
||||
details,
|
||||
existing_created_at,
|
||||
agent_hash=agent_hash or existing_hash,
|
||||
guid=effective_guid,
|
||||
)
|
||||
conn.commit()
|
||||
if not normalized_guid:
|
||||
@@ -1054,6 +1076,7 @@ def api_login():
|
||||
cur.execute("UPDATE users SET last_login=?, updated_at=? WHERE id=?", (now, now, row[0]))
|
||||
conn.commit()
|
||||
conn.commit()
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# set session cookie
|
||||
session['username'] = row[1]
|
||||
@@ -2427,6 +2450,203 @@ latest_images: Dict[str, Dict] = {}
|
||||
DB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "database.db"))
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
|
||||
DEVICE_TABLE = "devices"
|
||||
_DEVICE_JSON_LIST_FIELDS = {
|
||||
"memory": [],
|
||||
"network": [],
|
||||
"software": [],
|
||||
"storage": [],
|
||||
}
|
||||
_DEVICE_JSON_OBJECT_FIELDS = {
|
||||
"cpu": {},
|
||||
}
|
||||
|
||||
|
||||
def _serialize_device_json(value: Any, default: Any) -> str:
|
||||
candidate = value
|
||||
if candidate is None:
|
||||
candidate = default
|
||||
if not isinstance(candidate, (list, dict)):
|
||||
candidate = default
|
||||
try:
|
||||
return json.dumps(candidate)
|
||||
except Exception:
|
||||
try:
|
||||
return json.dumps(default)
|
||||
except Exception:
|
||||
return "{}" if isinstance(default, dict) else "[]"
|
||||
|
||||
|
||||
def _clean_device_str(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
text = str(value)
|
||||
elif isinstance(value, str):
|
||||
text = value
|
||||
else:
|
||||
try:
|
||||
text = str(value)
|
||||
except Exception:
|
||||
return None
|
||||
text = text.strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _coerce_int(value: Any) -> Optional[int]:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
if isinstance(value, str) and value.strip() == "":
|
||||
return None
|
||||
return int(float(value))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _extract_device_columns(details: Dict[str, Any]) -> Dict[str, Any]:
|
||||
summary = details.get("summary") or {}
|
||||
payload = {}
|
||||
|
||||
for field, default in _DEVICE_JSON_LIST_FIELDS.items():
|
||||
payload[field] = _serialize_device_json(details.get(field), default)
|
||||
payload["cpu"] = _serialize_device_json(summary.get("cpu") or details.get("cpu"), _DEVICE_JSON_OBJECT_FIELDS["cpu"])
|
||||
|
||||
payload["device_type"] = _clean_device_str(summary.get("device_type") or summary.get("type"))
|
||||
payload["domain"] = _clean_device_str(summary.get("domain"))
|
||||
payload["external_ip"] = _clean_device_str(summary.get("external_ip") or summary.get("public_ip"))
|
||||
payload["internal_ip"] = _clean_device_str(summary.get("internal_ip") or summary.get("private_ip"))
|
||||
payload["last_reboot"] = _clean_device_str(summary.get("last_reboot") or summary.get("last_boot"))
|
||||
payload["last_seen"] = _coerce_int(summary.get("last_seen"))
|
||||
payload["last_user"] = _clean_device_str(
|
||||
summary.get("last_user")
|
||||
or summary.get("last_user_name")
|
||||
or summary.get("username")
|
||||
)
|
||||
payload["operating_system"] = _clean_device_str(
|
||||
summary.get("operating_system")
|
||||
or summary.get("agent_operating_system")
|
||||
or summary.get("os")
|
||||
)
|
||||
uptime_value = (
|
||||
summary.get("uptime_sec")
|
||||
or summary.get("uptime_seconds")
|
||||
or summary.get("uptime")
|
||||
)
|
||||
payload["uptime"] = _coerce_int(uptime_value)
|
||||
payload["agent_id"] = _clean_device_str(summary.get("agent_id"))
|
||||
return payload
|
||||
|
||||
|
||||
def _device_upsert(
|
||||
cur: sqlite3.Cursor,
|
||||
hostname: str,
|
||||
description: Optional[str],
|
||||
merged_details: Dict[str, Any],
|
||||
created_at: Optional[int],
|
||||
*,
|
||||
agent_hash: Optional[str] = None,
|
||||
guid: Optional[str] = None,
|
||||
) -> None:
|
||||
if not hostname:
|
||||
return
|
||||
try:
|
||||
details_json = json.dumps(merged_details or {})
|
||||
except Exception:
|
||||
details_json = json.dumps({})
|
||||
column_values = _extract_device_columns(merged_details or {})
|
||||
|
||||
normalized_description = description if description is not None else ""
|
||||
try:
|
||||
normalized_description = str(normalized_description)
|
||||
except Exception:
|
||||
normalized_description = ""
|
||||
|
||||
normalized_hash = _clean_device_str(agent_hash) or None
|
||||
normalized_guid = _clean_device_str(guid) or None
|
||||
if normalized_guid:
|
||||
try:
|
||||
normalized_guid = _normalize_guid(normalized_guid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
created_ts = _coerce_int(created_at)
|
||||
if not created_ts:
|
||||
created_ts = int(time.time())
|
||||
|
||||
sql = f"""
|
||||
INSERT INTO {DEVICE_TABLE}(
|
||||
hostname,
|
||||
description,
|
||||
details,
|
||||
created_at,
|
||||
agent_hash,
|
||||
guid,
|
||||
memory,
|
||||
network,
|
||||
software,
|
||||
storage,
|
||||
cpu,
|
||||
device_type,
|
||||
domain,
|
||||
external_ip,
|
||||
internal_ip,
|
||||
last_reboot,
|
||||
last_seen,
|
||||
last_user,
|
||||
operating_system,
|
||||
uptime,
|
||||
agent_id
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
description=excluded.description,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE({DEVICE_TABLE}.created_at, excluded.created_at),
|
||||
agent_hash=COALESCE(NULLIF(excluded.agent_hash, ''), {DEVICE_TABLE}.agent_hash),
|
||||
guid=COALESCE(NULLIF(excluded.guid, ''), {DEVICE_TABLE}.guid),
|
||||
memory=excluded.memory,
|
||||
network=excluded.network,
|
||||
software=excluded.software,
|
||||
storage=excluded.storage,
|
||||
cpu=excluded.cpu,
|
||||
device_type=COALESCE(NULLIF(excluded.device_type, ''), {DEVICE_TABLE}.device_type),
|
||||
domain=COALESCE(NULLIF(excluded.domain, ''), {DEVICE_TABLE}.domain),
|
||||
external_ip=COALESCE(NULLIF(excluded.external_ip, ''), {DEVICE_TABLE}.external_ip),
|
||||
internal_ip=COALESCE(NULLIF(excluded.internal_ip, ''), {DEVICE_TABLE}.internal_ip),
|
||||
last_reboot=COALESCE(NULLIF(excluded.last_reboot, ''), {DEVICE_TABLE}.last_reboot),
|
||||
last_seen=COALESCE(NULLIF(excluded.last_seen, 0), {DEVICE_TABLE}.last_seen),
|
||||
last_user=COALESCE(NULLIF(excluded.last_user, ''), {DEVICE_TABLE}.last_user),
|
||||
operating_system=COALESCE(NULLIF(excluded.operating_system, ''), {DEVICE_TABLE}.operating_system),
|
||||
uptime=COALESCE(NULLIF(excluded.uptime, 0), {DEVICE_TABLE}.uptime),
|
||||
agent_id=COALESCE(NULLIF(excluded.agent_id, ''), {DEVICE_TABLE}.agent_id)
|
||||
"""
|
||||
|
||||
params: List[Any] = [
|
||||
hostname,
|
||||
normalized_description,
|
||||
details_json,
|
||||
created_ts,
|
||||
normalized_hash,
|
||||
normalized_guid,
|
||||
column_values.get("memory"),
|
||||
column_values.get("network"),
|
||||
column_values.get("software"),
|
||||
column_values.get("storage"),
|
||||
column_values.get("cpu"),
|
||||
column_values.get("device_type"),
|
||||
column_values.get("domain"),
|
||||
column_values.get("external_ip"),
|
||||
column_values.get("internal_ip"),
|
||||
column_values.get("last_reboot"),
|
||||
column_values.get("last_seen"),
|
||||
column_values.get("last_user"),
|
||||
column_values.get("operating_system"),
|
||||
column_values.get("uptime"),
|
||||
column_values.get("agent_id"),
|
||||
]
|
||||
cur.execute(sql, params)
|
||||
|
||||
|
||||
# --- Simple at-rest secret handling for service account passwords ---
|
||||
_SERVER_SECRET_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'server_secret.key'))
|
||||
|
||||
@@ -2514,20 +2734,101 @@ def init_db():
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Device details table
|
||||
# Device table (renamed from historical device_details)
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS device_details (hostname TEXT PRIMARY KEY, description TEXT, details TEXT, created_at INTEGER, agent_hash TEXT, guid TEXT)"
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||||
(DEVICE_TABLE,),
|
||||
)
|
||||
# Backfill missing created_at column on existing installs
|
||||
has_devices = cur.fetchone()
|
||||
if not has_devices:
|
||||
cur.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='device_details'"
|
||||
)
|
||||
legacy = cur.fetchone()
|
||||
if legacy:
|
||||
cur.execute("ALTER TABLE device_details RENAME TO devices")
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
hostname TEXT PRIMARY KEY,
|
||||
description TEXT,
|
||||
details TEXT,
|
||||
created_at INTEGER,
|
||||
agent_hash TEXT,
|
||||
guid TEXT,
|
||||
memory TEXT,
|
||||
network TEXT,
|
||||
software TEXT,
|
||||
storage TEXT,
|
||||
cpu TEXT,
|
||||
device_type TEXT,
|
||||
domain TEXT,
|
||||
external_ip TEXT,
|
||||
internal_ip TEXT,
|
||||
last_reboot TEXT,
|
||||
last_seen INTEGER,
|
||||
last_user TEXT,
|
||||
operating_system TEXT,
|
||||
uptime INTEGER,
|
||||
agent_id TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Ensure required columns exist on upgraded installs
|
||||
cur.execute(f"PRAGMA table_info({DEVICE_TABLE})")
|
||||
existing_cols = [r[1] for r in cur.fetchall()]
|
||||
|
||||
def _ensure_column(name: str, decl: str) -> None:
|
||||
if name not in existing_cols:
|
||||
cur.execute(f"ALTER TABLE {DEVICE_TABLE} ADD COLUMN {name} {decl}")
|
||||
existing_cols.append(name)
|
||||
|
||||
_ensure_column("description", "TEXT")
|
||||
_ensure_column("details", "TEXT")
|
||||
_ensure_column("created_at", "INTEGER")
|
||||
_ensure_column("agent_hash", "TEXT")
|
||||
_ensure_column("guid", "TEXT")
|
||||
_ensure_column("memory", "TEXT")
|
||||
_ensure_column("network", "TEXT")
|
||||
_ensure_column("software", "TEXT")
|
||||
_ensure_column("storage", "TEXT")
|
||||
_ensure_column("cpu", "TEXT")
|
||||
_ensure_column("device_type", "TEXT")
|
||||
_ensure_column("domain", "TEXT")
|
||||
_ensure_column("external_ip", "TEXT")
|
||||
_ensure_column("internal_ip", "TEXT")
|
||||
_ensure_column("last_reboot", "TEXT")
|
||||
_ensure_column("last_seen", "INTEGER")
|
||||
_ensure_column("last_user", "TEXT")
|
||||
_ensure_column("operating_system", "TEXT")
|
||||
_ensure_column("uptime", "INTEGER")
|
||||
_ensure_column("agent_id", "TEXT")
|
||||
|
||||
# Backfill expanded columns from stored JSON
|
||||
try:
|
||||
cur.execute("PRAGMA table_info(device_details)")
|
||||
cols = [r[1] for r in cur.fetchall()]
|
||||
if 'created_at' not in cols:
|
||||
cur.execute("ALTER TABLE device_details ADD COLUMN created_at INTEGER")
|
||||
if 'agent_hash' not in cols:
|
||||
cur.execute("ALTER TABLE device_details ADD COLUMN agent_hash TEXT")
|
||||
if 'guid' not in cols:
|
||||
cur.execute("ALTER TABLE device_details ADD COLUMN guid TEXT")
|
||||
cur.execute(f"SELECT hostname, details FROM {DEVICE_TABLE}")
|
||||
rows = cur.fetchall()
|
||||
for hostname, details_json in rows:
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
details = {}
|
||||
column_values = _extract_device_columns(details)
|
||||
update_fields = []
|
||||
params: List[Any] = []
|
||||
for key, value in column_values.items():
|
||||
if key not in existing_cols:
|
||||
continue
|
||||
update_fields.append(f"{key}=?")
|
||||
params.append(value)
|
||||
if update_fields:
|
||||
params.append(hostname)
|
||||
cur.execute(
|
||||
f"UPDATE {DEVICE_TABLE} SET {', '.join(update_fields)} WHERE hostname=?",
|
||||
params,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -2986,7 +3287,9 @@ def _load_device_records(limit: int = 0):
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT hostname, description, details FROM device_details")
|
||||
cur.execute(
|
||||
f"SELECT hostname, description, last_user, internal_ip, external_ip, details FROM {DEVICE_TABLE}"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
# Build device -> site mapping
|
||||
@@ -3005,19 +3308,18 @@ def _load_device_records(limit: int = 0):
|
||||
site_map = {}
|
||||
|
||||
out = []
|
||||
for hostname, description, details_json in rows:
|
||||
d = {}
|
||||
for hostname, description, last_user, internal_ip, external_ip, details_json in rows:
|
||||
summary = {}
|
||||
try:
|
||||
d = json.loads(details_json or "{}")
|
||||
summary = (json.loads(details_json or "{}") or {}).get("summary") or {}
|
||||
except Exception:
|
||||
d = {}
|
||||
summary = d.get("summary") or {}
|
||||
summary = {}
|
||||
rec = {
|
||||
"hostname": hostname or summary.get("hostname") or "",
|
||||
"description": (description or summary.get("description") or ""),
|
||||
"last_user": summary.get("last_user") or summary.get("last_user_name") or "",
|
||||
"internal_ip": summary.get("internal_ip") or "",
|
||||
"external_ip": summary.get("external_ip") or "",
|
||||
"last_user": last_user or summary.get("last_user") or summary.get("last_user_name") or "",
|
||||
"internal_ip": internal_ip or summary.get("internal_ip") or "",
|
||||
"external_ip": external_ip or summary.get("external_ip") or "",
|
||||
}
|
||||
site_info = site_map.get(rec["hostname"]) or {}
|
||||
rec.update({
|
||||
@@ -3120,6 +3422,138 @@ def search_suggest():
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/devices", methods=["GET"])
|
||||
def list_devices():
|
||||
"""Return all devices with expanded columns for the WebUI."""
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT d.hostname, d.description, d.details, d.created_at, d.agent_hash, d.guid,
|
||||
d.memory, d.network, d.software, d.storage, d.cpu,
|
||||
d.device_type, d.domain, d.external_ip, d.internal_ip,
|
||||
d.last_reboot, d.last_seen, d.last_user, d.operating_system,
|
||||
d.uptime, d.agent_id,
|
||||
s.id, s.name, s.description
|
||||
FROM {DEVICE_TABLE} d
|
||||
LEFT JOIN device_sites ds ON ds.device_hostname = d.hostname
|
||||
LEFT JOIN sites s ON s.id = ds.site_id
|
||||
"""
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
return jsonify({"error": str(exc)}), 500
|
||||
|
||||
def _parse_json_list(raw: Optional[str]) -> list:
|
||||
try:
|
||||
return json.loads(raw or "[]")
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _parse_json_obj(raw: Optional[str]) -> dict:
|
||||
try:
|
||||
return json.loads(raw or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _ts_to_iso(ts: Optional[int]) -> str:
|
||||
if not ts:
|
||||
return ""
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
return datetime.fromtimestamp(int(ts), timezone.utc).isoformat()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
devices = []
|
||||
now = time.time()
|
||||
for row in rows:
|
||||
(
|
||||
hostname,
|
||||
description,
|
||||
details_json,
|
||||
created_at,
|
||||
agent_hash,
|
||||
guid,
|
||||
memory_json,
|
||||
network_json,
|
||||
software_json,
|
||||
storage_json,
|
||||
cpu_json,
|
||||
device_type,
|
||||
domain,
|
||||
external_ip,
|
||||
internal_ip,
|
||||
last_reboot,
|
||||
last_seen,
|
||||
last_user,
|
||||
operating_system,
|
||||
uptime,
|
||||
agent_id,
|
||||
site_id,
|
||||
site_name,
|
||||
site_description,
|
||||
) = row
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
details = {}
|
||||
summary = details.get("summary") or {}
|
||||
if description:
|
||||
summary.setdefault("description", description)
|
||||
if agent_hash:
|
||||
summary.setdefault("agent_hash", agent_hash)
|
||||
normalized_guid = _normalize_guid(guid) if guid else ""
|
||||
if normalized_guid:
|
||||
summary.setdefault("agent_guid", normalized_guid)
|
||||
status = "Offline"
|
||||
try:
|
||||
if last_seen and (now - float(last_seen)) <= 300:
|
||||
status = "Online"
|
||||
except Exception:
|
||||
pass
|
||||
devices.append(
|
||||
{
|
||||
"hostname": hostname or summary.get("hostname") or "",
|
||||
"description": description or summary.get("description") or "",
|
||||
"details": details,
|
||||
"summary": summary,
|
||||
"created_at": int(created_at or 0),
|
||||
"created_at_iso": _ts_to_iso(created_at),
|
||||
"agent_hash": (agent_hash or "").strip() if agent_hash else summary.get("agent_hash", ""),
|
||||
"agent_guid": normalized_guid,
|
||||
"memory": _parse_json_list(memory_json),
|
||||
"network": _parse_json_list(network_json),
|
||||
"software": _parse_json_list(software_json),
|
||||
"storage": _parse_json_list(storage_json),
|
||||
"cpu": _parse_json_obj(cpu_json),
|
||||
"device_type": (device_type or "").strip() or summary.get("device_type") or "",
|
||||
"domain": (domain or "").strip(),
|
||||
"external_ip": (external_ip or "").strip() or summary.get("external_ip") or "",
|
||||
"internal_ip": (internal_ip or "").strip() or summary.get("internal_ip") or "",
|
||||
"last_reboot": (last_reboot or "").strip() or summary.get("last_reboot") or "",
|
||||
"last_seen": int(last_seen or 0),
|
||||
"last_seen_iso": _ts_to_iso(last_seen),
|
||||
"last_user": (last_user or "").strip() or summary.get("last_user") or "",
|
||||
"operating_system": (operating_system or "").strip()
|
||||
or summary.get("operating_system")
|
||||
or summary.get("agent_operating_system")
|
||||
or "",
|
||||
"uptime": int(uptime or 0),
|
||||
"agent_id": (agent_id or "").strip() or summary.get("agent_id") or "",
|
||||
"site_id": site_id,
|
||||
"site_name": site_name or "",
|
||||
"site_description": site_description or "",
|
||||
"status": status,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({"devices": devices})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Device List Views API
|
||||
# ---------------------------------------------
|
||||
@@ -3277,7 +3711,7 @@ def delete_device_list_view(view_id: int):
|
||||
|
||||
|
||||
def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None):
|
||||
"""Persist last_seen (and agent_id if provided) into device_details.details JSON.
|
||||
"""Persist last_seen (and agent_id if provided) into the stored device record.
|
||||
|
||||
Ensures that after a server restart, we can restore last_seen from DB even
|
||||
if the agent is offline, and helps merge entries by keeping track of the
|
||||
@@ -3289,7 +3723,7 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None):
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at, guid FROM device_details WHERE hostname = ?",
|
||||
f"SELECT details, description, created_at, guid, agent_hash FROM {DEVICE_TABLE} WHERE hostname = ?",
|
||||
(hostname,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -3304,6 +3738,7 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None):
|
||||
details = {}
|
||||
description = ""
|
||||
created_at = 0
|
||||
existing_hash = row[4] if row and len(row) > 4 else None
|
||||
|
||||
summary = details.get("summary") or {}
|
||||
summary["hostname"] = summary.get("hostname") or hostname
|
||||
@@ -3335,16 +3770,16 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None):
|
||||
pass
|
||||
|
||||
# Single upsert to avoid unique-constraint races
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_details(hostname, description, details, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
description=excluded.description,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE(device_details.created_at, excluded.created_at)
|
||||
""",
|
||||
(hostname, description, json.dumps(details), target_created_at),
|
||||
effective_hash = summary.get("agent_hash") or existing_hash
|
||||
effective_guid = summary.get("agent_guid") or existing_guid
|
||||
_device_upsert(
|
||||
cur,
|
||||
hostname,
|
||||
description,
|
||||
details,
|
||||
target_created_at,
|
||||
agent_hash=effective_hash,
|
||||
guid=effective_guid,
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -3357,9 +3792,11 @@ def load_agents_from_db():
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT hostname, details, agent_hash, guid FROM device_details")
|
||||
cur.execute(
|
||||
f"SELECT hostname, description, details, created_at, agent_hash, guid FROM {DEVICE_TABLE}"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
for hostname, details_json, agent_hash, guid in rows:
|
||||
for hostname, description, details_json, created_at, agent_hash, guid in rows:
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
@@ -3373,11 +3810,17 @@ def load_agents_from_db():
|
||||
stored_hash = summary.get("agent_hash") or ""
|
||||
agent_guid = (summary.get("agent_guid") or guid or "").strip()
|
||||
if guid and not summary.get("agent_guid"):
|
||||
summary["agent_guid"] = _normalize_guid(guid)
|
||||
normalized_guid = _normalize_guid(guid)
|
||||
summary["agent_guid"] = normalized_guid
|
||||
try:
|
||||
cur.execute(
|
||||
"UPDATE device_details SET details=? WHERE hostname=?",
|
||||
(json.dumps(details), hostname),
|
||||
_device_upsert(
|
||||
cur,
|
||||
hostname,
|
||||
description,
|
||||
details,
|
||||
created_at,
|
||||
agent_hash=agent_hash or stored_hash,
|
||||
guid=normalized_guid,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -3428,7 +3871,7 @@ def _device_rows_for_agent(cur, agent_id: str) -> List[Dict[str, Any]]:
|
||||
return results
|
||||
try:
|
||||
cur.execute(
|
||||
"SELECT hostname, agent_hash, details, guid FROM device_details WHERE LOWER(hostname) = ?",
|
||||
f"SELECT hostname, agent_hash, details, guid FROM {DEVICE_TABLE} WHERE LOWER(hostname) = ?",
|
||||
(base_host.lower(),),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
@@ -3470,14 +3913,22 @@ def _ensure_agent_guid_for_hostname(cur, hostname: str, agent_id: Optional[str]
|
||||
if not normalized_host:
|
||||
return None
|
||||
cur.execute(
|
||||
"SELECT hostname, guid, details FROM device_details WHERE LOWER(hostname) = ?",
|
||||
f"SELECT hostname, guid, details, description, created_at, agent_hash FROM {DEVICE_TABLE} WHERE LOWER(hostname) = ?",
|
||||
(normalized_host.lower(),),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
actual_host, existing_guid, details_json = row
|
||||
actual_host = row[0]
|
||||
existing_guid = row[1] or ""
|
||||
details_json = row[2] or "{}"
|
||||
description = row[3] if len(row) > 3 else ""
|
||||
created_at = row[4] if len(row) > 4 else None
|
||||
agent_hash = row[5] if len(row) > 5 else None
|
||||
else:
|
||||
actual_host, existing_guid, details_json = normalized_host, "", "{}"
|
||||
description = ""
|
||||
created_at = None
|
||||
agent_hash = None
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
@@ -3494,26 +3945,28 @@ def _ensure_agent_guid_for_hostname(cur, hostname: str, agent_id: Optional[str]
|
||||
if existing_guid:
|
||||
normalized = _normalize_guid(existing_guid)
|
||||
summary.setdefault("agent_guid", normalized)
|
||||
cur.execute(
|
||||
"UPDATE device_details SET guid=?, details=? WHERE hostname=?",
|
||||
(normalized, json.dumps(details), actual_host),
|
||||
_device_upsert(
|
||||
cur,
|
||||
actual_host,
|
||||
description,
|
||||
details,
|
||||
created_at,
|
||||
agent_hash=agent_hash,
|
||||
guid=normalized,
|
||||
)
|
||||
return summary.get("agent_guid") or normalized
|
||||
|
||||
new_guid = str(uuid.uuid4())
|
||||
summary["agent_guid"] = new_guid
|
||||
now = int(time.time())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_details(hostname, description, details, created_at, guid)
|
||||
VALUES (?, COALESCE((SELECT description FROM device_details WHERE hostname = ?), ''), ?, COALESCE((SELECT created_at FROM device_details WHERE hostname = ?), ?), ?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
guid=excluded.guid,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE(device_details.created_at, excluded.created_at),
|
||||
description=COALESCE(device_details.description, excluded.description)
|
||||
""",
|
||||
(actual_host, actual_host, json.dumps(details), actual_host, now, new_guid),
|
||||
_device_upsert(
|
||||
cur,
|
||||
actual_host,
|
||||
description,
|
||||
details,
|
||||
created_at or now,
|
||||
agent_hash=agent_hash,
|
||||
guid=new_guid,
|
||||
)
|
||||
return new_guid
|
||||
|
||||
@@ -3531,9 +3984,23 @@ def _ensure_agent_guid(agent_id: str, hostname: Optional[str] = None) -> Optiona
|
||||
if candidate:
|
||||
summary = row.get("details", {}).setdefault("summary", {})
|
||||
summary.setdefault("agent_guid", candidate)
|
||||
host = row.get("hostname")
|
||||
cur.execute(
|
||||
"UPDATE device_details SET guid=?, details=? WHERE hostname=?",
|
||||
(candidate, json.dumps(row.get("details", {})), row.get("hostname")),
|
||||
f"SELECT description, created_at, agent_hash FROM {DEVICE_TABLE} WHERE hostname = ?",
|
||||
(host,),
|
||||
)
|
||||
info = cur.fetchone()
|
||||
description = info[0] if info else None
|
||||
created_at = info[1] if info else None
|
||||
agent_hash = info[2] if info else None
|
||||
_device_upsert(
|
||||
cur,
|
||||
host,
|
||||
description,
|
||||
row.get("details", {}),
|
||||
created_at,
|
||||
agent_hash=agent_hash,
|
||||
guid=candidate,
|
||||
)
|
||||
conn.commit()
|
||||
return candidate
|
||||
@@ -3641,7 +4108,7 @@ def save_agent_details():
|
||||
cur = conn.cursor()
|
||||
# Load existing row to preserve description and created_at and merge fields
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at, guid FROM device_details WHERE hostname = ?",
|
||||
f"SELECT details, description, created_at, guid, agent_hash FROM {DEVICE_TABLE} WHERE hostname = ?",
|
||||
(hostname,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -3649,6 +4116,7 @@ def save_agent_details():
|
||||
description = ""
|
||||
created_at = 0
|
||||
existing_guid = None
|
||||
existing_agent_hash = None
|
||||
if row:
|
||||
try:
|
||||
prev_details = json.loads(row[0] or '{}')
|
||||
@@ -3663,6 +4131,10 @@ def save_agent_details():
|
||||
existing_guid = (row[3] or "").strip()
|
||||
except Exception:
|
||||
existing_guid = None
|
||||
try:
|
||||
existing_agent_hash = (row[4] or "").strip()
|
||||
except Exception:
|
||||
existing_agent_hash = None
|
||||
else:
|
||||
existing_guid = None
|
||||
|
||||
@@ -3737,25 +4209,14 @@ def save_agent_details():
|
||||
pass
|
||||
|
||||
# Upsert row without destroying created_at; keep previous created_at if exists
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_details(hostname, description, details, created_at, agent_hash, guid)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
description=excluded.description,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE(device_details.created_at, excluded.created_at),
|
||||
agent_hash=COALESCE(NULLIF(excluded.agent_hash, ''), device_details.agent_hash),
|
||||
guid=COALESCE(NULLIF(excluded.guid, ''), device_details.guid)
|
||||
""",
|
||||
(
|
||||
hostname,
|
||||
description,
|
||||
json.dumps(merged),
|
||||
created_at,
|
||||
agent_hash or None,
|
||||
(normalized_effective_guid or None),
|
||||
),
|
||||
_device_upsert(
|
||||
cur,
|
||||
hostname,
|
||||
description,
|
||||
merged,
|
||||
created_at,
|
||||
agent_hash=agent_hash or existing_agent_hash,
|
||||
guid=normalized_effective_guid,
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -3790,7 +4251,15 @@ def get_device_details(hostname: str):
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at, agent_hash, guid FROM device_details WHERE hostname = ?",
|
||||
f"""
|
||||
SELECT details, description, created_at, agent_hash, guid,
|
||||
memory, network, software, storage, cpu,
|
||||
device_type, domain, external_ip, internal_ip,
|
||||
last_reboot, last_seen, last_user, operating_system,
|
||||
uptime, agent_id
|
||||
FROM {DEVICE_TABLE}
|
||||
WHERE hostname = ?
|
||||
""",
|
||||
(hostname,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -3819,16 +4288,48 @@ def get_device_details(hostname: str):
|
||||
details['summary']['agent_hash'] = agent_hash
|
||||
except Exception:
|
||||
pass
|
||||
if len(row) > 4:
|
||||
agent_guid = (row[4] or "").strip()
|
||||
if agent_guid:
|
||||
try:
|
||||
details.setdefault('summary', {})
|
||||
if not details['summary'].get('agent_guid'):
|
||||
details['summary']['agent_guid'] = _normalize_guid(agent_guid)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(details)
|
||||
agent_guid = (row[4] or "").strip() if len(row) > 4 else ""
|
||||
normalized_guid = _normalize_guid(agent_guid) if agent_guid else ""
|
||||
if normalized_guid:
|
||||
details.setdefault('summary', {})
|
||||
details['summary'].setdefault('agent_guid', normalized_guid)
|
||||
|
||||
def _parse_list(raw: Optional[str]) -> list:
|
||||
try:
|
||||
return json.loads(raw or '[]')
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _parse_obj(raw: Optional[str]) -> dict:
|
||||
try:
|
||||
return json.loads(raw or '{}')
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
payload = {
|
||||
"details": details,
|
||||
"summary": details.get("summary", {}),
|
||||
"description": description or "",
|
||||
"created_at": created_at,
|
||||
"agent_hash": agent_hash,
|
||||
"agent_guid": normalized_guid,
|
||||
"memory": _parse_list(row[5] if len(row) > 5 else None),
|
||||
"network": _parse_list(row[6] if len(row) > 6 else None),
|
||||
"software": _parse_list(row[7] if len(row) > 7 else None),
|
||||
"storage": _parse_list(row[8] if len(row) > 8 else None),
|
||||
"cpu": _parse_obj(row[9] if len(row) > 9 else None),
|
||||
"device_type": (row[10] or "").strip() if len(row) > 10 and row[10] else "",
|
||||
"domain": (row[11] or "").strip() if len(row) > 11 and row[11] else "",
|
||||
"external_ip": (row[12] or "").strip() if len(row) > 12 and row[12] else "",
|
||||
"internal_ip": (row[13] or "").strip() if len(row) > 13 and row[13] else "",
|
||||
"last_reboot": (row[14] or "").strip() if len(row) > 14 and row[14] else "",
|
||||
"last_seen": int(row[15] or 0) if len(row) > 15 and row[15] is not None else 0,
|
||||
"last_user": (row[16] or "").strip() if len(row) > 16 and row[16] else "",
|
||||
"operating_system": (row[17] or "").strip() if len(row) > 17 and row[17] else "",
|
||||
"uptime": int(row[18] or 0) if len(row) > 18 and row[18] is not None else 0,
|
||||
"agent_id": (row[19] or "").strip() if len(row) > 19 and row[19] else "",
|
||||
}
|
||||
return jsonify(payload)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({})
|
||||
@@ -3841,17 +4342,34 @@ def set_device_description(hostname: str):
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
now = int(time.time())
|
||||
# Insert row if missing with created_at; otherwise update description only
|
||||
cur.execute(
|
||||
"INSERT INTO device_details(hostname, description, details, created_at) "
|
||||
"VALUES (?, COALESCE(?, ''), COALESCE((SELECT details FROM device_details WHERE hostname = ?), '{}'), ?) "
|
||||
"ON CONFLICT(hostname) DO NOTHING",
|
||||
(hostname, description, hostname, now),
|
||||
f"SELECT details, created_at, agent_hash, guid, description FROM {DEVICE_TABLE} WHERE hostname = ?",
|
||||
(hostname,),
|
||||
)
|
||||
cur.execute(
|
||||
"UPDATE device_details SET description=? WHERE hostname=?",
|
||||
(description, hostname),
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
details_json, created_at, agent_hash, guid, existing_description = row
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
details = {}
|
||||
created_at = created_at or int(time.time())
|
||||
else:
|
||||
details = {}
|
||||
created_at = int(time.time())
|
||||
agent_hash = None
|
||||
guid = None
|
||||
existing_description = ""
|
||||
summary = details.setdefault("summary", {})
|
||||
summary["description"] = description
|
||||
_device_upsert(
|
||||
cur,
|
||||
hostname,
|
||||
description or existing_description or "",
|
||||
details,
|
||||
created_at,
|
||||
agent_hash=agent_hash,
|
||||
guid=guid,
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -4876,7 +5394,7 @@ def handle_collector_status(data):
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at FROM device_details WHERE hostname = ?",
|
||||
f"SELECT details, description, created_at, agent_hash, guid FROM {DEVICE_TABLE} WHERE hostname = ?",
|
||||
(host,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -4887,25 +5405,27 @@ def handle_collector_status(data):
|
||||
details = {}
|
||||
description = row[1] or ""
|
||||
created_at = int(row[2] or 0)
|
||||
existing_hash = (row[3] or "").strip() if len(row) > 3 else None
|
||||
existing_guid = (row[4] or "").strip() if len(row) > 4 else None
|
||||
else:
|
||||
details = {}
|
||||
description = ""
|
||||
created_at = 0
|
||||
existing_hash = None
|
||||
existing_guid = None
|
||||
summary = details.get('summary') or {}
|
||||
# Only update last_user if provided; do not clear other fields
|
||||
summary['last_user'] = last_user
|
||||
details['summary'] = summary
|
||||
now = int(time.time())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_details(hostname, description, details, created_at)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
description=excluded.description,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE(device_details.created_at, excluded.created_at)
|
||||
""",
|
||||
(host, description, json.dumps(details), created_at or now),
|
||||
_device_upsert(
|
||||
cur,
|
||||
host,
|
||||
description,
|
||||
details,
|
||||
created_at or now,
|
||||
agent_hash=existing_hash,
|
||||
guid=existing_guid,
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -4918,10 +5438,10 @@ def delete_agent(agent_id: str):
|
||||
"""Remove an agent from the registry and database."""
|
||||
info = registered_agents.pop(agent_id, None)
|
||||
agent_configurations.pop(agent_id, None)
|
||||
# IMPORTANT: Do NOT delete device_details here. Multiple in-memory agent
|
||||
# IMPORTANT: Do NOT delete devices here. Multiple in-memory agent
|
||||
# records can refer to the same hostname; removing one should not wipe the
|
||||
# persisted device inventory for the hostname. A dedicated endpoint can be
|
||||
# added later to purge device_details by hostname if needed.
|
||||
# added later to purge devices by hostname if needed.
|
||||
if info:
|
||||
return jsonify({"status": "removed"})
|
||||
return jsonify({"error": "agent not found"}), 404
|
||||
|
||||
Reference in New Issue
Block a user