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.
67 lines
1.9 KiB
JavaScript
67 lines
1.9 KiB
JavaScript
// src/utils/pendingIgmpFixes.js
|
|
//
|
|
// In-memory store for pending IGMP-snooping / multicast-fix
|
|
// confirmation cards. Identical shape to pendingHostAssigns — every
|
|
// entry is stamped with a `timestamp` on insert, and a background
|
|
// sweep expires un-acted cards after TTL_MS so the map never leaks.
|
|
//
|
|
// Entries typically hold:
|
|
// {
|
|
// networkId, networkName, storeNum,
|
|
// requester, // from extractRequester(trigger)
|
|
// summary, // { defaultSnoopOn, defaultFloodOff, deviatingOverrides.length }
|
|
// timestamp, // auto-set
|
|
// }
|
|
//
|
|
// TTL is deliberately longer than pendingHostAssigns because a
|
|
// multicast change often gets a "hold on, let me check with the
|
|
// store first" pause before someone actually clicks. 15 minutes is
|
|
// enough for a normal handoff without the card staying live long
|
|
// enough for a stale click to fire against a since-fixed network.
|
|
|
|
import { logger } from './logger.js';
|
|
|
|
const TTL_MS = 15 * 60 * 1000; // expire un-acted cards after 15 min
|
|
const SWEEP_INTERVAL_MS = 60 * 1000; // check once a minute
|
|
|
|
const _store = new Map();
|
|
|
|
export const pendingIgmpFixes = {
|
|
set(cardId, data) {
|
|
_store.set(cardId, { ...data, timestamp: Date.now() });
|
|
return this;
|
|
},
|
|
|
|
get(cardId) {
|
|
return _store.get(cardId);
|
|
},
|
|
|
|
has(cardId) {
|
|
return _store.has(cardId);
|
|
},
|
|
|
|
delete(cardId) {
|
|
return _store.delete(cardId);
|
|
},
|
|
|
|
get size() {
|
|
return _store.size;
|
|
},
|
|
|
|
entries() {
|
|
return _store.entries();
|
|
},
|
|
};
|
|
|
|
const sweepHandle = setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [cardId, data] of _store.entries()) {
|
|
const stamped = typeof data?.timestamp === 'number' ? data.timestamp : 0;
|
|
if (now - stamped > TTL_MS) {
|
|
logger('igmp:cleanup', `Expired card ${cardId} for store ${data?.storeNum || 'unknown'} network ${data?.networkName || 'unknown'}`);
|
|
_store.delete(cardId);
|
|
}
|
|
}
|
|
}, SWEEP_INTERVAL_MS);
|
|
|
|
sweepHandle.unref?.();
|