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>
269 lines
13 KiB
Markdown
269 lines
13 KiB
Markdown
# StoreHealthAnalyzer
|
||
|
||
Webex bot that provides store-level network and device health analysis by correlating data from Meraki, SIW (Store Information Warehouse), and Workspace ONE MDM.
|
||
|
||
## What it does
|
||
|
||
- `st [number]` — Store info pulled from SIW (location, brand, status, environment). No upstream checks.
|
||
- `st [number] network` — Switch, AP, and store-server health (Meraki + MDM).
|
||
- `st [number] pos` — POS devices: registers, mobile registers, printers, payment terminals.
|
||
- `st [number] ios` — iOS devices (mainly iPhones).
|
||
- `st [number] phone` — wired Cisco 78xx IP phones (Meraki-matched by MAC) and DECT basestations + handsets via Webex Service App.
|
||
- `st [number] av` — A/V hardware snapshot: Atlas (Xyte) AMPs **plus** MDM-tracked Apple TVs, video walls (`*VW*`), music players (`*MSC*`), and LED displays (`*LED*`). Each row is MAC-matched into Meraki for the where-it's-connected line.
|
||
|
||
The bot helps operations teams quickly understand the connectivity and device state of a retail location.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌─────────────────┐ ┌─────────────────────┐
|
||
│ Webex Bot │◄────────►│ StoreHealthAnalyzer │
|
||
│ (Webex rooms) │ Webex │ (server.js) │
|
||
└─────────────────┘ └─────────┬───────────┘
|
||
│
|
||
│ WebSocket (authenticated)
|
||
▼
|
||
┌─────────────────────┐
|
||
│ Remote Agent │◄──► SIW API (Basic Auth)
|
||
│ (remoteAgent.js) │◄──► Workspace ONE MDM
|
||
└─────────────────────┘
|
||
│
|
||
▼
|
||
Meraki API (direct from server)
|
||
```
|
||
|
||
**Why the remote agent?**
|
||
SIW and some MDM systems are only reachable from specific internal networks. The remote agent runs in that environment and proxies requests back to the main StoreHealthAnalyzer server over an authenticated WebSocket.
|
||
|
||
## Prerequisites
|
||
|
||
- Node.js >= 18
|
||
- A Webex bot account with access token
|
||
- Meraki API key with organization access
|
||
- Access to your organization's SIW API and Workspace ONE (AirWatch) environment
|
||
- Ability to run the remote agent on a machine that can reach SIW/MDM
|
||
|
||
## Setup
|
||
|
||
1. **Clone and install**
|
||
|
||
```bash
|
||
git clone <repo>
|
||
cd storehealthanalyzer
|
||
npm install
|
||
```
|
||
|
||
2. **Configure environment**
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
# Edit .env with your real credentials
|
||
```
|
||
|
||
Required values are documented in `.env.example`.
|
||
|
||
3. **(Optional but recommended) Rotate all secrets**
|
||
If you previously had credentials in the repository, rotate:
|
||
- Webex bot token
|
||
- Meraki API key
|
||
- SIW credentials
|
||
- Workspace ONE client secret + tenant code
|
||
- WS_TOKEN
|
||
|
||
## Running
|
||
|
||
### Main server + bot (where the Webex connection lives)
|
||
|
||
```bash
|
||
npm start
|
||
# or for development with auto-reload
|
||
npm run dev
|
||
```
|
||
|
||
This starts:
|
||
|
||
- The Webex bot framework (listens for messages in Webex spaces)
|
||
- The WebSocket server on the port defined in `WS_PORT` (default 8080)
|
||
|
||
### Remote agent (run on a machine that can reach internal systems)
|
||
|
||
Two options — pick whichever fits your host.
|
||
|
||
**Option A: run directly with Node**
|
||
|
||
```bash
|
||
# On the internal machine
|
||
node remoteAgent.js
|
||
```
|
||
|
||
Make sure `WS_URL` in its environment points to the main server with the correct `WS_TOKEN`.
|
||
|
||
**Option B: run as a Docker container (build + run on the same host)**
|
||
|
||
Ship the agent as a standalone container instead of installing Node on the host. Full instructions live in [`docker/remote-agent/README.md`](docker/remote-agent/README.md); the short version:
|
||
|
||
```bash
|
||
# Build (from the repo root)
|
||
docker build -f docker/remote-agent/Dockerfile -t sha-remote-agent:latest .
|
||
|
||
# Configure
|
||
cp docker/remote-agent/.env.example docker/remote-agent/.env
|
||
# Edit WS_URL + WS_TOKEN
|
||
|
||
# Run (compose is the easy path)
|
||
docker compose -f docker/remote-agent/docker-compose.yml up -d --build
|
||
```
|
||
|
||
The container is small (~60–80 MB), ships only `remoteAgent.js` + its three runtime deps (`ws`, `axios`, `dotenv`), and runs as the unprivileged `node` user. `tini` is PID 1 so `docker stop` closes the websocket cleanly.
|
||
|
||
**Option C: package as a portable ZIP for offline / manual transfer**
|
||
|
||
If the remote host can't reach a registry (or you just want a one-file drop), package everything into a self-contained ZIP:
|
||
|
||
```bash
|
||
# On the dev machine (from the repo root)
|
||
npm run agent:package
|
||
# → docker/remote-agent/dist/sha-remote-agent-<version>.zip
|
||
```
|
||
|
||
Then copy the ZIP to the remote host and:
|
||
|
||
```bash
|
||
unzip sha-remote-agent-<version>.zip
|
||
cd sha-remote-agent-<version>
|
||
./install.sh # loads the image, seeds .env, starts the container
|
||
```
|
||
|
||
The bundle contains the Docker image tarball, a runtime `docker-compose.yml`, a SHA-256 checksum, and an installer that verifies + loads + starts. Details in [`docker/remote-agent/deploy/README.md`](docker/remote-agent/deploy/README.md) (also included inside the ZIP as its top-level `README.md`).
|
||
|
||
### Webex Service App (only needed for `st [number] phone`)
|
||
|
||
The phone command talks to Webex APIs (people, devices, telephony) as a Service App, separate from the bot identity. Cisco rotates the refresh token on every call, so the only manual step is seeding the initial tokens once.
|
||
|
||
1. Set `WEBEX_CLIENT_ID` and `WEBEX_CLIENT_SECRET` in `.env` (from the Service App registration in the Webex Developer Portal).
|
||
2. Seed the tokens file:
|
||
|
||
```bash
|
||
# Default: imports the tokens JSON from collabFinder, verifies it via one immediate refresh
|
||
npm run webex:seed
|
||
|
||
# Or import a tokens JSON from a specific path
|
||
npm run webex:seed -- --from-file /path/to/webex-service-tokens.json
|
||
|
||
# Or seed with a raw refresh token string
|
||
npm run webex:seed -- --refresh-token <refresh-token-value>
|
||
```
|
||
|
||
The seed step performs one `grant_type=refresh_token` call against `https://webexapis.com/v1/access_token` and writes the rotated pair to `WEBEX_TOKENS_PATH` (default `./tokens/webex-service-tokens.json`, gitignored).
|
||
|
||
3. From then on, the auth singleton keeps the token fresh automatically (5-minute safety buffer ahead of the stated expiry). If the refresh token is ever revoked, `st [number] phone` degrades to a single warning banner ("Webex phone data unavailable") instead of erroring — re-seed and try again.
|
||
|
||
### Atlas / Xyte (only needed for `st [number] av`)
|
||
|
||
The AV command pulls hardware data from two places:
|
||
|
||
- **Atlas (`hub.xyte.io`)** — long-lived API key. Add `ATLAS_AUTH_KEY` (and optionally `ATLAS_BASE_URL`) to `.env`. The Atlas client maintains a 1-hour in-process cache of the org-wide device list, then filters by zero-padded store number (e.g. store 782 → `US000782*`) to find that store's AMPs.
|
||
- **MDM (Workspace ONE)** — uses the existing WebSocket-proxied MDM connection. The AV view filters MDM devices whose friendly name matches `/(VW|MSC|LED|AppleTV)/i`, mirroring the collabFinder strict-AV filter.
|
||
|
||
If `ATLAS_AUTH_KEY` is unset (or the Atlas API is unreachable), the AV report still renders the MDM-tracked devices and shows an inline "Atlas AV data unavailable" banner above them. Likewise, an MDM outage doesn't suppress the Atlas section. Both sources empty → `_No AV hardware registered for this store._`.
|
||
|
||
## Bot Commands
|
||
|
||
In any Webex space where the bot is a member:
|
||
|
||
- `st 782` — store info (location, brand, status, environment)
|
||
- `st 782 network` — switches, APs, store server(s)
|
||
- `st 782 pos` — POS devices (registers, mobile registers, customer displays, printers, payment terminals)
|
||
- `st 782 ios` — iOS devices (iPhones)
|
||
- `st 782 phone` — wired (78xx) + DECT bases & handsets via Webex
|
||
- `st 782 av` — Atlas AMPs + MDM-tracked Apple TVs / video walls / music / LED displays
|
||
- `help` / `help st` — show this list inside Webex.
|
||
|
||
The bot answers single-message replies by default and only splits them into multiple messages when the output exceeds ~7000 characters (Webex's safe message-size cap).
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
.
|
||
├── bot/
|
||
│ └── handlers.js # Webex command handlers (store / analyze / help)
|
||
├── config/
|
||
│ └── index.js # Centralized env-driven configuration + validation
|
||
├── integrations/
|
||
│ ├── storeDetail.js # Per-mode store report builder (Meraki + SIW + MDM + Webex phones + Atlas AV)
|
||
│ ├── atlas/
|
||
│ │ ├── atlasClient.js # Atlas (hub.xyte.io) axios wrapper + AtlasUnavailableError
|
||
│ │ └── atlasDevices.js # Paginated org-device fetcher + 1h in-memory cache
|
||
│ └── webex/
|
||
│ └── WebexServiceAppAuth.js # Service App OAuth singleton w/ auto-rotating refresh
|
||
├── models/
|
||
│ └── Store.js # Store domain model (normalizes SIW location + general)
|
||
├── scripts/
|
||
│ ├── cleanupWebexDevices.js
|
||
│ └── seedWebexTokens.js # One-shot Webex Service App token seed
|
||
├── services/
|
||
│ ├── meraki.js # Meraki API client (cached, retried)
|
||
│ ├── siw.js # SIW calls (proxied via the remote agent)
|
||
│ ├── mdm.js # Workspace ONE MDM client (retried)
|
||
│ ├── webexService.js # Webex API axios wrapper (auto-refresh on 401)
|
||
│ ├── webexPhone.js # Phone discovery: 78xx + DECT bases + handsets
|
||
│ ├── avService.js # AV (Atlas) device shaper for `st [number] av`
|
||
│ └── websocket.js # WS server + proxyRequest helper
|
||
├── tests/
|
||
│ ├── *.test.js # Unit tests
|
||
│ ├── integration/ # Integration tests using mocks
|
||
│ └── mocks/ # Service mocks for tests
|
||
├── utils/
|
||
│ ├── chunkReport.js # Split markdown into <7000-char Webex messages
|
||
│ ├── logger.js # Structured JSON logger (LOG_LEVEL aware)
|
||
│ ├── merakiMatcher.js # Device ↔ Meraki client matching
|
||
│ ├── retry.js # withRetry wrapper (exponential backoff)
|
||
│ └── validate.js # Input parsers
|
||
├── docker/
|
||
│ └── remote-agent/ # Standalone Docker packaging for remoteAgent.js
|
||
│ ├── Dockerfile
|
||
│ ├── docker-compose.yml # Local build/run
|
||
│ ├── package.json # Minimal deps: ws + axios + dotenv
|
||
│ ├── package.sh # Build + save + zip → dist/
|
||
│ ├── .env.example
|
||
│ ├── README.md
|
||
│ ├── deploy/ # Files bundled into the deploy ZIP
|
||
│ │ ├── docker-compose.yml # Runtime-only (references loaded image)
|
||
│ │ ├── install.sh # docker load + .env bootstrap + up
|
||
│ │ └── README.md # Remote-host instructions (also in the ZIP)
|
||
│ └── dist/ # Generated ZIPs (gitignored)
|
||
├── constants.js # Shared constants (STORE_MODES, MDM device types)
|
||
├── remoteAgent.js # Lightweight proxy client (run on internal host)
|
||
├── server.js # Main entry point (bot + WS server)
|
||
├── package.json
|
||
├── eslint.config.js
|
||
├── .prettierrc.json
|
||
├── .dockerignore
|
||
└── .env.example
|
||
```
|
||
|
||
## Logging
|
||
|
||
The app emits single-line JSON to stdout/stderr via `utils/logger.js`. Set `LOG_LEVEL` in `.env` to one of `debug | info | warn | error` (default: `info`).
|
||
|
||
## Security Notes
|
||
|
||
- **Never commit `.env`** (it is gitignored, along with `.env.bak.*`).
|
||
- The WebSocket connection between server and remote agent is protected by a shared `WS_TOKEN`. The remote agent now sends the token in an `Authorization: Bearer` header (the legacy `?token=...` query parameter still works for older deployments but should be migrated).
|
||
- Only one remote agent may be connected at a time; a newer connection replaces the older one and any in-flight proxy requests are rejected (rather than silently hanging).
|
||
- All SIW communication uses Basic Auth and is only performed through the remote agent.
|
||
- Meraki and MDM calls use tokens that should be scoped to the minimum necessary permissions.
|
||
|
||
## Development
|
||
|
||
```bash
|
||
npm run lint # eslint + prettier check
|
||
npm run lint:fix # auto-fix lint issues
|
||
npm run format # prettier write
|
||
npm test # jest (unit + integration tests using mocks)
|
||
npm run test:watch
|
||
```
|
||
|
||
## License
|
||
|
||
ISC
|