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>
120 lines
4.2 KiB
JavaScript
120 lines
4.2 KiB
JavaScript
// src/services/voiceDiag/checks/dnd.js
|
|
//
|
|
// Detects whether the store's canonical Webex Calling user has Do
|
|
// Not Disturb enabled. DND-on is a top-5 cause of "the store phone
|
|
// doesn't ring" tickets — with DND on, incoming Webex calls skip the
|
|
// device entirely and go straight to voicemail (or the configured
|
|
// forward-when-no-answer target).
|
|
//
|
|
// Endpoint: GET/PUT /v1/people/{personId}/features/doNotDisturb
|
|
// Payload: { enabled: boolean, ringSplashEnabled: boolean }
|
|
//
|
|
// Scope: spark-admin:people_read (GET) + spark-admin:people_write (PUT).
|
|
// These are the scopes /webexhost / /offboarduser already use,
|
|
// so no re-bootstrapping is needed. NOTE: the plan initially
|
|
// referenced /v1/telephony/config/people/{id}/callSettings/*
|
|
// but every one of those paths returns 404 "no static resource"
|
|
// from the Webex API gateway — the current admin surface for
|
|
// per-person call settings is `/v1/people/{id}/features/*`.
|
|
// See wxc_sdk's user-call-settings table for the full list.
|
|
//
|
|
// Remediation: PUT `{enabled: false, ringSplashEnabled: false}`. We
|
|
// force ringSplashEnabled off too on the reasonable assumption that a
|
|
// store phone user in a diagnostic sweep doesn't want the visual
|
|
// "someone is calling you" splash left dangling half-configured. If
|
|
// this ever becomes a policy issue, split it into two remediations
|
|
// and drop the ringSplash toggle here.
|
|
|
|
import { logger } from '../../../utils/logger.js';
|
|
import { describeRequester } from '../../../utils/requester.js';
|
|
|
|
const ENDPOINT = (personId) =>
|
|
`people/${personId}/features/doNotDisturb`;
|
|
|
|
export const dndCheck = {
|
|
id: 'dnd',
|
|
label: 'Do Not Disturb',
|
|
requires: ['personId'],
|
|
scope: 'spark-admin:people_read',
|
|
|
|
async run(ctx) {
|
|
const data = await ctx.webex.request('GET', ENDPOINT(ctx.personId));
|
|
|
|
const enabled = !!data?.enabled;
|
|
const ringSplashEnabled = !!data?.ringSplashEnabled;
|
|
|
|
if (!enabled) {
|
|
return {
|
|
status: 'ok',
|
|
message: 'DND is off.',
|
|
details: { enabled, ringSplashEnabled },
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'warn',
|
|
message:
|
|
'DND is enabled — incoming calls will be silenced on this user\'s phones.',
|
|
details: { enabled, ringSplashEnabled },
|
|
remediation: {
|
|
action: 'disable_dnd',
|
|
title: 'Disable DND',
|
|
summary: `Turn DND off for ${ctx.personLabel}.`,
|
|
payload: {
|
|
personId: ctx.personId,
|
|
personLabel: ctx.personLabel,
|
|
storeNum: ctx.storeNum,
|
|
// Snapshot of the "before" state so the audit log line
|
|
// reads correctly after we PUT the new value.
|
|
before: { enabled, ringSplashEnabled },
|
|
},
|
|
},
|
|
};
|
|
},
|
|
|
|
remediations: {
|
|
async disable_dnd(bot, data, requester) {
|
|
const { personId, personLabel, storeNum, before } = data;
|
|
|
|
logger(
|
|
'voicediag:audit',
|
|
`CONFIRMED disable_dnd for ${personLabel} (person=${personId}, store=${storeNum}) ` +
|
|
`by ${describeRequester(requester)} — was enabled=${before?.enabled ?? 'unknown'}, ` +
|
|
`ringSplashEnabled=${before?.ringSplashEnabled ?? 'unknown'}`,
|
|
);
|
|
|
|
try {
|
|
// Import lazily so unit tests can replace the exported webex
|
|
// singleton via ctx.webex in run(), while the remediation path
|
|
// stays honest about which client it uses in production.
|
|
const { default: webex } = await import('../../../integrations/webex/WebexClient.js');
|
|
await webex.request('PUT', ENDPOINT(personId), {
|
|
enabled: false,
|
|
ringSplashEnabled: false,
|
|
});
|
|
} catch (err) {
|
|
logger(
|
|
'voicediag:audit',
|
|
`FAILED disable_dnd for ${personLabel}: ${err.message}`,
|
|
'error',
|
|
);
|
|
await bot.say(
|
|
'markdown',
|
|
`❌ Failed to disable DND for **${personLabel}**: ${err.message}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
await bot.say(
|
|
'markdown',
|
|
`✅ DND disabled for **${personLabel}** (store ${storeNum}). ` +
|
|
`Re-run \`/voicediag ${storeNum}\` to verify.`,
|
|
);
|
|
logger(
|
|
'voicediag:audit',
|
|
`COMPLETED disable_dnd for ${personLabel} (store ${storeNum})`,
|
|
);
|
|
},
|
|
},
|
|
};
|