Commit graph

4 commits

Author SHA1 Message Date
59460b849b Fix DECT relay container: install node_modules at /workspace, not under agent
The runtime container failed with ERR_MODULE_NOT_FOUND: axios when
integrations/cisco-dect/client.js tried to load. Root cause is
Node's ESM resolver: it walks UP from the IMPORTING file looking
for node_modules, never sideways into siblings.

Container filesystem before:
  /workspace/dect-relay-agent/node_modules/       <- axios lives here
  /workspace/dect-relay-agent/index.js            <- ok, finds it by walking up
  /workspace/integrations/cisco-dect/client.js    <- walks up to /, never sees axios

Node 20.20 has --experimental-detect-module ON by default, so
client.js is still treated as ESM (starts with `import`), and the
resolver correctly reports "cannot find package 'axios'" rather
than syntax-erroring on the import keyword. But it still can't find
the package — the location is wrong.

Fix: install node_modules at /workspace/ so BOTH the agent AND the
shared modules can find it by walking up.

  /workspace/node_modules/                        <- axios here now
  /workspace/package.json                         <- also here, "type":"module" for all descendants
  /workspace/dect-relay-agent/index.js            <- walks up to /workspace/node_modules ✓
  /workspace/integrations/cisco-dect/client.js    <- walks up to /workspace/node_modules ✓
  /workspace/utils/httpDigestAuth.js              <- same ✓

WORKDIR moves from /workspace/dect-relay-agent to /workspace, and
CMD changes accordingly:
  node --enable-source-maps dect-relay-agent/index.js

Rebuild + reship the bundle with `./dect-relay-agent/bundle.sh` and
`./install.sh` on the DC host — it's an idempotent upgrade.

Verified locally (agent modules import cleanly using the same
directory shape as the container).
2026-07-03 10:15:57 -04:00
4f9ebdb5fb Rework DECT relay bundle to ship a pre-built Docker image
The previous packager (scripts/packageDectRelayAgent.js) shipped a
source-only bundle and expected the DC host to build the image with
`docker compose up --build`. That fails hard in corporate DCs with
TLS-intercepted egress: Alpine's apk fetch of dl-cdn.alpinelinux.org
can't verify the intercepted certificate ("apk: TLS: server
certificate not trusted"), and npm install would fail the same way
if apk had succeeded.

New approach: build the image ONCE on the dev machine (where TLS
works), save it as a gzipped tarball, and ship a ZIP whose install
step is `docker load` + `docker compose up -d`. Zero network calls
inside the DC container, ever.

Bundling (dev-machine):
- dect-relay-agent/bundle.sh: build → docker save → gzip → zip.
  Auto-derives version from package.json, records git sha + dirty
  flag + build date into image labels. Cross-arch friendly
  (--platform=linux/amd64 by default; --platform linux/arm64 for
  ARM DCs). Output: dect-relay-agent-bundle-<YYYYMMDD-HHMMSS>.zip
  at repo root (typically 40-60MB).
- dect-relay-agent/Dockerfile: multi-stage node:20-alpine build.
  No apk add. No runtime npm install. Non-root `node` user (uid
  1000). Node handles SIGTERM natively via index.js handlers, so
  no tini/dumb-init needed. Designed to build from the REPO ROOT
  (not the agent folder) because the agent imports shared modules
  from ../integrations/cisco-dect and ../utils.
- dect-relay-agent/Dockerfile.dockerignore: per-Dockerfile ignore
  (BuildKit ≥ 23.0) with a whitelist that keeps the build context
  to ~50KB. Older Docker daemons fall through to the repo-root
  .dockerignore, which already excludes secrets — nothing sensitive
  can leak either way.
- package.json: `npm run package:relay` now invokes bundle.sh.

Runtime (DC-host):
- dect-relay-agent/docker-compose.yml: pins IMAGE_TAG from .env
  (install.sh writes it there — never falls back to :latest), reads
  the rest of the config via env_file, restart: unless-stopped,
  host networking (needed to reach 10.x/8 without userland proxy
  translation, and the agent doesn't listen on anything). Hardened:
  read_only: true rootfs with a 16MB /tmp tmpfs, cap_drop: ALL,
  no-new-privileges, log rotation at 10MB × 5 files.
- dect-relay-agent/install.sh: preflight (docker + compose present,
  daemon reachable, bundle files intact), docker load, pin loaded
  tag into .env, validate .env has the three required values not
  still set to placeholder strings, docker compose up -d, tail last
  40 log lines. Idempotent — safe to re-run on upgrades.

Cleanup:
- scripts/packageDectRelayAgent.js: deleted (superseded).
- .gitignore: drops the scripts/* + !packageDectRelayAgent.js dance
  since we no longer need to whitelist that one file; add pattern
  for the datestamped bundle zips + staging dirs at repo root.
- dect-relay-agent/README.md: replaces the deploy section with the
  new dev-machine-build → DC-host-load workflow, plus a
  troubleshooting section keyed on the exact error messages seen
  during the failed in-DC build (TLS cert not trusted, docker perm
  denied, DIGEST_401).

Verified: all 113 existing tests still pass. Docker build itself
requires a Docker daemon (dev machine) so can't be exercised in
this sandbox — the bash scripts pass `bash -n` syntax checks.
2026-07-03 10:05:05 -04:00
e8500b4324 Package dect-relay-agent as a Docker deploy bundle
Adds a one-command packager (`npm run package:relay`) that produces a
self-contained zip ready to transfer into the data center and start
with `docker compose up -d --build`. Three commands on the DC host:
unzip, edit .env, docker compose up.

Why a packager instead of `docker build` in the repo:
The agent's index.js imports the shared cisco-dect + httpDigestAuth
modules via `../integrations/...` paths, so a naive
`docker build dect-relay-agent/` would fail because those files live
outside the build context. The packager copies them into a
`workspace/` tree inside the bundle so the Dockerfile sees them as
local paths without any source rewriting.

Docker artifacts (in dect-relay-agent/):
- Dockerfile: multi-stage node:20-alpine build (~55MB final image),
  non-root `dect` user (UID/GID 1500), tini as PID 1 for clean
  SIGTERM propagation to node's graceful-shutdown path,
  `npm install --omit=dev --ignore-scripts` in the deps stage.
- docker-compose.yml: restart:unless-stopped, JSON log rotation
  (10MB × 5 files), pgrep-based health check. No `ports:` block
  because the agent is outbound-only (dials the bot).
- .dockerignore: defensive — the bundle already excludes cruft, but
  this hardens against a stray manual build.

Packager (scripts/packageDectRelayAgent.js):
- Assembles agent code + shared modules + deploy artifacts into a
  timestamped staging dir (.package-relay-tmp/, git-ignored).
- Generates a bundle README with three-command deploy instructions,
  ongoing-ops table, no-internet-DC fallback (docker save/load), and
  troubleshooting for the most common failure modes.
- Generates BUNDLE_INFO.txt with build metadata (git sha + dirty
  flag + timestamp + size) so the DC operator can trace deployed
  bundles back to source.
- Emits `dist/dect-relay-agent-bundle-<YYYYMMDD-HHMMSS>.zip` (30KB).
- Cleans staging in a finally block so failed runs don't leak.

Bundle layout (matches Dockerfile expectations):
  dect-relay-agent-bundle-<version>/
    Dockerfile, docker-compose.yml, .dockerignore
    .env.example, README.md, BUNDLE_INFO.txt
    workspace/dect-relay-agent/{package.json, index.js}
    workspace/integrations/cisco-dect/{client,probes,statusXml}.js
    workspace/utils/httpDigestAuth.js

Wiring:
- package.json: new `package:relay` and `test` npm scripts.
- .gitignore: `scripts/` changed to `scripts/*` so `!scripts/
  packageDectRelayAgent.js` can re-include just the packager
  (git forbids re-including files under a fully-excluded directory,
  hence the glob form).
- dect-relay-agent/README.md: rewrites deployment section to show
  the Docker path as the recommended production route, with the
  node-directly path kept for local dev.

Verified end-to-end: `npm run package:relay` produces a valid zip
that unpacks to the expected layout in <2s. All 113 existing tests
still pass.
2026-07-03 09:32:26 -04:00
96b26a5aca DECT relay Phase 1: WSS hub + agent + /phonestatus follow-up
The bot runs in the public cloud and can't reach the 10.x/8 network
where DBS-210 bases live. This phase adds a data-center-resident relay
agent that dials outbound over WSS to the bot, and lets /phonestatus
post a follow-up message with per-base health after its main output
has already shipped.

Bot side (services/):
- dectRelayHub.js: WebSocket upgrade handler on /dect-relay/ws with
  bearer-token auth (constant-time compare, header + Sec-WebSocket-
  Protocol fallback for header-stripping proxies). Promise-based RPC
  API with per-call timeouts, mid-flight-disconnect rejection, and
  clean replacement of a stale agent socket when a newer one connects.
- dectDiscovery.js: pure filter that turns a phoneService result into
  a list of reachable bases. Enforces the "must be on 10.0.0.0/8"
  guardrail per requirements, dedups by IP + MAC, prefers Meraki-live
  IP over Webex-cached IP.
- dectCollectorService.js: fan-out layer over the hub. collectAll()
  runs one RPC per base in parallel with per-base error isolation —
  one bad base never fails the batch.

Phone-status integration:
- Renderer gets a dectFollowUpBaseCount opt that emits an italic
  "diagnostics loading for N base(s)..." hint inside the DECT section
  of the main message.
- New exported renderDectDiagnosticsMarkdown() renders the follow-up
  message: healthy/warning icon per base, uptime + firmware summary,
  structured Power Loss reboot line, and per-base failure hints (e.g.
  "relay accepted the request but the base did not respond in time").
- commands/phoneStatus.js discovers reachable bases synchronously
  (pure), sends the main message, then fires collectAll() and posts
  the follow-up as a separate message. Failures logged, never thrown
  back to the user.
- Chat only: HTTP callers keep their single-message contract.

Agent side (dect-relay-agent/):
- Standalone Node process with its own package.json (only ws, axios,
  dotenv). Reuses the shared integrations/cisco-dect/{client,probes,
  statusXml}.js modules from the parent workspace so there's no code
  duplication.
- Auto-reconnect with exponential backoff + jitter.
- Dispatches collect / reboot / force-reboot / reboot-chain /
  force-reboot-chain / factory-reset / reconfigure-tree.
- DECT admin credentials live ONLY on the agent (never on the bot).
  Shared bearer token gates the WSS handshake.
- README.md covers install, config, wire protocol, and safety model.

Env / infra:
- .env.example: adds DECT_RELAY_AGENT_TOKEN + optional DECT_RELAY_PATH
  and DECT_COLLECT_TIMEOUT_MS. Reframes DECT_TEST_* as the local-dev
  test harness rather than the production path.
- index.js: captures the http.Server from app.listen() and attaches
  the relay hub when DECT_RELAY_AGENT_TOKEN is set; graceful shutdown
  now closes the hub so in-flight RPCs get rejected cleanly.
- Adds "ws" to bot dependencies.

Tests (99 -> 113):
- tests/dectDiscovery.test.js: 13 cases covering the 10.x guardrail,
  MAC normalization, IP source preference, dedup, and warning shape.
- tests/dectRelayHub.test.js: 14 integration cases using a real
  ws pair on an ephemeral 127.0.0.1 port — auth (missing / wrong /
  correct via header / correct via protocol fallback), hello frame,
  RPC round-trip with correlation, agent error surfacing, concurrent
  out-of-order replies, timeout, mid-flight disconnect, replacement
  of a stale socket, and execAction routing.
- tests/renderers.test.js: 8 new cases for the DECT-follow-up loading
  hint (plural / singular / off) and the diagnostics renderer (empty,
  healthy, warning, power-loss dedup, active RTP, error hint, footer).
2026-07-02 17:03:32 -04:00