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>
118 lines
4.1 KiB
Bash
Executable file
118 lines
4.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# StoreHealthAnalyzer Remote Agent — install / (re)start on a remote host.
|
|
#
|
|
# Run this after extracting the deploy ZIP:
|
|
# unzip sha-remote-agent-<version>.zip
|
|
# cd sha-remote-agent-<version>
|
|
# ./install.sh
|
|
#
|
|
# On first run: verifies the image tarball, loads it into Docker, and drops
|
|
# a starter .env so you can fill in WS_URL / WS_TOKEN. Re-runs are safe —
|
|
# the script is idempotent.
|
|
|
|
set -euo pipefail
|
|
|
|
# Move to the script's own directory so relative paths work regardless of
|
|
# where the user invoked it from.
|
|
cd "$(dirname "$0")"
|
|
|
|
RED=$'\033[0;31m'
|
|
GRN=$'\033[0;32m'
|
|
YLW=$'\033[1;33m'
|
|
RST=$'\033[0m'
|
|
|
|
log() { printf '%s[install]%s %s\n' "$GRN" "$RST" "$*"; }
|
|
warn() { printf '%s[install]%s %s\n' "$YLW" "$RST" "$*"; }
|
|
die() { printf '%s[install]%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; }
|
|
|
|
# --- 1. Preflight ----------------------------------------------------------
|
|
|
|
command -v docker >/dev/null 2>&1 || die "Docker not found on PATH."
|
|
docker info >/dev/null 2>&1 \
|
|
|| die "Cannot talk to the Docker daemon. Is it running / do you have permission?"
|
|
|
|
# Detect either `docker compose` (v2 plugin) or the legacy `docker-compose`.
|
|
if docker compose version >/dev/null 2>&1; then
|
|
COMPOSE=(docker compose)
|
|
elif command -v docker-compose >/dev/null 2>&1; then
|
|
COMPOSE=(docker-compose)
|
|
else
|
|
die "Neither 'docker compose' nor 'docker-compose' is available. Install Docker Compose."
|
|
fi
|
|
|
|
[[ -f VERSION ]] || die "VERSION file missing from bundle — is this a valid deploy ZIP?"
|
|
VERSION="$(cat VERSION)"
|
|
|
|
IMAGE_TARBALL="sha-remote-agent-${VERSION}.tar.gz"
|
|
[[ -f "$IMAGE_TARBALL" ]] || die "Image tarball not found: $IMAGE_TARBALL"
|
|
|
|
# --- 2. Verify checksum (optional; skip gracefully if no shasum tool) -----
|
|
|
|
if [[ -f SHA256SUMS ]]; then
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
log "Verifying SHA256 checksum..."
|
|
sha256sum -c SHA256SUMS >/dev/null || die "Checksum verification FAILED."
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
log "Verifying SHA256 checksum (macOS shasum)..."
|
|
shasum -a 256 -c SHA256SUMS >/dev/null || die "Checksum verification FAILED."
|
|
else
|
|
warn "No sha256sum/shasum tool found — skipping integrity check."
|
|
fi
|
|
log "Checksum OK."
|
|
else
|
|
warn "No SHA256SUMS file in bundle — skipping integrity check."
|
|
fi
|
|
|
|
# --- 3. Load the image -----------------------------------------------------
|
|
|
|
IMAGE_TAG="sha-remote-agent:${VERSION}"
|
|
if docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
|
log "Image $IMAGE_TAG already present — skipping load."
|
|
else
|
|
log "Loading Docker image from $IMAGE_TARBALL..."
|
|
docker load -i "$IMAGE_TARBALL"
|
|
fi
|
|
|
|
# --- 3a. Platform sanity check --------------------------------------------
|
|
# If the image was built for a different CPU architecture than this host,
|
|
# Docker will let it "run" but tini (and node) fail with cryptic errors
|
|
# like "exec format error". Catch that up front with a clear message.
|
|
|
|
IMAGE_ARCH="$(docker image inspect --format '{{.Architecture}}' "$IMAGE_TAG" 2>/dev/null || true)"
|
|
HOST_ARCH_RAW="$(uname -m)"
|
|
case "$HOST_ARCH_RAW" in
|
|
x86_64|amd64) HOST_ARCH="amd64" ;;
|
|
aarch64|arm64) HOST_ARCH="arm64" ;;
|
|
armv7l) HOST_ARCH="arm" ;;
|
|
*) HOST_ARCH="$HOST_ARCH_RAW" ;;
|
|
esac
|
|
|
|
if [[ -n "$IMAGE_ARCH" && "$IMAGE_ARCH" != "$HOST_ARCH" ]]; then
|
|
warn "Image architecture ($IMAGE_ARCH) does not match this host ($HOST_ARCH)."
|
|
warn "The container will fail to start with 'exec format error'."
|
|
die "Rebuild on the dev host with: ./docker/remote-agent/package.sh --platform linux/${HOST_ARCH}"
|
|
fi
|
|
|
|
# --- 4. Bootstrap .env -----------------------------------------------------
|
|
|
|
if [[ ! -f .env ]]; then
|
|
if [[ -f .env.example ]]; then
|
|
cp .env.example .env
|
|
warn ".env did not exist — copied .env.example into place."
|
|
warn "EDIT .env now to set WS_URL and WS_TOKEN, then re-run this script."
|
|
exit 0
|
|
else
|
|
die ".env is missing and no .env.example is bundled. Cannot proceed."
|
|
fi
|
|
fi
|
|
|
|
# --- 5. Start the container -----------------------------------------------
|
|
|
|
log "Starting sha-remote-agent (version ${VERSION})..."
|
|
"${COMPOSE[@]}" up -d
|
|
|
|
log "Done. Tail logs with:"
|
|
log " ${COMPOSE[*]} logs -f"
|
|
log "Stop with:"
|
|
log " ${COMPOSE[*]} down"
|