Wire CP-78xx probe discovery, relay phone-probe commands, and a chat follow-up message so store desk phones get registration, switch, and provisioning detail alongside DECT and WAN diagnostics. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
4.7 KiB
JavaScript
185 lines
4.7 KiB
JavaScript
// services/phoneDiscovery.js
|
|
//
|
|
// Turn collectPhoneStatus() desk phones into relay targets on 10.x.
|
|
// Pure — no I/O.
|
|
|
|
import { isTenDotIp, normalizeMac } from './dectDiscovery.js';
|
|
|
|
// Webex /devices product strings vary: "Cisco CP-7841", "Cisco 7841", etc.
|
|
const MPP_PRODUCT_PATTERNS = [
|
|
/(?:^|\s)CP-\d+/i,
|
|
/^Cisco\s+CP-/i,
|
|
/^Cisco\s+78\d{2}\b/i,
|
|
/^Cisco\s+88\d{2}\b/i,
|
|
/^Cisco\s+68\d{2}\b/i,
|
|
/\bCP-78\d{2}\b/i,
|
|
/\bCP-88\d{2}\b/i,
|
|
/\bCP-68\d{2}\b/i,
|
|
];
|
|
|
|
// Room / collaboration devices — never probe via MPP phone relay.
|
|
const ROOM_PRODUCT_PATTERNS = [
|
|
/room kit/i,
|
|
/room bar/i,
|
|
/room 55/i,
|
|
/room 70/i,
|
|
/room 90/i,
|
|
/board pro/i,
|
|
/desk pro/i,
|
|
/webex desk pro/i,
|
|
/\bcodec\b/i,
|
|
/webex room/i,
|
|
/dbs-210/i,
|
|
/dect/i,
|
|
];
|
|
|
|
/**
|
|
* @param {object} phoneStatus collectPhoneStatus() output
|
|
* @returns {{ phones: object[], warnings: object[], inventory: object }}
|
|
*/
|
|
export function discoverDeskPhones(phoneStatus) {
|
|
const phones = [];
|
|
const warnings = [];
|
|
const seenIps = new Set();
|
|
const seenMacs = new Set();
|
|
const skipped = [];
|
|
|
|
const raw = extractPhoneList(phoneStatus);
|
|
|
|
for (const phone of raw) {
|
|
if (!isMppDeskPhone(phone)) {
|
|
skipped.push({
|
|
mac: phone.mac || null,
|
|
product: phone.product || phone.model || null,
|
|
name: phone.name || phone.displayName || null,
|
|
reason: 'not classified as MPP desk phone',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const ip = pickIp(phone);
|
|
const mac = pickMac(phone);
|
|
const name = phone.name || phone.displayName || `Phone ${mac || '?'}`;
|
|
|
|
if (!mac) {
|
|
warnings.push({ mac: null, ip, name, reason: 'phone has no MAC address in inventory' });
|
|
continue;
|
|
}
|
|
if (!ip) {
|
|
warnings.push({ mac, ip: null, name, reason: 'no IP address available (phone may be unreachable)' });
|
|
continue;
|
|
}
|
|
if (!isTenDotIp(ip)) {
|
|
warnings.push({
|
|
mac,
|
|
ip,
|
|
name,
|
|
reason: `phone IP ${ip} is not on the corporate 10.0.0.0/8 network; skipping`,
|
|
});
|
|
continue;
|
|
}
|
|
if (seenIps.has(ip)) {
|
|
warnings.push({ mac, ip, name, reason: `duplicate IP ${ip} — keeping first entry` });
|
|
continue;
|
|
}
|
|
if (seenMacs.has(mac)) {
|
|
warnings.push({ mac, ip, name, reason: `duplicate MAC ${mac} — keeping first entry` });
|
|
continue;
|
|
}
|
|
seenIps.add(ip);
|
|
seenMacs.add(mac);
|
|
|
|
phones.push({
|
|
mac,
|
|
ip,
|
|
name,
|
|
webexId: phone.id || null,
|
|
product: phone.product || phone.model || null,
|
|
source: phone.meraki?.ip ? 'meraki' : 'webex',
|
|
});
|
|
}
|
|
|
|
if (raw.length > 0 && phones.length === 0 && skipped.length > 0) {
|
|
warnings.push({
|
|
mac: null,
|
|
ip: null,
|
|
name: null,
|
|
reason: `${raw.length} Webex device(s) in inventory but none selected for MPP probe`,
|
|
skipped,
|
|
});
|
|
}
|
|
|
|
return {
|
|
phones,
|
|
warnings,
|
|
inventory: {
|
|
webexDeviceCount: raw.length,
|
|
mppSelected: phones.length,
|
|
skipped,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function extractPhoneList(phoneStatus) {
|
|
if (Array.isArray(phoneStatus?.phones)) return phoneStatus.phones;
|
|
if (Array.isArray(phoneStatus?.phones?.data)) return phoneStatus.phones.data;
|
|
return [];
|
|
}
|
|
|
|
export function isMppDeskPhone(phone) {
|
|
if (!phone || typeof phone !== 'object') return false;
|
|
|
|
const product = String(phone.product || phone.model || '').trim();
|
|
const name = String(phone.name || phone.displayName || '').trim();
|
|
const haystack = `${product} ${name}`.trim();
|
|
|
|
for (const re of ROOM_PRODUCT_PATTERNS) {
|
|
if (re.test(haystack)) return false;
|
|
}
|
|
for (const re of MPP_PRODUCT_PATTERNS) {
|
|
if (re.test(haystack)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function pickIp(phone) {
|
|
const merakiIp = phone?.meraki?.ip;
|
|
if (typeof merakiIp === 'string' && merakiIp.trim()) return merakiIp.trim();
|
|
const webexIp = phone?.ipAddress;
|
|
if (typeof webexIp === 'string' && webexIp.trim() && webexIp !== '—') return webexIp.trim();
|
|
return null;
|
|
}
|
|
|
|
/** Webex sometimes omits MAC; Meraki client match is a reliable fallback. */
|
|
function pickMac(phone) {
|
|
const candidates = [
|
|
phone?.mac,
|
|
phone?.meraki?.mac,
|
|
phone?.meraki?.client?.mac,
|
|
];
|
|
for (const raw of candidates) {
|
|
if (!raw || raw === '—') continue;
|
|
const normalized = normalizeMac(raw);
|
|
if (normalized) return normalized;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Build a manual probe target (bypasses Webex discovery).
|
|
* @param {string} ip
|
|
* @param {object} [attrs]
|
|
*/
|
|
export function manualPhoneTarget(ip, attrs = {}) {
|
|
const trimmed = String(ip || '').trim();
|
|
if (!trimmed) return null;
|
|
return {
|
|
mac: attrs.mac || null,
|
|
ip: trimmed,
|
|
name: attrs.name || `manual ${trimmed}`,
|
|
webexId: null,
|
|
product: attrs.product || null,
|
|
source: 'manual',
|
|
};
|
|
}
|