collabSupport/services/voiceDiag/checks/port/_helpers.js
jmcqueen d12723d010 Voicediag: store voice standards + port-hygiene checks + apply-all card
Refactor every /voicediag check to declare a top-level `standards`
object so the desired state is legible without reading run() logic
and can drive a documented reference table. Upgrade callForwarding
to error severity, tighten voicemail with three send-to-VM error
paths + a `stop_sending_to_voicemail` remediation, and add a
`disable_hoteling` remediation.

Add a port-hygiene check bucket under services/voiceDiag/checks/port
(portType, portVlan, portPoe, portEnabled) that reuses the phone-
status snapshot to enforce switchport standards. Configurable via
VOICE_STANDARD_PHONE_VLAN (default 102) and VOICE_STANDARD_ENABLED
(kill-switch). Preserve Meraki `portType`/`voiceVlan`/`dataVlan`
through the enrichment chain so the checks have clean data to read.

Add an "apply all N fixes" combined card that shows up when 2+
remediations are available. New confirm_voicediag_all /
cancel_voicediag_all actions run each fix in sequence (readable
audit trail, no per-person write-throttle stacking), accumulate
individual failures into a summary rather than aborting.

Adds regression tests asserting every check exposes .standards,
plus coverage for port checks, kill-switch, and combined-card
iteration. 63 tests in the checks file, 188 total, all green.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:14:38 -04:00

77 lines
3.2 KiB
JavaScript

// src/services/voiceDiag/checks/port/_helpers.js
//
// Shared helpers for the port-hygiene check bucket. Each of the port
// checks (portType, portVlan, portPoe, portEnabled) walks the same
// per-phone / per-DECT-base list out of ctx.phoneStatus and reports
// a per-device verdict. Centralising the walk + labelling here keeps
// the individual check modules focused on their single signal.
/**
* Collect every device that could plausibly have a wired port. This
* is desk phones + DECT basestations. DECT handsets are excluded —
* they're radio-connected to their base, they don't have their own
* switchport. Wireless desk phones aren't filtered out here; the
* per-check `isWireless(d)` guard decides how to render them.
*
* @param {object} phoneStatus collectPhoneStatus() result
* @returns {Array<object>} each entry retains its full shape so
* checks can reach `.meraki.*` freely.
*/
export function collectPortDevices(phoneStatus) {
if (!phoneStatus) return [];
const phones = Array.isArray(phoneStatus?.phones?.data) ? phoneStatus.phones.data : [];
const dectBases = Array.isArray(phoneStatus?.dectBasestations) ? phoneStatus.dectBasestations : [];
const tag = (kind) => (d) => ({ ...d, _voiceDiagKind: kind });
return [
...phones.map(tag('phone')),
...dectBases.map(tag('dect-base')),
];
}
/** True when the device is on wireless — no switchport to inspect. */
export function isWireless(dev) {
const conn = String(dev?.meraki?.connectionType || '').toLowerCase();
if (conn === 'wireless') return true;
return false;
}
/**
* Global kill-switch for the port-hygiene bucket. Env
* `VOICE_STANDARD_ENABLED=false` returns a short "port checks are
* currently disabled" skip result that every port check can return
* verbatim. Feature-config checks (DND, forwarding, etc.) are
* intentionally *not* gated by this — those are unconditional
* standards. Only the switchport bucket bows out.
*
* The check descriptor is passed in so the returned result carries
* the caller's id / label without callers repeating themselves.
*/
export function maybeSkippedByKillSwitch(check) {
const raw = String(process.env.VOICE_STANDARD_ENABLED ?? 'true').toLowerCase().trim();
const enabled = !(raw === 'false' || raw === '0' || raw === 'no' || raw === 'off');
if (enabled) return null;
return {
status: 'skipped',
message:
`${check.label} skipped — VOICE_STANDARD_ENABLED=false (port-hygiene checks are silenced).`,
details: { killSwitch: 'VOICE_STANDARD_ENABLED', value: raw },
remediation: null,
};
}
/** Human labels used in per-device drilldowns. Kept short so
* aggregate messages don't blow past Webex's chat readability. */
export function portLabelFor(dev) {
const kind = dev?._voiceDiagKind || 'device';
const deviceLabel = dev?.name || dev?.mac || (kind === 'dect-base' ? 'DECT base' : 'Phone');
const switchName = dev?.meraki?.switchName || dev?.meraki?.deviceName || '—';
const portName = dev?.meraki?.portName || dev?.meraki?.port || '—';
return {
kind,
deviceLabel,
switchName,
portName,
portLabel: `${switchName}:${portName}`,
mac: dev?.mac || null,
};
}