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.9 KiB
Markdown
130 lines
4.9 KiB
Markdown
# /voicediag
|
|
|
|
Rules-engine style diagnostic for the store's Webex Calling
|
|
per-user configuration. The orchestrator (`voiceDiagService.js`)
|
|
walks a registry of check modules, gathers their `CheckResult`
|
|
objects, and hands them to the renderer + adaptive-card layer in
|
|
`commands/voiceDiag.js`.
|
|
|
|
## Command
|
|
|
|
```
|
|
/voicediag <storeNum> default: hides OK checks, posts fixable cards
|
|
/voicediag <storeNum> detail include OK checks + expand every details block
|
|
/voicediag <storeNum> --only dnd,callForwarding
|
|
/voicediag list-checks enumerate every registered check + its scope
|
|
```
|
|
|
|
HTTP path: `GET /voicediag?storeNum=<n>[&detailed=true][&only=dnd,callWaiting]`.
|
|
HTTP callers get the markdown snapshot only — remediation cards are
|
|
chat-only.
|
|
|
|
## Prerequisites
|
|
|
|
All checks call `/v1/people/{personId}/features/{feature}` (with a
|
|
few Webex-namespace variants noted inline in each check module).
|
|
The scopes required are:
|
|
|
|
- `spark-admin:people_read` — every read (all checks)
|
|
- `spark-admin:people_write` — remediations (disable DND, clear
|
|
forwarding, enable call waiting, disable call intercept)
|
|
|
|
These are the same scopes `/webexhost` and `/offboarduser` already
|
|
use, so no operator work is needed to enable `/voicediag` on an
|
|
existing deployment. If the token ever loses those scopes,
|
|
each check self-reports as `skipped: scope missing` — the run still
|
|
completes, and the phone-online summary keeps working.
|
|
|
|
### About the URL scheme
|
|
|
|
Early drafts of this feature documented `/v1/telephony/config/people/{id}/callSettings/*`.
|
|
That path family returns 404 "no static resource" from the Webex API
|
|
gateway — it is not a live surface. The correct admin path is
|
|
`/v1/people/{id}/features/{feature}`, per Webex's User Call Settings
|
|
docs. A couple of features use different feature-name segments than
|
|
their check id — most notably `intercept` (not `callIntercept`);
|
|
see each check module for the exact endpoint. The runner
|
|
distinguishes routing-404s ("URL moved, update the check") from
|
|
"not applicable" 404s ("this person isn't a calling user") using
|
|
the `no static resource` marker in the response body.
|
|
|
|
## Adding a new check
|
|
|
|
1. Create `services/voiceDiag/checks/<newCheck>.js` and export a
|
|
descriptor:
|
|
|
|
```js
|
|
export const myNewCheck = {
|
|
id: 'myNew',
|
|
label: 'My New Check',
|
|
requires: ['personId'], // subset of ['personId','phoneStatus','telephonyProfile']
|
|
scope: 'spark-admin:people_read',
|
|
async run(ctx) {
|
|
const data = await ctx.webex.request('GET', `people/${ctx.personId}/features/whatever`);
|
|
// ...evaluate...
|
|
return {
|
|
status: 'warn', // 'ok' | 'warn' | 'error' | 'skipped'
|
|
message: 'Human sentence for the chat row.',
|
|
details: { relevant: 'facts' },
|
|
remediation: { // optional
|
|
action: 'fix_my_thing', // globally unique remediation id
|
|
title: 'Fix My Thing',
|
|
summary: 'One-line description of what the fix will do.',
|
|
payload: { personId: ctx.personId, personLabel: ctx.personLabel, storeNum: ctx.storeNum },
|
|
},
|
|
};
|
|
},
|
|
remediations: { // optional
|
|
async fix_my_thing(bot, data, requester) {
|
|
// do the PUT, audit, reply
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
2. Register it in `services/voiceDiag/checks/index.js` at the
|
|
position you want the renderer to output it.
|
|
|
|
3. Add a `describe`-level block to `tests/voiceDiag.checks.test.js`
|
|
covering the enabled / disabled / 403 branches.
|
|
|
|
That's it — no changes to the runner, renderer, command handler,
|
|
`index.js` dispatcher, or `commands/registry.js`. The single
|
|
`confirm_voicediag` / `cancel_voicediag` action namespace routes
|
|
through the shared dispatcher, which looks the remediation up in the
|
|
central registry built at module load. Remediation id collisions
|
|
throw at import so you'll notice immediately.
|
|
|
|
### CheckResult shape
|
|
|
|
```ts
|
|
type Severity = 'ok' | 'warn' | 'error' | 'skipped';
|
|
|
|
type Remediation = {
|
|
action: string; // globally unique across all checks
|
|
title: string; // button label
|
|
summary?: string; // optional card body line
|
|
payload: Record<string, unknown>; // handed to remediations[action](bot, payload, requester)
|
|
};
|
|
|
|
type CheckResult = {
|
|
id: string; // set from check.id automatically
|
|
label: string; // set from check.label automatically
|
|
status: Severity;
|
|
message: string;
|
|
details?: object | null; // shown under `detail` mode
|
|
remediation?: Remediation | null;
|
|
};
|
|
```
|
|
|
|
### Error handling
|
|
|
|
Never throw from `run()`. The runner wraps every check in a
|
|
try/catch and converts:
|
|
|
|
- HTTP 401/403/404 → `status: 'skipped'` with a "missing scope"
|
|
hint pointing at `check.scope`.
|
|
- Any other error → `status: 'error'` with the API message.
|
|
|
|
So if you _can't_ reason about a specific 5xx, just let it bubble —
|
|
the runner will surface it.
|