Fix Enrich from CARD for items without IP — use host_id lookup

The enrich-batch endpoint now accepts a host_ids array alongside ips.
When queue items have no IP address but have a host_id (from ivanti_findings),
the frontend sends host_ids and the backend resolves them via CARD asset-search.

Results include the resolved IP so it populates the IPV4_ADDRESS column.
The LoaderModal now carries _host_id from initialDevices through to the
enrich call.
This commit is contained in:
Jordan Ramos
2026-06-09 13:00:02 -06:00
parent 54d6e49cb1
commit 23ea3983c8
4 changed files with 83 additions and 21 deletions

View File

@@ -84,6 +84,7 @@ export default function LoaderModal({ isOpen, onClose, initialDevices }) {
setDevices(initialDevices.map(d => ({
IPV4_ADDRESS: d.ip_address || '',
EQUIP_NAME: d.hostname || '',
_host_id: d.host_id || null,
})));
} else {
setDevices([]);
@@ -213,17 +214,23 @@ export default function LoaderModal({ isOpen, onClose, initialDevices }) {
// --- CARD Enrichment ---
const enrichFromCard = async () => {
const ips = devices.map(d => d.IPV4_ADDRESS).filter(Boolean);
if (ips.length === 0) return;
const hostIds = devices.filter(d => !d.IPV4_ADDRESS && d._host_id).map(d => d._host_id);
if (ips.length === 0 && hostIds.length === 0) return;
setEnriching(true);
setEnrichErrors([]);
try {
const body = {};
if (ips.length > 0) body.ips = ips;
if (hostIds.length > 0) body.host_ids = hostIds;
const resp = await fetch(`${API_BASE}/card/enrich-batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ ips }),
body: JSON.stringify(body),
});
if (!resp.ok) {
@@ -238,9 +245,16 @@ export default function LoaderModal({ isOpen, onClose, initialDevices }) {
// Map results back to devices
setDevices(prev => prev.map((device, idx) => {
const result = data.results.find(r => r.ip === device.IPV4_ADDRESS);
// Try matching by IP first
let result = data.results ? data.results.find(r => r.ip === device.IPV4_ADDRESS) : null;
// If no IP match, try matching by host_id
if (!result && device._host_id && data.host_id_results) {
result = data.host_id_results.find(r => String(r.host_id) === String(device._host_id));
}
if (!result || !result.found) {
if (result) errors.push({ ip: result.ip, error: result.error || 'Not found' });
if (result) errors.push({ ip: device.IPV4_ADDRESS || `hostId:${device._host_id}`, error: result.error || 'Not found' });
return device;
}
@@ -248,6 +262,10 @@ export default function LoaderModal({ isOpen, onClose, initialDevices }) {
const updated = { ...device };
const rowOverrides = overrides[idx] || {};
// Populate IP from host_id result if device didn't have one
if (result.ip && !device.IPV4_ADDRESS) {
updated.IPV4_ADDRESS = result.ip;
}
if (result.equip_inst_id && !rowOverrides.EQUIP_INST_ID && !device.EQUIP_INST_ID) {
updated.EQUIP_INST_ID = result.equip_inst_id;
}

View File

@@ -1230,7 +1230,7 @@ export default function IvantiTodoQueuePage() {
<LoaderModal
isOpen={showLoaderModal}
onClose={() => setShowLoaderModal(false)}
initialDevices={showLoaderModal ? queueItems.filter(i => selectedIds.has(i.id) && ['CARD', 'GRANITE', 'DECOM'].includes(i.workflow_type)).map(i => ({ ip_address: i.ip_address || '', hostname: i.hostname || '' })) : null}
initialDevices={showLoaderModal ? queueItems.filter(i => selectedIds.has(i.id) && ['CARD', 'GRANITE', 'DECOM'].includes(i.workflow_type)).map(i => ({ ip_address: i.ip_address || '', hostname: i.hostname || '', host_id: i.host_id || null })) : null}
/>
{/* Remediation Notes Modal */}

View File

@@ -3266,9 +3266,9 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
onClose={() => setShowLoaderModal(false)}
initialDevices={showLoaderModal ? (() => {
const selected = items.filter(i => selectedIds.has(i.id) && ['CARD', 'GRANITE', 'DECOM'].includes(i.workflow_type));
if (selected.length > 0) return selected.map(i => ({ ip_address: i.ip_address || '', hostname: i.hostname || '' }));
if (selected.length > 0) return selected.map(i => ({ ip_address: i.ip_address || '', hostname: i.hostname || '', host_id: i.host_id || null }));
// Standalone: use all CARD/GRANITE/DECOM items
return items.filter(i => ['CARD', 'GRANITE', 'DECOM'].includes(i.workflow_type)).map(i => ({ ip_address: i.ip_address || '', hostname: i.hostname || '' }));
return items.filter(i => ['CARD', 'GRANITE', 'DECOM'].includes(i.workflow_type)).map(i => ({ ip_address: i.ip_address || '', hostname: i.hostname || '', host_id: i.host_id || null }));
})() : null}
/>