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>
138 lines
4.3 KiB
JavaScript
138 lines
4.3 KiB
JavaScript
// src/services/voiceDiag/checks/phoneOnline.js
|
|
//
|
|
// Summarises how many of the store's registered devices are online
|
|
// right now, plus a per-device drill-down. Deliberately reuses the
|
|
// phone-status snapshot already fetched during buildContext() — this
|
|
// check makes ZERO new API calls, which keeps it fast and keeps the
|
|
// /voicediag output aligned with /phonestatus for the same store
|
|
// (same source, same view of "which phones are up right now").
|
|
//
|
|
// Signal shape:
|
|
// status: 'error' when at least one phone is offline (a store with
|
|
// a dead phone is a real ticket).
|
|
// status: 'warn' when the store has no registered devices at all
|
|
// (this is often "we didn't ship / provision yet" but still worth
|
|
// surfacing).
|
|
// status: 'ok' when every device reports `connected`.
|
|
//
|
|
// Device shape from collectPhoneStatus:
|
|
// data.phones.data[i] = { mac, name, status, lastSeen, firmware, model, ... }
|
|
// data.dectBasestations[i] = { mac, name, status, lastSeen, firmware, ... }
|
|
//
|
|
// Statuses observed in the wild: 'connected', 'disconnected',
|
|
// 'unknown', 'activating', 'offline'. We treat 'connected' as the
|
|
// only truly OK value; everything else counts as offline for the
|
|
// purposes of the summary.
|
|
|
|
const OK_STATUSES = new Set(['connected']);
|
|
|
|
function isOnline(dev) {
|
|
const s = String(dev?.status || '').toLowerCase();
|
|
return OK_STATUSES.has(s);
|
|
}
|
|
|
|
export const PHONE_ONLINE_STANDARDS = Object.freeze({
|
|
// Every registered device must report 'connected'. Anything else
|
|
// (disconnected, unknown, activating, offline) is treated as
|
|
// offline.
|
|
status: 'connected',
|
|
});
|
|
|
|
export const phoneOnlineCheck = {
|
|
id: 'phoneOnline',
|
|
label: 'Phone Online Status',
|
|
requires: ['phoneStatus'],
|
|
// No new API scope required — reuses collectPhoneStatus() output,
|
|
// which is already gated on the /phonestatus set of scopes.
|
|
scope: null,
|
|
standards: PHONE_ONLINE_STANDARDS,
|
|
|
|
async run(ctx) {
|
|
const data = ctx.phoneStatus;
|
|
if (!data) {
|
|
return {
|
|
status: 'skipped',
|
|
message: 'phoneStatus snapshot unavailable — cannot report device online counts.',
|
|
details: null,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
const phones = Array.isArray(data.phones?.data) ? data.phones.data : [];
|
|
const dectBases = Array.isArray(data.dectBasestations) ? data.dectBasestations : [];
|
|
|
|
const phonesOnline = phones.filter(isOnline);
|
|
const phonesOffline = phones.filter((p) => !isOnline(p));
|
|
const basesOnline = dectBases.filter(isOnline);
|
|
const basesOffline = dectBases.filter((b) => !isOnline(b));
|
|
|
|
const totalDevices = phones.length + dectBases.length;
|
|
|
|
if (totalDevices === 0) {
|
|
return {
|
|
status: 'warn',
|
|
message: 'No desk phones or DECT basestations are registered for this store.',
|
|
details: {
|
|
phonesTotal: 0,
|
|
phonesOnline: 0,
|
|
phonesOffline: 0,
|
|
basesTotal: 0,
|
|
basesOnline: 0,
|
|
basesOffline: 0,
|
|
},
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
const offlineCount = phonesOffline.length + basesOffline.length;
|
|
const summary =
|
|
`Desk phones: ${phonesOnline.length}/${phones.length} online. ` +
|
|
`DECT bases: ${basesOnline.length}/${dectBases.length} online.`;
|
|
|
|
const offlineList = [
|
|
...phonesOffline.map((p) => ({
|
|
kind: 'phone',
|
|
name: p.name || 'Unknown phone',
|
|
mac: p.mac || null,
|
|
status: p.status || 'unknown',
|
|
lastSeen: p.lastSeen || null,
|
|
model: p.model || null,
|
|
})),
|
|
...basesOffline.map((b) => ({
|
|
kind: 'dect-base',
|
|
name: b.name || 'Unknown DECT base',
|
|
mac: b.mac || null,
|
|
status: b.status || 'unknown',
|
|
lastSeen: b.lastSeen || null,
|
|
model: b.model || null,
|
|
})),
|
|
];
|
|
|
|
const details = {
|
|
phonesTotal: phones.length,
|
|
phonesOnline: phonesOnline.length,
|
|
phonesOffline: phonesOffline.length,
|
|
basesTotal: dectBases.length,
|
|
basesOnline: basesOnline.length,
|
|
basesOffline: basesOffline.length,
|
|
offlineDevices: offlineList,
|
|
};
|
|
|
|
if (offlineCount === 0) {
|
|
return {
|
|
status: 'ok',
|
|
message: summary,
|
|
details,
|
|
remediation: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'error',
|
|
message:
|
|
`${offlineCount} device(s) offline for this store. ${summary}`,
|
|
details,
|
|
remediation: null,
|
|
};
|
|
},
|
|
};
|