Add Granite Loader to AEO Compliance page with CARD enrichment and pagination
- Add checkbox selection + Granite Loader button to compliance device table - Integrate LoaderModal for generating loader sheets from compliance devices - Add direct IP resolve path (resolveAssetId + searchByAssetId) for CARD enrichment on compliance devices without Ivanti host IDs - Add searchByAssetId helper for full enriched record via asset-search endpoint - Include NTS-AEO-ACCESS-OPS in default enrich-batch team search - Increase CARD quick-mode timeout from 15s to 30s - Add timeout vs not-found distinction in enrichment error reporting - Fix LoaderModal enriching state not resetting on modal reopen - Add pagination to compliance device table (25/50/100/200 per page) - Page resets on team, tab, filter, or search change
This commit is contained in:
@@ -312,6 +312,23 @@ async function searchByIvantiHostId(ivantiHostId, options) {
|
||||
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/asset-search/{assetId}?search_param=deep_search
|
||||
* Search CARD by asset ID (e.g., "24.24.100.20-CTEC"). Returns the full
|
||||
* enriched asset record including ncim_discovery, netops_granite_allips, etc.
|
||||
*
|
||||
* @param {string} assetId - CARD asset identifier (IP-SUFFIX format)
|
||||
* @param {object} [options] - { timeout }
|
||||
*/
|
||||
async function searchByAssetId(assetId, options) {
|
||||
const id = (assetId || '').trim();
|
||||
if (!id) {
|
||||
return { status: 400, body: '{"error":"Asset ID is required."}', ok: false };
|
||||
}
|
||||
const res = await cardGet(`/api/v2/asset-search/${encodeURIComponent(id)}?search_param=deep_search`, options);
|
||||
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a bare IP address to a full CARD asset ID by trying known suffixes.
|
||||
* Returns the first asset ID that returns a valid owner record, or null if none found.
|
||||
@@ -322,7 +339,7 @@ async function searchByIvantiHostId(ivantiHostId, options) {
|
||||
async function resolveAssetId(ip, options) {
|
||||
const quick = options && options.quick;
|
||||
const SUFFIXES = quick ? ['CTEC'] : ['CTEC', 'NATL', 'CHTR', 'COML', 'RESI', 'WIFI', 'VOIP'];
|
||||
const timeout = quick ? 15000 : undefined; // 15s timeout for quick mode
|
||||
const timeout = quick ? 30000 : undefined; // 30s timeout for quick mode
|
||||
const trimmedIp = (ip || '').trim();
|
||||
if (!trimmedIp) return null;
|
||||
|
||||
@@ -384,4 +401,5 @@ module.exports = {
|
||||
invalidateToken,
|
||||
resolveAssetId,
|
||||
searchByIvantiHostId,
|
||||
searchByAssetId,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const {
|
||||
redirectAsset,
|
||||
resolveAssetId,
|
||||
searchByIvantiHostId,
|
||||
searchByAssetId,
|
||||
} = require('../helpers/cardApi');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1025,16 +1026,66 @@ function createCardApiRouter() {
|
||||
}
|
||||
}
|
||||
|
||||
let foundCount = Object.keys(resultMap).length;
|
||||
// Direct resolve path: for IPs not found via ivanti_findings, resolve
|
||||
// the asset ID via suffix guessing (CTEC first) and fetch the full asset
|
||||
// record via asset-search. This returns ncim_discovery, netops_granite, etc.
|
||||
const unresolvedIps = ipsArray.filter(ip => !resultMap[ip]);
|
||||
if (unresolvedIps.length > 0) {
|
||||
const CONCURRENCY = 5;
|
||||
for (let i = 0; i < unresolvedIps.length; i += CONCURRENCY) {
|
||||
const batch = unresolvedIps.slice(i, i + CONCURRENCY);
|
||||
await Promise.all(batch.map(async (ip) => {
|
||||
if (resultMap[ip]) return;
|
||||
try {
|
||||
const assetId = await resolveAssetId(ip, { quick: true });
|
||||
if (assetId) {
|
||||
// Use asset-search to get the full enriched record (30s timeout)
|
||||
const searchResult = await searchByAssetId(assetId, { timeout: 30000 });
|
||||
if (searchResult.ok) {
|
||||
const searchData = JSON.parse(searchResult.body);
|
||||
const assets = searchData.assets || [];
|
||||
if (assets.length > 0) {
|
||||
resultMap[ip] = extractGraniteFields(assets[0], ip);
|
||||
} else {
|
||||
// Fallback: asset-search returned empty, try owner record
|
||||
const ownerResult = await getOwner(assetId);
|
||||
if (ownerResult.ok) {
|
||||
const ownerData = JSON.parse(ownerResult.body);
|
||||
resultMap[ip] = extractGraniteFields(ownerData, ip);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// asset-search failed, fall back to owner endpoint
|
||||
const ownerResult = await getOwner(assetId);
|
||||
if (ownerResult.ok) {
|
||||
const ownerData = JSON.parse(ownerResult.body);
|
||||
resultMap[ip] = extractGraniteFields(ownerData, ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const isTimeout = err.message && (err.message.includes('CARD_TIMEOUT') || err.message.includes('timed out'));
|
||||
if (isTimeout) {
|
||||
console.warn(`[card-api] enrich-batch: Direct resolve for ${ip} timed out`);
|
||||
resultMap[ip] = { _timeout: true };
|
||||
} else {
|
||||
console.warn(`[card-api] enrich-batch: Direct resolve for ${ip} failed:`, err.message);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let foundCount = Object.keys(resultMap).filter(k => !resultMap[k]._timeout).length;
|
||||
|
||||
// Fallback: paginated team-assets loop for any IPs not resolved by fast path
|
||||
// The team assets endpoint returns the full enriched record with ncim_discovery,
|
||||
// card_flags, netops_granite_allips, etc.
|
||||
const teams = team ? [team] : ['NTS-AEO-STEAM', 'NTS-AEO-ACCESS-ENG'];
|
||||
// Skip if all unresolved IPs already timed out (heavier calls will also timeout)
|
||||
const teams = team ? [team] : ['NTS-AEO-STEAM', 'NTS-AEO-ACCESS-ENG', 'NTS-AEO-ACCESS-OPS'];
|
||||
const dispositions = ['confirmed', 'unconfirmed', 'candidate'];
|
||||
const stillUnresolved = [...targetIps].filter(ip => !resultMap[ip]);
|
||||
|
||||
for (const teamName of teams) {
|
||||
if (foundCount >= targetIps.size) break;
|
||||
if (stillUnresolved.length === 0 || foundCount >= targetIps.size) break;
|
||||
|
||||
for (const disposition of dispositions) {
|
||||
if (foundCount >= targetIps.size) break;
|
||||
@@ -1097,8 +1148,13 @@ function createCardApiRouter() {
|
||||
}
|
||||
|
||||
if (resultMap[trimmedIp]) {
|
||||
results.push({ ip: trimmedIp, found: true, ...resultMap[trimmedIp] });
|
||||
enrichedCount++;
|
||||
if (resultMap[trimmedIp]._timeout) {
|
||||
results.push({ ip: trimmedIp, found: false, equip_inst_id: null, hostname: null, error: 'CARD lookup timed out — try again' });
|
||||
notFoundCount++;
|
||||
} else {
|
||||
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' });
|
||||
notFoundCount++;
|
||||
|
||||
Reference in New Issue
Block a user