netanalyzer/server.js
Joseph McQueen 0a46d25bd6 chore: project cleanup — dead code, logging, tests, WS hardening
- Delete unused dataMerger, formatter, Device model, dead service exports,
  and the empty agents/ + .gitkeep placeholders.
- Extract STORE_MODES and MDM device-type filters into a shared constants.js.
- Anchor bot regexes (^help|^store|^analyze) so "analyze store 305" no
  longer fires both handlers; replace catch-all noise.
- Hoist inline require() calls in integrations to top-of-file imports.
- Harden WebSocket server: Authorization header support, single-agent
  enforcement, bounded pending requests, server-level error handler,
  coalesced cache refresh in Meraki client.
- Wrap Meraki/MDM network calls with withRetry; add request timeouts.
- Migrate all console.* calls onto utils/logger.js (LOG_LEVEL aware);
  drive Webex framework logLevel from env.
- Refactor storeDetail.js: shared renderClientLine + buildMdmSection
  helpers cut duplication roughly in half.
- Refresh README structure, document LOG_LEVEL, add npm run agent script,
  add jest testMatch + new tests (handlers, HealthReport, Store, ws).

Verified: npm run lint clean, 7 suites / 31 tests passing.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 17:21:22 -04:00

108 lines
3.2 KiB
JavaScript

const Framework = require('webex-node-bot-framework');
const config = require('./config');
const { startWebSocketServer, stopWebSocketServer } = require('./services/websocket');
const { handleStoreCommand, handleAnalyzeCommand, handleHelpCommand } = require('./bot/handlers');
const logger = require('./utils/logger');
const framework = new Framework({
token: config.webex.token,
removeWebhooksOnStart: true,
logLevel: config.logLevel,
});
logger.info('Starting NetAnalyzer Framework', { botName: config.webex.name });
// Register command handlers (anchored at the start of the message so
// "please analyze store 305" doesn't fire both store and analyze handlers).
framework.hears(
/^\s*help\b/i,
handleHelpCommand,
'Show available commands (try: help, help store, help analyze)'
);
framework.hears(
/^\s*store\b/i,
handleStoreCommand,
'store <number> — info + network + server\nstore <number> pos — POS devices\nstore <number> ios — iOS devices'
);
framework.hears(
/^\s*analyze\b/i,
handleAnalyzeCommand,
'analyze <number> — health summary\nanalyze <number> pos — POS health (broken only)\nanalyze <number> ios — iOS health (broken only)'
);
// Friendly fallback for anything that didn't match the commands above.
framework.hears(
/.*/,
(bot, trigger) => {
logger.info('Unhandled message', { text: trigger.message.text });
bot.say(
'I heard: ' +
trigger.message.text +
'\n\nTry `store <number>`, `store <number> pos`, `analyze <number>`, or `help` for commands.'
);
},
99999
);
framework.on('initialized', () => {
logger.info('Framework initialized and connected via WebSocket');
});
framework.on('spawn', (bot, _id, addedBy) => {
logger.info('Bot spawned in room', { room: bot.room.title || 'Unknown' });
if (addedBy) {
bot.say(
'NetAnalyzer is ready!\n\nTry `store 782`, `store 782 pos`, `analyze 782`, or type `help` for all commands.'
);
}
});
framework
.start()
.then(() => {
logger.info('WebSocket server for remote agent starting');
startWebSocketServer();
})
.catch(err => {
logger.error('Error starting framework', { error: err.message });
process.exit(1);
});
// ==================== Graceful Shutdown ====================
let isShuttingDown = false;
async function shutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
logger.info('Shutting down gracefully', { signal });
try {
stopWebSocketServer();
// webex-node-bot-framework may not expose a clean stop; do what we can.
if (framework && typeof framework.stop === 'function') {
await framework.stop().catch(() => {});
}
logger.info('Cleanup complete. Exiting.');
} catch (err) {
logger.error('Error during shutdown', { error: err.message });
} finally {
process.exit(0);
}
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('uncaughtException', err => {
logger.error('Uncaught Exception', { error: err.message, stack: err.stack });
shutdown('uncaughtException');
});
process.on('unhandledRejection', reason => {
logger.error('Unhandled Rejection', {
reason: reason instanceof Error ? reason.message : String(reason),
});
});