From 1256c7510f32f11ea0e11a91b03e86414488076e Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Wed, 27 May 2026 19:50:01 -0600 Subject: [PATCH] Rewrite enrich-batch to use team assets endpoint for full data The owner endpoint only returns ownership info (no card_flags, ncim_discovery, or netops_granite_allips). Switch to fetching team assets (paginated) which returns the full enriched record with EQUIP_INST_ID, CARD_HOSTNAME, CARD_ASN, CARD_DEVICE_ID, CARD_VENDOR_MODEL, CARD_CLLI, and ncim_discovery data. Accepts optional 'team' parameter (defaults to NTS-AEO-STEAM). Paginates through confirmed and unconfirmed dispositions until all target IPs are found or pages are exhausted. --- backend/routes/cardApi.js | 106 +++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/backend/routes/cardApi.js b/backend/routes/cardApi.js index 08104f2..acd8d1e 100644 --- a/backend/routes/cardApi.js +++ b/backend/routes/cardApi.js @@ -518,7 +518,7 @@ function createCardApiRouter() { return res.status(503).json({ error: 'CARD API is not configured.', missingVars }); } - const { ips } = req.body || {}; + const { ips, team } = req.body || {}; if (!Array.isArray(ips) || ips.length === 0) { return res.status(400).json({ error: 'ips must be a non-empty array of IP address strings.' }); } @@ -526,59 +526,81 @@ function createCardApiRouter() { return res.status(400).json({ error: 'Maximum 200 IPs per request.' }); } - // Known CARD asset ID suffixes to try - const SUFFIXES = ['CTEC', 'NATL', 'CHTR', 'COML', 'RESI', 'WIFI', 'VOIP']; + // Build a set of IPs we're looking for + const targetIps = new Set(ips.map(ip => (ip || '').trim()).filter(Boolean)); + const resultMap = {}; + // Strategy: fetch team assets (paginated) and match against our target IPs. + // The team assets endpoint returns the full enriched record with ncim_discovery, + // card_flags, netops_granite_allips, etc. + const teamName = team || 'NTS-AEO-STEAM'; + const dispositions = ['confirmed', 'unconfirmed']; + let foundCount = 0; + + for (const disposition of dispositions) { + if (foundCount >= targetIps.size) break; + + let page = 1; + const pageSize = 200; + let hasMore = true; + + while (hasMore && foundCount < targetIps.size) { + try { + const result = await getTeamAssets(teamName, { disposition, page, pageSize }); + if (!result.ok) break; + + const data = JSON.parse(result.body); + const assets = data.assets || data.results || (Array.isArray(data) ? data : []); + + if (assets.length === 0) { + hasMore = false; + break; + } + + // Match assets against target IPs + for (const asset of assets) { + const assetId = asset._id || ''; + // Extract IP from asset ID (e.g., "10.240.78.110-CTEC" → "10.240.78.110") + const assetIp = assetId.replace(/-[A-Z]+$/i, ''); + + if (targetIps.has(assetIp) && !resultMap[assetIp]) { + resultMap[assetIp] = extractGraniteFields(asset, assetIp); + foundCount++; + } + } + + // Check if there are more pages + const total = data.total || data.count || 0; + if (page * pageSize >= total || assets.length < pageSize) { + hasMore = false; + } else { + page++; + } + } catch (err) { + console.error(`[card-api] enrich-batch: Error fetching ${teamName}/${disposition} page ${page}:`, err.message); + hasMore = false; + } + } + } + + // Build results array in the same order as input IPs const results = []; let enrichedCount = 0; let notFoundCount = 0; for (const ip of ips) { - if (!ip || typeof ip !== 'string') { - results.push({ ip: ip || '', found: false, equip_inst_id: null, hostname: null, error: 'Invalid IP' }); + const trimmedIp = (ip || '').trim(); + if (!trimmedIp) { + results.push({ ip: '', found: false, equip_inst_id: null, hostname: null, error: 'Invalid IP' }); notFoundCount++; continue; } - const trimmedIp = ip.trim(); - let found = false; - let enriched = null; - - // Try owner lookup with each suffix - for (const suffix of SUFFIXES) { - try { - const assetId = `${trimmedIp}-${suffix}`; - const ownerResult = await getOwner(assetId); - if (ownerResult.ok) { - const asset = JSON.parse(ownerResult.body); - enriched = extractGraniteFields(asset, trimmedIp); - found = true; - break; - } - } catch (_) { - // Continue to next suffix - } - } - - // Try without suffix (bare IP) - if (!found) { - try { - const ownerResult = await getOwner(trimmedIp); - if (ownerResult.ok) { - const asset = JSON.parse(ownerResult.body); - enriched = extractGraniteFields(asset, trimmedIp); - found = true; - } - } catch (_) { - // Not found - } - } - - if (found && enriched) { - results.push({ ip: trimmedIp, found: true, ...enriched }); + if (resultMap[trimmedIp]) { + results.push({ ip: trimmedIp, found: true, ...resultMap[trimmedIp] }); enrichedCount++; } else { - results.push({ ip: trimmedIp, found: false, equip_inst_id: null, hostname: null, error: 'IP not found in CARD' }); + results.push({ ip: trimmedIp, found: false, equip_inst_id: null, hostname: null, error: 'IP not found in CARD team assets' }); notFoundCount++; } }