collabSupport/utils/pendingVoiceFixes.js
jmcqueen 2eb31a2ddc Add /voicediag rules-engine command with 9 per-user calling checks
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>
2026-07-07 18:51:31 -04:00

75 lines
2.1 KiB
JavaScript

// src/utils/pendingVoiceFixes.js
//
// In-memory store for pending /voicediag remediation cards. Identical
// shape to pendingIgmpFixes / pendingHostAssigns — every entry is
// stamped with a `timestamp` on insert, and a background sweep expires
// un-acted cards after TTL_MS so the map never leaks.
//
// Entries hold:
// {
// storeNum,
// personId, // Webex person id the remediation targets
// personLabel, // displayName or email for message/audit
// remediationId, // e.g. 'disable_dnd', 'clear_forwarding_always'
// remediationPayload, // free-form; whatever the check's remediation
// // handler expects (usually the pre-computed
// // PUT body plus contextual identifiers).
// requester, // from extractRequester(trigger)
// timestamp, // auto-set on .set()
// }
//
// TTL matches pendingIgmpFixes at 15 minutes — long enough for a
// normal "let me check with the store first" handoff without leaving
// a stale card interactive past the point where the underlying state
// may have already been changed by hand.
import { logger } from './logger.js';
const TTL_MS = 15 * 60 * 1000;
const SWEEP_INTERVAL_MS = 60 * 1000;
const _store = new Map();
export const pendingVoiceFixes = {
set(cardId, data) {
_store.set(cardId, { ...data, timestamp: Date.now() });
return this;
},
get(cardId) {
return _store.get(cardId);
},
has(cardId) {
return _store.has(cardId);
},
delete(cardId) {
return _store.delete(cardId);
},
get size() {
return _store.size;
},
entries() {
return _store.entries();
},
};
const sweepHandle = setInterval(() => {
const now = Date.now();
for (const [cardId, data] of _store.entries()) {
const stamped = typeof data?.timestamp === 'number' ? data.timestamp : 0;
if (now - stamped > TTL_MS) {
logger(
'voicediag:cleanup',
`Expired card ${cardId} for store ${data?.storeNum || 'unknown'} ` +
`remediation ${data?.remediationId || 'unknown'}`,
);
_store.delete(cardId);
}
}
}, SWEEP_INTERVAL_MS);
sweepHandle.unref?.();