DECT basestations rely on multicast for handset discovery/registration.
When Meraki switches have IGMP snooping enabled without a querier —
or per-switch overrides that deviate from a DECT-safe policy — those
frames are pruned and DECT handsets silently fail to register.
- integrations/meraki/switches.js: getSwitchMulticastSettings,
setSwitchMulticastSettings, and pure summarizeMulticast verdict fn.
- services/phoneService.js: fires the multicast fetch as soon as the
network id is known, overlapping DECT enrichment; attaches
data.multicast summary to the collector output. Non-fatal on error.
- services/renderers/phoneStatusRenderer.js: emits a single warning
line inside the DECT Basestations section only when needsFix is
true, itemizing which parts deviate (default snoop, default flood,
N overrides).
- commands/igmpFix.js: frozen DECT_SAFE_MULTICAST_PAYLOAD constant
({snoop:false, flood:true, overrides:[]}), adaptive-card builder,
and confirm/cancel handlers.
- commands/phoneStatus.js: appends the adaptive card when needsFix
and the trigger came from chat (skipped on HTTP path).
- index.js: IGMP_FIX_ACTIONS set + dispatch branch mirroring the
HOST_ASSIGN pattern (one-shot pending lookup, censorActionCard,
domain call).
- utils/pendingIgmpFixes.js: 15-min TTL pending-card store.
- tests: 11 summarizer cases (all deviation permutations + malformed
input), 4 renderer cases (each warning-line shape + silence when
needsFix false or no DECT), and a frozen-constant regression guard
on the PUT payload. Full suite: 47/47 passing.
112 lines
4.7 KiB
JavaScript
112 lines
4.7 KiB
JavaScript
// src/commands/phoneStatus.js
|
||
//
|
||
// Chat + HTTP entry point for /phonestatus. The heavy rendering lives in
|
||
// services/renderers/phoneStatusRenderer.js so the Jira poller can emit
|
||
// the same markdown (see services/jiraPollerService.js). This handler
|
||
// stays thin: parse args, call the collector, hand data to the renderer,
|
||
// respond.
|
||
|
||
import { randomUUID } from 'node:crypto';
|
||
import { collectPhoneStatus } from '../services/phoneService.js';
|
||
import { renderPhoneStatusMarkdown } from '../services/renderers/phoneStatusRenderer.js';
|
||
import { buildIgmpFixCard } from './igmpFix.js';
|
||
import { pendingIgmpFixes } from '../utils/pendingIgmpFixes.js';
|
||
import { extractRequester } from '../utils/requester.js';
|
||
import { logger } from '../utils/logger.js';
|
||
|
||
export async function handlePhoneStatus(bot, trigger) {
|
||
logger('phone:status', 'Handler entered', 'debug');
|
||
|
||
// Support both Webex (args) and HTTP (query) calls
|
||
const query = trigger.query || {};
|
||
const args = trigger.args || [];
|
||
|
||
let storeNum = args[0]?.trim() || query.storeNum || query.store || query.s;
|
||
|
||
const isDetailed = (args[1]?.toLowerCase() === 'detailed') ||
|
||
(query.mode === 'detailed') ||
|
||
(query.detailed === 'true' || query.detailed === true);
|
||
|
||
if (!storeNum || !/^\d{2,4}$/.test(storeNum)) {
|
||
const errorMsg = 'Please provide a 2–4 digit store number.\n' +
|
||
'Example: `/phonestatus 782` (or `detailed` / `?detailed=true` for more; includes Meraki links) or `https://.../phonestatus?storeNum=782`';
|
||
await bot.say('markdown', errorMsg);
|
||
return;
|
||
}
|
||
|
||
logger('phone:status', `Collecting phone status for store ${storeNum}`, 'debug');
|
||
|
||
try {
|
||
const data = await collectPhoneStatus(storeNum);
|
||
if (!data) throw new Error('collectPhoneStatus returned undefined');
|
||
|
||
// JSON alt-output path (kept in the handler because it bypasses
|
||
// markdown rendering entirely — no shared renderer applies).
|
||
if (query.format === 'json' || (args[1] && args[1].toLowerCase() === 'json')) {
|
||
const jsonPayload = {
|
||
store: storeNum,
|
||
mainNumber: data.locationMainNumber,
|
||
timezone: (data.telephonyProfile && data.telephonyProfile.timeZone) || null,
|
||
person: data.person ? { displayName: data.person.displayName, phoneNumbers: data.person.phoneNumbers } : null,
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
await bot.say('markdown', '```json\n' + JSON.stringify(jsonPayload, null, 2) + '\n```');
|
||
return;
|
||
}
|
||
|
||
const reply = renderPhoneStatusMarkdown(data, {
|
||
storeNum,
|
||
detailed: isDetailed,
|
||
footer: true,
|
||
});
|
||
await bot.say('markdown', reply || 'No data available.');
|
||
|
||
// IGMP-snooping remediation card — only when (a) the multicast
|
||
// summary flagged deviation AND (b) we know the networkId (can't
|
||
// fix what we can't address) AND (c) the invocation came from
|
||
// chat, not HTTP. HTTP callers don't have adaptive-card UX; the
|
||
// remediation surface for them is a future gated POST endpoint.
|
||
// `trigger.person` is populated by the framework for chat triggers
|
||
// and absent for HTTP triggers (see index.js command dispatch).
|
||
if (data.multicast?.needsFix && data.multicast.networkId && trigger.person) {
|
||
const cardId = randomUUID();
|
||
const requester = extractRequester(trigger);
|
||
pendingIgmpFixes.set(cardId, {
|
||
networkId: data.multicast.networkId,
|
||
networkName: data.multicast.networkName,
|
||
storeNum,
|
||
requester,
|
||
// Only the summary is stashed. The fix payload is a constant,
|
||
// so we don't need the raw snapshot — keeps the pending-store
|
||
// memory footprint tiny and avoids the temptation to
|
||
// read-then-mutate at PUT time.
|
||
summary: {
|
||
defaultSnoopOn: data.multicast.defaultSnoopOn,
|
||
defaultFloodOff: data.multicast.defaultFloodOff,
|
||
deviatingOverrides: data.multicast.deviatingOverrides || [],
|
||
},
|
||
});
|
||
const card = buildIgmpFixCard({
|
||
storeNum,
|
||
networkName: data.multicast.networkName,
|
||
summary: {
|
||
defaultSnoopOn: data.multicast.defaultSnoopOn,
|
||
defaultFloodOff: data.multicast.defaultFloodOff,
|
||
deviatingOverrides: data.multicast.deviatingOverrides || [],
|
||
},
|
||
cardId,
|
||
});
|
||
await bot.say({
|
||
markdown: `Multicast policy on store ${storeNum}'s network deviates from DECT-safe defaults. Review and confirm:`,
|
||
attachments: [{
|
||
contentType: 'application/vnd.microsoft.card.adaptive',
|
||
content: card,
|
||
}],
|
||
});
|
||
}
|
||
|
||
} catch (err) {
|
||
logger('phone:status', `Error collecting phone status for store ${storeNum}: ${err.message}`, 'error');
|
||
await bot.say('markdown', `Error collecting phone status: ${err.message}`);
|
||
}
|
||
}
|