netanalyzer/docker/remote-agent/deploy/install.sh
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

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"