# /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 default: hides OK checks, posts fixable cards /voicediag detail include OK checks + expand every details block /voicediag --only dnd,callForwarding /voicediag list-checks enumerate every registered check + its scope ``` HTTP path: `GET /voicediag?storeNum=[&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/.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; // 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.