Multi-integration Webex chat/HTTP bot that unifies phone, AV, and network status for retail store support. Consolidates data from Webex Calling, Meraki, Workspace ONE (MDM), Atlas AMP, RED digital signage, and OptiSigns into rich per-store status commands. Key surfaces: - /phonestatus, /avstatus — per-store phone & AV device reports with clickable Meraki deep-links and per-port detail. - /webexhost — check/assign Webex Meetings host licenses via the Service App; adaptive-card confirmation flow, HTTP-API-gated. - /offboarduser — full Webex Admin offboarding (auth revoke, device wipe, license removal); adaptive-card confirmation. - /jirapoll — on-demand trigger for the hourly Jira poller. - /bulkavstatuscsv — bulk store CSV export with concurrency limits. Automation: - Hourly Jira poller (node-cron) with an X.AI (Grok) ticket classifier that categorizes unassigned tickets as phone/av/skip, extracts store numbers from free-text, and enriches Jira with the same detailed markdown the chat commands emit (converted to Jira ADF, preserves bold + Meraki links). Idempotent via a `bot-enriched` Jira label. Architecture: - Node.js 20+, ESM, Express 5, webex-node-bot-framework. - Layered integrations (integrations/*), services (services/*), commands (commands/*), utils (utils/*). - Shared markdown renderers (services/renderers/*) feed both chat handlers and the Jira poller so the two surfaces stay in sync. - Hand-rolled markdown-to-ADF converter (utils/markdownToAdf.js) — no new npm dependency. - Node built-in test runner (`node --test tests/*.test.js`), 30 tests covering the converter, renderers, and poller ADF assembly. Docker + docker-compose deployment. Config via .env (see .env.example for the full option surface).
98 lines
3.8 KiB
JavaScript
98 lines
3.8 KiB
JavaScript
// services/enrichment/redMatcher.js
|
|
//
|
|
// Unified RED player matching for AV enrichment.
|
|
// This is the single source of truth for finding which RED player (if any)
|
|
// corresponds to a given MDM/AV device (mainly the MSC* ones).
|
|
//
|
|
// Consolidates logic previously duplicated in:
|
|
// - deviceService.js (enrichMdmDevices, with special ae/offline/aerie cases)
|
|
// - avDeviceBuilder.js (enrichDomainData, with translate + includes)
|
|
// - avEnricher.js (findRedMatch, which was weaker for dotted DeviceIDs)
|
|
//
|
|
// Uses translateREDDeviceID (and normalizePlayerName via barrel) from normalizers.
|
|
// Extracted as part of Option A incremental consolidation.
|
|
|
|
import { normalizePlayerName, translateREDDeviceID } from './normalizers.js';
|
|
|
|
/**
|
|
* Find the best matching full RED player object for a source identifier.
|
|
* Identifier can be a friendlyName, device.identifier, etc. (e.g. "US002477MSCOFF"
|
|
* or "US002477MSCAE").
|
|
*
|
|
* Handles RED DeviceIDs in formats like:
|
|
* - "US.OFFLINE.2477" -> matches US002477MSCOFF devices
|
|
* - "US.AE.1234" -> matches US001234MSCAE
|
|
* - "US.AERIE.999"
|
|
* - Also falls back to Name field contains, and direct matches.
|
|
*
|
|
* @param {string} identifier
|
|
* @param {Array<Object>} redList - raw players from getREDStatusForStore (full objects)
|
|
* @returns {Object|null} matching RED player (full data, including AvailabilityStatus etc.) or null
|
|
*/
|
|
export function findBestRedMatch(identifier, redList) {
|
|
if (!identifier || !Array.isArray(redList) || redList.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Canonical form for the AV/MDM side (e.g. US002477MSCOFF)
|
|
const idUpper = normalizePlayerName(identifier).toUpperCase().trim();
|
|
if (!idUpper) return null;
|
|
|
|
// Helper: does hay contain id as a "whole" token (not as prefix of longer brand like MSCAE inside MSCAERIE)
|
|
function containsWhole(id, hay) {
|
|
if (!hay || !id) return false;
|
|
let idx = -1;
|
|
while ((idx = hay.indexOf(id, idx + 1)) !== -1) {
|
|
const after = hay[idx + id.length];
|
|
if (after === undefined || !/[A-Z0-9]/.test(after)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Strategy 1 (preferred for translated cases): Translate RED DeviceID using the canonical translator and exact match.
|
|
// This is the reliable path for OFFLINE/AE/AERIE branded players (US.OFFLINE.2477 etc.)
|
|
// and covers all the previous ad-hoc special cases in deviceService.
|
|
for (const redPlayer of redList) {
|
|
const translated = translateREDDeviceID(redPlayer.DeviceID);
|
|
if (translated && translated.toUpperCase() === idUpper) {
|
|
return redPlayer;
|
|
}
|
|
}
|
|
|
|
// Strategy 2: Direct "whole token" match against RED's DeviceID or Name (preserves original builder heuristic,
|
|
// but avoids false positives on overlapping suffixes like MSCAE vs MSCAERIE).
|
|
let match = redList.find(p => {
|
|
const did = (p.DeviceID || '').toUpperCase();
|
|
const nm = (p.Name || '').toUpperCase();
|
|
return containsWhole(idUpper, did) || containsWhole(idUpper, nm);
|
|
});
|
|
if (match) {
|
|
return match;
|
|
}
|
|
|
|
// Strategy 3: Fallback using normalizePlayerName on the RED side too (exact after norm).
|
|
// This preserves (and may slightly improve) the previous avEnricher findRedMatch behavior
|
|
// for RED records that are already in a "clean" form. For dotted DeviceIDs this alone
|
|
// would have missed before (normalize on "US.OFFLINE.2477" gives "US002477"), but
|
|
// Strategy 2 will have caught it.
|
|
const targetLower = idUpper.toLowerCase();
|
|
match = redList.find(r => {
|
|
const fromRed = normalizePlayerName(r.DeviceID || r.Name || '').toLowerCase().trim();
|
|
return fromRed === targetLower;
|
|
});
|
|
if (match) {
|
|
return match;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Backward-compat alias (used internally by avEnricher etc. during migration).
|
|
* New code should prefer findBestRedMatch.
|
|
*/
|
|
export function findRedMatch(name, redList) {
|
|
return findBestRedMatch(name, redList);
|
|
}
|