// 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, }; }, };