Rebrand NetAnalyzer -> StoreHealthAnalyzer and consolidate the store
reporting surface into a single `st [number]` command with focused
sub-modes.
Commands
- st [number] - general info (SIW + brands + Meraki net link)
- st [number] network - switches, APs, store server
- st [number] pos - registers, payment terminals, customer display
- st [number] ios - MDM-tracked iOS hardware
- st [number] phone - wired 78xx + DECT basestations/handsets with
registration state, extensions and main DID
- st [number] av - Atlas AMPs + MDM-tracked Apple TVs, video
walls, music players, LED displays
- Removed `analyze` in favor of the unified `st` surface
Integrations
- integrations/webex: Service App OAuth with rotating refresh tokens,
seed + cleanup scripts, tokens/ storage (git-ignored)
- integrations/atlas: Xyte client + cached device discovery keyed on
zero-padded 6-digit store numbers, cold-cache failure -> unavailable
banner instead of a misleading empty result
- services/webexPhone, services/webexService, services/avService: shape
raw upstream data into the report layer's contract
- utils/merakiMatcher: FQDN hostname extraction so payment terminals
match Meraki descriptions; case-insensitive lookup
- utils/chunkReport: split long markdown replies at 7000-char boundaries
Reliability / ops
- server.js: awaited framework.stop() + 8s hard-kill timer so nodemon /
Docker restarts don't leak WDM device registrations ("excessive device
registrations")
- nodemon.json: SIGINT so the graceful path always runs
- scripts/cleanupWebexDevices.js: one-shot WDM cleanup utility
- Group-space routing: hears() regexes tolerate the leading @BotName
prefix Webex prepends to mentions
- Replaced HTML-unsafe <number> placeholders with [number] in all help
strings
Remote agent containerization
- docker/remote-agent/: multi-stage node:22-alpine image, non-root user,
tini for signal handling, minimal deps (ws/axios/dotenv)
- docker/remote-agent/package.sh: docker buildx build defaulting to
linux/amd64 (with override), saves image + assembles deploy/ + writes
SHA256 + zips for offline transfer
- docker/remote-agent/deploy/: runtime docker-compose.yml, install.sh
with platform sanity check, remote-host README
- .dockerignore + .gitignore updates for build artifacts and dist bundles
- npm run agent:package convenience script
Cleanup
- Dropped storeHealth.js / HealthReport.js and their tests/mocks in favor
of the shared storeDetail pipeline
- Store model handles null SIW records gracefully; toSummary always
ends with a newline so the Meraki link sits on its own line
Tests
- 144 tests across 14 suites passing; new coverage for atlasClient,
atlasDevices, avService, avCategory classification, webexPhone,
webexServiceAppAuth, storeDetail integration, siw, chunkReport and
the updated meraki matcher
Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.5 KiB
JavaScript
105 lines
3.5 KiB
JavaScript
require('dotenv').config();
|
|
const logger = require('../utils/logger');
|
|
|
|
/**
|
|
* Required environment variables for core operation.
|
|
* The app will refuse to start if these are missing.
|
|
*/
|
|
const REQUIRED_ENV_VARS = ['WEBEX_ACCESS_TOKEN', 'MERAKI_API_KEY', 'MERAKI_ORG_ID', 'WS_TOKEN'];
|
|
|
|
/**
|
|
* Recommended environment variables for full functionality.
|
|
* Warnings will be logged but startup will continue.
|
|
*/
|
|
const RECOMMENDED_ENV_VARS = [
|
|
'SIW_BASE_URL',
|
|
'SIW_USERNAME',
|
|
'SIW_PASSWORD',
|
|
'WS1_BASE_URL',
|
|
'WS1_TOKEN_URL',
|
|
'WS1_CLIENT_ID',
|
|
'WS1_CLIENT_SECRET',
|
|
'WS1_TENANT_CODE',
|
|
// Webex Service App (only needed for `st <store> phone`). Missing values
|
|
// surface as an unavailable banner in phone mode rather than a fatal error.
|
|
'WEBEX_CLIENT_ID',
|
|
'WEBEX_CLIENT_SECRET',
|
|
// Atlas / Xyte (only needed for `st <store> av`). Missing key surfaces as
|
|
// an inline banner in AV mode; MDM-side AV devices still render.
|
|
'ATLAS_AUTH_KEY',
|
|
];
|
|
|
|
function validateEnvironment() {
|
|
const missingRequired = REQUIRED_ENV_VARS.filter(
|
|
key => !process.env[key] || process.env[key].trim() === ''
|
|
);
|
|
const missingRecommended = RECOMMENDED_ENV_VARS.filter(
|
|
key => !process.env[key] || process.env[key].trim() === ''
|
|
);
|
|
|
|
if (missingRequired.length > 0) {
|
|
logger.error('Missing required environment variables', { missing: missingRequired });
|
|
throw new Error(`Missing required environment variables: ${missingRequired.join(', ')}`);
|
|
}
|
|
|
|
if (missingRecommended.length > 0) {
|
|
logger.warn('Optional environment variables not set; some features may be limited', {
|
|
missing: missingRecommended,
|
|
});
|
|
}
|
|
|
|
const wsPort = parseInt(process.env.WS_PORT, 10);
|
|
if (process.env.WS_PORT && (isNaN(wsPort) || wsPort < 1 || wsPort > 65535)) {
|
|
throw new Error('WS_PORT must be a valid port number between 1 and 65535');
|
|
}
|
|
|
|
logger.info('Environment validation passed');
|
|
}
|
|
|
|
// Skip validation when running unit tests so tests don't require a fully
|
|
// populated .env. Integration tests can opt in by unsetting JEST_WORKER_ID.
|
|
if (!process.env.JEST_WORKER_ID) {
|
|
validateEnvironment();
|
|
}
|
|
|
|
module.exports = {
|
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
webex: {
|
|
token: process.env.WEBEX_ACCESS_TOKEN,
|
|
name: process.env.BOT_NAME || 'StoreHealthAnalyzer',
|
|
},
|
|
meraki: {
|
|
baseUrl: 'https://api.meraki.com/api/v1',
|
|
apiKey: process.env.MERAKI_API_KEY,
|
|
orgId: process.env.MERAKI_ORG_ID,
|
|
},
|
|
ws: {
|
|
port: parseInt(process.env.WS_PORT) || 8080,
|
|
token: process.env.WS_TOKEN,
|
|
},
|
|
siw: {
|
|
baseUrl: process.env.SIW_BASE_URL,
|
|
username: process.env.SIW_USERNAME,
|
|
password: process.env.SIW_PASSWORD,
|
|
},
|
|
mdm: {
|
|
baseUrl: process.env.WS1_BASE_URL,
|
|
tokenUrl: process.env.WS1_TOKEN_URL,
|
|
clientId: process.env.WS1_CLIENT_ID,
|
|
clientSecret: process.env.WS1_CLIENT_SECRET,
|
|
tenantCode: process.env.WS1_TENANT_CODE,
|
|
},
|
|
webexServiceApp: {
|
|
clientId: process.env.WEBEX_CLIENT_ID,
|
|
clientSecret: process.env.WEBEX_CLIENT_SECRET,
|
|
// Absolute path resolved by WebexServiceAppAuth at construction time.
|
|
tokensPath: process.env.WEBEX_TOKENS_PATH || './tokens/webex-service-tokens.json',
|
|
},
|
|
atlas: {
|
|
// Long-lived API key; sent verbatim as the Authorization header value
|
|
// (no Bearer prefix). The Atlas client throws AtlasUnavailableError
|
|
// when this is unset so the AV renderer can show a banner.
|
|
authKey: process.env.ATLAS_AUTH_KEY,
|
|
baseUrl: process.env.ATLAS_BASE_URL || 'https://hub.xyte.io/core/v1',
|
|
},
|
|
};
|