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>
222 lines
7.4 KiB
Markdown
222 lines
7.4 KiB
Markdown
# StoreHealthAnalyzer Remote Agent — Docker
|
||
|
||
Standalone container for the remote agent that proxies SIW / MDM requests
|
||
from an internal network back to the main StoreHealthAnalyzer server over an
|
||
authenticated WebSocket.
|
||
|
||
## What ships in the image
|
||
|
||
- `node:22-alpine` runtime with [`tini`](https://github.com/krallin/tini) as
|
||
PID 1 so `docker stop` reaches Node's SIGTERM handler and the websocket
|
||
closes cleanly.
|
||
- Just the agent script (`remoteAgent.js`) and its three runtime deps
|
||
(`ws`, `axios`, `dotenv`). No bot framework, no Express, no test tooling.
|
||
- Runs as the unprivileged `node` user.
|
||
|
||
Final image size is small (roughly 60–80 MB depending on architecture),
|
||
compared to ~180 MB if the root `package.json` were installed.
|
||
|
||
## Files in this folder
|
||
|
||
| File | Purpose |
|
||
| --- | --- |
|
||
| `Dockerfile` | Two-stage build (`deps` → `runtime`). Uses the repo root as the build context so it can pull in `remoteAgent.js`. |
|
||
| `package.json` | Minimal manifest: `ws`, `axios`, `dotenv`. |
|
||
| `docker-compose.yml` | Convenience wrapper for **local** builds; run from the repo root. |
|
||
| `.env.example` | Copy to `.env`, fill in `WS_URL` + `WS_TOKEN`. |
|
||
| `package.sh` | Builds the image and produces a self-contained deploy ZIP under `dist/`. |
|
||
| `deploy/` | Files that get bundled into the deploy ZIP (runtime compose, `install.sh`, remote README). |
|
||
| `dist/` | Generated ZIPs (gitignored). |
|
||
|
||
## Prerequisites
|
||
|
||
- Docker 24+ (BuildKit is default and required for the `syntax=` line).
|
||
- The main StoreHealthAnalyzer server reachable from the host that will run
|
||
this container (outbound only — the agent doesn't listen on any port).
|
||
- A shared `WS_TOKEN` value matching the one configured on the server.
|
||
|
||
## Build
|
||
|
||
Always build from the **repository root** — the Dockerfile expects that
|
||
context so it can copy `remoteAgent.js`:
|
||
|
||
```bash
|
||
# From the repo root
|
||
docker build \
|
||
-f docker/remote-agent/Dockerfile \
|
||
-t sha-remote-agent:latest \
|
||
.
|
||
```
|
||
|
||
Tag with a version too if you plan to ship it to a registry:
|
||
|
||
```bash
|
||
docker tag sha-remote-agent:latest ghcr.io/<owner>/sha-remote-agent:1.0.0
|
||
docker push ghcr.io/<owner>/sha-remote-agent:1.0.0
|
||
```
|
||
|
||
## Configure
|
||
|
||
```bash
|
||
cp docker/remote-agent/.env.example docker/remote-agent/.env
|
||
$EDITOR docker/remote-agent/.env
|
||
```
|
||
|
||
Required values:
|
||
|
||
- `WS_URL` — websocket URL of the main server (e.g. `wss://sha.example.com/ws`).
|
||
- `WS_TOKEN` — shared secret matching the server's `WS_TOKEN`.
|
||
|
||
Both `.env` and `.env.*` are excluded by the top-level `.dockerignore`, so
|
||
the file is never baked into the image.
|
||
|
||
## Run
|
||
|
||
### Docker CLI
|
||
|
||
```bash
|
||
docker run --rm -it \
|
||
--name sha-remote-agent \
|
||
--env-file docker/remote-agent/.env \
|
||
sha-remote-agent:latest
|
||
```
|
||
|
||
Add `-d` for detached mode and `--restart unless-stopped` if you want it to
|
||
auto-recover on host reboots.
|
||
|
||
### Docker Compose (recommended)
|
||
|
||
```bash
|
||
# From the repo root
|
||
docker compose -f docker/remote-agent/docker-compose.yml up -d --build
|
||
|
||
# Tail logs
|
||
docker compose -f docker/remote-agent/docker-compose.yml logs -f
|
||
|
||
# Stop
|
||
docker compose -f docker/remote-agent/docker-compose.yml down
|
||
```
|
||
|
||
Compose sets `restart: unless-stopped` and 10 MB / 3-file JSON log rotation
|
||
so the container survives host restarts and doesn't fill the disk with
|
||
reconnect chatter.
|
||
|
||
## Deploy elsewhere (ZIP bundle — recommended)
|
||
|
||
For hosts you can't reach with a registry, use the packaging script — it
|
||
produces a single ZIP with the image, a runtime compose file, an installer,
|
||
and a checksum:
|
||
|
||
```bash
|
||
# From the repo root — defaults to building for linux/amd64
|
||
npm run agent:package
|
||
# or, equivalently:
|
||
./docker/remote-agent/package.sh
|
||
```
|
||
|
||
Output lands in `docker/remote-agent/dist/sha-remote-agent-<version>.zip`
|
||
(the folder is gitignored). Transfer that one file 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
|
||
```
|
||
|
||
Full remote-host instructions ship inside the ZIP as `README.md` and are
|
||
also visible here for reference: [`deploy/README.md`](deploy/README.md).
|
||
|
||
The script tags the image both `sha-remote-agent:<version>` and
|
||
`sha-remote-agent:latest`, so local `docker compose` still works after
|
||
packaging.
|
||
|
||
### Target-platform selection (very important on Apple Silicon)
|
||
|
||
Docker images are architecture-specific. If you build on an Apple Silicon
|
||
Mac with `docker build`, you get an `arm64` image — which will **fail to
|
||
start** on a typical x86_64 Linux server (RHEL, Rocky, CentOS, Ubuntu)
|
||
with `exec /sbin/tini: exec format error`. The packaging script uses
|
||
`docker buildx build --platform ...` to avoid that.
|
||
|
||
The default target is `linux/amd64`. Override with `--platform` when your
|
||
remote host is different:
|
||
|
||
```bash
|
||
# x86_64 Linux (the default — Linux RH / Rocky / CentOS / Ubuntu on Intel/AMD)
|
||
./docker/remote-agent/package.sh --platform linux/amd64
|
||
|
||
# ARM Linux (Raspberry Pi 4/5, Ampere servers, etc.)
|
||
./docker/remote-agent/package.sh --platform linux/arm64
|
||
|
||
# For local testing on Apple Silicon
|
||
./docker/remote-agent/package.sh --platform linux/arm64
|
||
```
|
||
|
||
`install.sh` on the remote host also detects `image_arch != host_arch` and
|
||
refuses to start with a clear message pointing at the right rebuild command,
|
||
so a wrong-arch ZIP fails fast instead of after `docker run`.
|
||
|
||
Cross-building requires `docker buildx` — Docker Desktop ships it by
|
||
default; on Linux install the `docker-buildx-plugin` package if it isn't
|
||
already there.
|
||
|
||
### Manual export (without the packaging script)
|
||
|
||
If you'd rather do it by hand:
|
||
|
||
```bash
|
||
# Export from the build host
|
||
docker save sha-remote-agent:latest | gzip > sha-remote-agent.tar.gz
|
||
|
||
# Import on the target host
|
||
gunzip -c sha-remote-agent.tar.gz | docker load
|
||
|
||
# On the target: only .env is needed; no source tree required
|
||
docker run --rm -d \
|
||
--name sha-remote-agent \
|
||
--restart unless-stopped \
|
||
--env-file /path/to/remote-agent.env \
|
||
sha-remote-agent:latest
|
||
```
|
||
|
||
## Networking
|
||
|
||
The agent is a **websocket client** — nothing listens inside the container,
|
||
so there's no port to publish. You just need outbound network access from
|
||
the container to:
|
||
|
||
- The main StoreHealthAnalyzer server (`WS_URL`).
|
||
- Whatever internal APIs the agent proxies for (SIW, MDM, ...).
|
||
|
||
If the internal APIs live only on the container host's network (e.g. a
|
||
private VLAN accessible only from the host), uncomment `network_mode: host`
|
||
in `docker-compose.yml` (Linux only). On Docker Desktop for macOS/Windows,
|
||
prefer running the container on a user-defined bridge network that has route
|
||
access to the required endpoints.
|
||
|
||
## Verifying it works
|
||
|
||
Startup logs from a healthy agent look like:
|
||
|
||
```
|
||
🔄 Connecting to wss://sha.example.com/ws...
|
||
✅ Remote Agent connected to StoreHealthAnalyzer
|
||
```
|
||
|
||
On the main server side you should see a matching `Remote agent connected`
|
||
log line. From then on, `st [number]` commands that need SIW data will
|
||
succeed instead of degrading to the "Remote agent is not connected" banner.
|
||
|
||
## Signals and shutdown
|
||
|
||
The agent handles `SIGTERM` and `SIGINT` explicitly (see
|
||
`remoteAgent.js`), closing the websocket before exiting. Because we run
|
||
`tini` as PID 1, `docker stop` (which sends `SIGTERM` then kills after the
|
||
grace period) reaches Node correctly and the exit is clean.
|
||
|
||
## Rebuilding after code changes
|
||
|
||
Because `remoteAgent.js` is copied in during the runtime stage, changing
|
||
the script requires a rebuild (`--build` with compose, or a fresh
|
||
`docker build`). The `deps` stage is cached whenever `package.json` is
|
||
unchanged, so incremental rebuilds are fast.
|