Introduces a new diagnostic command that walks a registry of check
modules against a store user's Webex Calling configuration and
surfaces per-issue adaptive-card remediation for the fixable ones.
Checks (services/voiceDiag/checks/): dnd, callForwarding, callWaiting,
callIntercept, voicemail, hoteling, executiveAssistant,
outgoingPermission, phoneOnline. Remediations offered for DND,
forwarding, waiting, and intercept.
Uses the /v1/people/{id}/features/* admin surface (spark-admin:people_read
+ spark-admin:people_write scopes we already hold) — the earlier
telephony/config/people/*/callSettings/* path scheme returns 404 from
the Webex gateway and is not a live surface. Runner distinguishes
routing-404s ("URL moved") from "not applicable" 404s ("no calling
license") via the response body.
Arg parser accepts detail/detailed/--detail/--detailed and normalises
macOS smart-dashes so --detailed doesn't die when auto-correct
turns it into an em-dash.
Wires a VOICEDIAG_ACTIONS dispatcher in index.js mirroring the IGMP
branch, and registers /voicediag in commands/registry.js. 170 tests
pass (52 new: 39 check + 12 renderer + 5 arg-normalization).
Docs updated in .env.example, services/phoneService.js:467, and a new
services/voiceDiag/README.md that includes a "how to add a check"
recipe plus a note on the earlier wrong URL scheme.
Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.1 KiB
JavaScript
130 lines
4.1 KiB
JavaScript
// src/services/voiceDiag/checks/phoneOnline.js
|
|
//
|
|
// Summarises how many of the store's registered devices are online
|
|
// right now, plus a per-device drill-down. Deliberately reuses the
|
|
// phone-status snapshot already fetched during buildContext() — this
|
|
// check makes ZERO new API calls, which keeps it fast and keeps the
|
|
// /voicediag output aligned with /phonestatus for the same store
|
|
// (same source, same view of "which phones are up right now").
|
|
//
|
|
// Signal shape:
|
|
// status: 'error' when at least one phone is offline (a store with
|
|
// a dead phone is a real ticket).
|
|
// status: 'warn' when the store has no registered devices at all
|
|
// (this is often "we didn't ship / provision yet" but still worth
|
|
// surfacing).
|
|
// status: 'ok' when every device reports `connected`.
|
|
//
|
|
// Device shape from collectPhoneStatus:
|
|
// data.phones.data[i] = { mac, name, status, lastSeen, firmware, model, ... }
|
|
// data.dectBasestations[i] = { mac, name, status, lastSeen, firmware, ... }
|
|
//
|
|
// Statuses observed in the wild: 'connected', 'disconnected',
|
|
// 'unknown', 'activating', 'offline'. We treat 'connected' as the
|
|
// only truly OK value; everything else counts as offline for the
|
|
// purposes of the summary.
|
|
|
|
const OK_STATUSES = new Set(['connected']);
|
|
|
|
function isOnline(dev) {
|
|
const s = String(dev?.status || '').toLowerCase();
|
|
return OK_STATUSES.has(s);
|
|
}
|
|
|
|
export const phoneOnlineCheck = {
|
|
id: 'phoneOnline',
|
|
label: 'Phone Online Status',
|
|
requires: ['phoneStatus'],
|
|
// No new API scope required — reuses collectPhoneStatus() output,
|
|
// which is already gated on the /phonestatus set of scopes.
|
|
scope: null,
|
|
|
|
async run(ctx) {
|
|
const data = ctx.phoneStatus;
|
|
if (!data) {
|
|
return {
|
|
status: 'skipped',
|
|
message: 'phoneStatus snapshot unavailable — cannot report device online counts.',
|
|
details: null,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
const phones = Array.isArray(data.phones?.data) ? data.phones.data : [];
|
|
const dectBases = Array.isArray(data.dectBasestations) ? data.dectBasestations : [];
|
|
|
|
const phonesOnline = phones.filter(isOnline);
|
|
const phonesOffline = phones.filter((p) => !isOnline(p));
|
|
const basesOnline = dectBases.filter(isOnline);
|
|
const basesOffline = dectBases.filter((b) => !isOnline(b));
|
|
|
|
const totalDevices = phones.length + dectBases.length;
|
|
|
|
if (totalDevices === 0) {
|
|
return {
|
|
status: 'warn',
|
|
message: 'No desk phones or DECT basestations are registered for this store.',
|
|
details: {
|
|
phonesTotal: 0,
|
|
phonesOnline: 0,
|
|
phonesOffline: 0,
|
|
basesTotal: 0,
|
|
basesOnline: 0,
|
|
basesOffline: 0,
|
|
},
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
const offlineCount = phonesOffline.length + basesOffline.length;
|
|
const summary =
|
|
`Desk phones: ${phonesOnline.length}/${phones.length} online. ` +
|
|
`DECT bases: ${basesOnline.length}/${dectBases.length} online.`;
|
|
|
|
const offlineList = [
|
|
...phonesOffline.map((p) => ({
|
|
kind: 'phone',
|
|
name: p.name || 'Unknown phone',
|
|
mac: p.mac || null,
|
|
status: p.status || 'unknown',
|
|
lastSeen: p.lastSeen || null,
|
|
model: p.model || null,
|
|
})),
|
|
...basesOffline.map((b) => ({
|
|
kind: 'dect-base',
|
|
name: b.name || 'Unknown DECT base',
|
|
mac: b.mac || null,
|
|
status: b.status || 'unknown',
|
|
lastSeen: b.lastSeen || null,
|
|
model: b.model || null,
|
|
})),
|
|
];
|
|
|
|
const details = {
|
|
phonesTotal: phones.length,
|
|
phonesOnline: phonesOnline.length,
|
|
phonesOffline: phonesOffline.length,
|
|
basesTotal: dectBases.length,
|
|
basesOnline: basesOnline.length,
|
|
basesOffline: basesOffline.length,
|
|
offlineDevices: offlineList,
|
|
};
|
|
|
|
if (offlineCount === 0) {
|
|
return {
|
|
status: 'ok',
|
|
message: summary,
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'error',
|
|
message:
|
|
`${offlineCount} device(s) offline for this store. ${summary}`,
|
|
details,
|
|
remediation: null,
|
|
};
|
|
},
|
|
};
|