netanalyzer/docker/remote-agent/README.md
Joseph McQueen b3c37bd7df feat: st command suite, Webex phone + Atlas AV integrations, dockerized remote agent
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>
2026-07-06 09:54:41 -04:00

7.4 KiB
Raw Blame History

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 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 6080 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 (depsruntime). 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:

# 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:

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

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

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.

# 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.

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:

# 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:

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.

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:

# 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:

# 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.