// src/commands/igmpFix.js // // Adaptive-card inline remediation for Meraki switch multicast policy // on a store's network. Not a standalone chat command — the card is // emitted by `/voicestatus` (see commands/voiceStatus.js) whenever the // collector reports `data.multicast.needsFix`. Confirm and Cancel // flow through the framework's `attachmentAction` event dispatched // from index.js, which follows the same pending-card / one-shot / // censor pattern as /webexhost and /offboarduser. // // End-state target (per plan): every store network's switch multicast // settings pinned to: // defaultSettings.igmpSnoopingEnabled = false // defaultSettings.floodUnknownMulticastTrafficEnabled = true // overrides = [] // // The target is a hardcoded constant. We do NOT read-then-mutate the // current snapshot at PUT time — the multicast object schema has only // these two fields, so there's nothing else to preserve, and a // constant PUT sidesteps read/write races. import { logger } from '../utils/logger.js'; import { describeRequester } from '../utils/requester.js'; import { setSwitchMulticastSettings } from '../integrations/meraki/switches.js'; // Exported so tests can regression-guard the payload shape and so the // applyDectSafeMulticast function has a single source of truth. export const DECT_SAFE_MULTICAST_PAYLOAD = Object.freeze({ defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true, }, overrides: [], }); /** * Build the adaptive-card object shown in-chat under a /voicestatus * reply when multicast policy deviates from DECT-safe defaults. The * card renders whatever the summarizer flagged as deviating so the * clicker knows exactly what will change. * * @param {object} args * @param {string} args.storeNum * @param {string} args.networkName * @param {object} args.summary data.multicast summary shape * @param {string} args.cardId uuid stashed in pendingIgmpFixes * @returns {object} Adaptive Card 1.3 JSON. */ export function buildIgmpFixCard({ storeNum, networkName, summary, cardId }) { const facts = []; if (summary.defaultSnoopOn) { facts.push({ title: 'Default IGMP snooping', value: 'ON → will change to OFF' }); } else { facts.push({ title: 'Default IGMP snooping', value: 'OFF (no change)' }); } if (summary.defaultFloodOff) { facts.push({ title: 'Default flood-unknown multicast', value: 'OFF → will change to ON' }); } else { facts.push({ title: 'Default flood-unknown multicast', value: 'ON (no change)' }); } const overrideCount = summary.deviatingOverrides?.length || 0; facts.push({ title: 'Switch/stack overrides', value: overrideCount > 0 ? `${overrideCount} deviating → all overrides cleared` : 'none (no change)', }); return { type: 'AdaptiveCard', $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', version: '1.3', body: [ { type: 'TextBlock', size: 'Medium', weight: 'Bolder', text: `Apply DECT-safe multicast on ${networkName || `store ${storeNum}`}?`, wrap: true, }, { type: 'TextBlock', text: `This will PUT the Meraki switch multicast settings for this network so IGMP snooping is OFF, ` + `flood-unknown-multicast is ON, and all per-switch overrides are cleared. Intended to unblock DECT ` + `handset registration for store ${storeNum}.`, wrap: true, spacing: 'Small', }, { type: 'FactSet', spacing: 'Medium', facts, }, ], actions: [ { type: 'Action.Submit', title: '✅ Apply DECT-safe defaults', data: { action: 'confirm_igmp_fix', cardId }, }, { type: 'Action.Submit', title: '❌ Cancel', data: { action: 'cancel_igmp_fix', cardId }, }, ], }; } /** * Confirm handler — PUTs the constant DECT-safe payload to the * network's multicast endpoint. Called from index.js after the * pending-card lookup + one-shot delete + card censor. * * Signature matches applyHostAssignConfirmation from /webexhost so the * dispatch in index.js stays uniform across all inline-card actions. * * @param {object} bot bot-framework per-room bot * @param {object} data pending-card payload (see pendingIgmpFixes) * @param {string} _roomId unused; bot is already room-scoped * @param {object} requester from extractRequester(trigger) */ export async function applyDectSafeMulticast(bot, data, _roomId, requester) { const { networkId, networkName, storeNum, summary } = data; logger( 'igmp:audit', `CONFIRMED DECT-safe multicast on ${networkName} (${networkId}) for store ${storeNum} ` + `by ${describeRequester(requester)} — was: snoop=${summary?.defaultSnoopOn ? 'on' : 'off'}, ` + `flood=${summary?.defaultFloodOff ? 'off' : 'on'}, overrides=${summary?.deviatingOverrides?.length ?? 0}`, ); try { await setSwitchMulticastSettings(networkId, DECT_SAFE_MULTICAST_PAYLOAD); } catch (err) { logger( 'igmp:audit', `FAILED DECT-safe multicast on ${networkName} for store ${storeNum}: ${err.message}`, 'error', ); await bot.say( 'markdown', `❌ Failed to update multicast config on **${networkName || `store ${storeNum}`}**: ${err.message}`, ); return; } await bot.say( 'markdown', `✅ Multicast pinned to DECT-safe defaults on **${networkName || `store ${storeNum}`}**: ` + `IGMP snoop=OFF, flood-unknown=ON, all switch overrides cleared. ` + `Re-run \`/voicestatus ${storeNum}\` to verify.`, ); logger( 'igmp:audit', `COMPLETED DECT-safe multicast on ${networkName} for store ${storeNum}`, ); } /** * Cancel handler — logs the cancel and posts a friendly line. No * mutation. Signature matches cancelHostAssignCard for uniform * dispatch in index.js. */ export async function cancelIgmpFixCard(bot, data, _roomId, requester) { const { networkName, storeNum } = data; await bot.say( 'markdown', `❌ Multicast change cancelled for **${networkName || `store ${storeNum}`}**. No changes were made.`, ); logger( 'igmp:audit', `CANCELLED DECT-safe multicast on ${networkName} for store ${storeNum} by ${describeRequester(requester)}`, ); }