# Deploying BizGaze Support

The app runs as a Docker container behind the existing **Nginx Proxy Manager**,
which terminates TLS and proxies `https://remote.bizgaze.com` → `bizgaze-support:8090`
on the shared `nginx_proxy_manager_default` network. No host ports are published.

Deployment model: **the server holds a git clone of this repo.** Each deploy is a
`git pull` + rebuild via [`deploy.sh`](deploy.sh). Two files are *not* in git and
live only on the server — they survive every pull:

| File | Purpose |
|------|---------|
| `.env` | Secrets — TURN credentials, optional `SSO_SECRET`, `BIZGAZE_WEBHOOK_URL`. See [.env.example](.env.example). |
| `server/cert.pem`, `server/key.pem` | Self-signed cert for the app's *optional* direct-HTTPS listener (8443). Not needed behind NPM, but harmless. |

Server facts:
- Host: `root@118.95.33.89` port `61`
- App path: `/opt/bizgaze-support`
- Data: Docker named volume `bizgaze_support_data` → `/data/data.db` (persists across rebuilds)
- Backups: `/opt/bizgaze-support.backups/` (newest 3 `.tgz` snapshots, auto-rotated)

---

## Operational guardrails (read before every deploy)

These are correctness/security invariants, not preferences. Breaking one degrades
or breaks the app even if the container starts fine.

- **Single instance only.** Chat, presence, and meeting (WebRTC) signaling use an
  **in-process** registry. Do **not** scale to multiple replicas or place several
  instances behind a round-robin load balancer — users on different processes
  can't see each other's messages/calls. One container, one process.
- **`ALLOW_LOCAL_LOGIN` must NOT be set in production.** It's a dev-only escape
  hatch that bypasses BizGaze SSO and the local-password lockout. Production logs
  in via BizGaze only.
- **`BIZGAZE_DIRECTORY_TOKEN` is server-side only** — it's used by the server to
  proxy directory lookups and must never be exposed to the browser/client.
- **HTML is served `Cache-Control: no-store` by design** so new builds land
  immediately. Do not add an HTTP/CDN cache layer that caches `.html`. Static JS
  (`icons.js`) is cache-busted with a `?v=` query, currently `?v=4`.
- **Node ≥ 22.5** (the image uses `node:24-alpine`) — required for the built-in
  `node:sqlite` that `db.js` relies on. `deploy.sh` rebuilds the image, so
  `npm install` (incl. `web-push`) happens automatically; no manual install.
- **No DB migration is required** for routine UI/chat releases. The `data.db`
  volume persists across rebuilds; schema changes (when present) auto-apply on boot.

### Env vars to confirm in `.env`
`.env` lives only on the server (never in git) and must contain, beyond the TURN
secrets already documented:

| Group | Vars | Needed for |
|-------|------|-----------|
| Login / SSO | `BIZGAZE_LOGIN_URL`, `BIZGAZE_DIRECTORY_URL`, `BIZGAZE_DIRECTORY_TOKEN`, `SSO_SECRET` | BizGaze sign-in + directory search |
| Web Push | `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, `VAPID_SUBJECT` | Background push for browsers / installed PWA |
| Native push — Android | `FCM_SERVICE_ACCOUNT` (path to / inline Firebase service-account JSON) | FCM push to the Android app |
| Native push — iOS | `APNS_KEY` (path/inline `.p8`), `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_BUNDLE_ID`, `APNS_PRODUCTION=1` | APNs push to the iOS app |
| Calls | `TURN_URLS` / `TURN_SECRET` (or `TURN_USERNAME`+`TURN_CREDENTIAL`) | Audio/video across NATs & mobile networks |

If the VAPID keys are missing, push silently no-ops (the app still runs). Push on
iOS additionally requires the user to **Add to Home Screen** (iOS 16.4+) — an
end-user step, not ops.

### Per-release verification
After deploy, open the app and check the browser console logs the expected build,
e.g. `Biz Connect build 2026-06-30-batch14`. That confirms the new HTML is being
served (not a stale cache).

---

## One-time bootstrap (server → git clone)

Run **once** to convert the existing folder into a git checkout without losing the
secrets. Prerequisite: the deployment files (Dockerfile, docker-compose.yml,
deploy.sh, etc.) are committed and pushed to `origin/master` first.

```bash
ssh -p 61 root@118.95.33.89          # or: plink -ssh -P 61 -pw '<pw>' root@118.95.33.89

# 1. Store git credentials so pulls are non-interactive (once per server).
git config --global credential.helper store
printf 'https://devops%%40bizgaze.com:Bizgaze%%40123@code.bizgaze.com\n' > ~/.git-credentials
chmod 600 ~/.git-credentials

# 2. Stash the live secrets.
cd /opt
cp -a bizgaze-support /opt/bizgaze-support.preclone.bak
mkdir -p /tmp/bzsecrets
cp bizgaze-support/.env                /tmp/bzsecrets/ 2>/dev/null || true
cp bizgaze-support/server/cert.pem     /tmp/bzsecrets/ 2>/dev/null || true
cp bizgaze-support/server/key.pem      /tmp/bzsecrets/ 2>/dev/null || true

# 3. Replace the folder with a fresh clone.
rm -rf bizgaze-support
git clone https://code.bizgaze.com/Sravan/BizGaze_Remote.git bizgaze-support
cd bizgaze-support

# 4. Restore the secrets the clone doesn't carry.
cp /tmp/bzsecrets/.env       ./.env
cp /tmp/bzsecrets/cert.pem   ./server/cert.pem 2>/dev/null || true
cp /tmp/bzsecrets/key.pem    ./server/key.pem  2>/dev/null || true
rm -rf /tmp/bzsecrets

# 5. Build & launch.
chmod +x deploy.sh
docker compose up -d --build
docker compose ps
```

If `.env` did not exist yet, create it from the template and fill in the TURN secret:

```bash
cp .env.example .env && nano .env
```

---

## Routine deploy

After pushing changes to `origin/master`:

```bash
ssh -p 61 root@118.95.33.89 'cd /opt/bizgaze-support && ./deploy.sh'
```

`deploy.sh` snapshots the current tree (keeping 3 backups), `git reset --hard`s to
`origin/master`, rebuilds, and verifies `/api/ice`. Flags:

- `./deploy.sh --no-pull` — rebuild the current checkout without pulling
- `./deploy.sh --rollback` — restore the newest backup snapshot and rebuild

---

## Desktop app releases (make the installer live + auto-update)

Web/UI changes reach the desktop app instantly (it loads the live site). Only a change to the
**native shell** (`desktop/`) needs a new installer. Publishing one both powers the site's
"Download for Windows" button and pushes an auto-update to already-installed apps.

The installer feed is served from `DOWNLOADS_DIR`, which docker-compose now points at
`/data/downloads` (the persistent volume) so uploads survive `deploy.sh` rebuilds. **First time
only**, redeploy once after pulling so the container picks up `DOWNLOADS_DIR`, then create the dir:

```bash
ssh -p 61 root@118.95.33.89 'docker exec bizgaze-support mkdir -p /data/downloads'
```

**Each desktop release:**

1. Build on a Windows machine (needs `desktop/build/icon.ico`; bump `desktop/package.json`
   `version` first):
   ```bash
   cd desktop && npm install && npm run dist
   ```
   Output in `desktop/dist/`: `Biz Connect Setup <ver>.exe`, `…​.exe.blockmap`, `latest.yml`.
2. Upload those **three** files into the container's `/data/downloads` (all three are required —
   `latest.yml` is the update manifest, `.blockmap` enables differential updates). `/data` is a
   **named Docker volume** (`bizgaze_support_data`), not a host bind-mount, so its real host path
   is `/var/lib/docker/volumes/bizgaze_support_data/_data`. scp straight into it — one step, no
   restart needed (the server serves the folder live):
   ```powershell
   # from the repo's desktop\dist folder on the Windows build machine
   scp -P 61 "Biz Connect Setup <ver>.exe" "Biz Connect Setup <ver>.exe.blockmap" latest.yml `
     root@118.95.33.89:/var/lib/docker/volumes/bizgaze_support_data/_data/downloads/
   ```
   Alternatively, scp to `/tmp` on the server and `docker cp` in (avoids touching the volume path):
   ```bash
   docker cp "/tmp/Biz Connect Setup <ver>.exe"          bizgaze-support:/data/downloads/
   docker cp "/tmp/Biz Connect Setup <ver>.exe.blockmap" bizgaze-support:/data/downloads/
   docker cp "/tmp/latest.yml"                           bizgaze-support:/data/downloads/
   ```
   Keep both versions' `.exe`/`.blockmap` on the server (older blockmaps let installed apps pull
   deltas); only `latest.yml` is overwritten — there must be exactly one, pointing at the newest.
3. Verify:
   ```bash
   curl -I https://remote.bizgaze.com/download/windows      # 302 → the new .exe
   curl    https://remote.bizgaze.com/downloads/latest.yml  # shows version <ver>
   ```

Installed apps check the feed on launch and every 6h, download in the background, and update on
next restart. Keep the `.exe` + `.blockmap` that `latest.yml` references on the server; older
versions can be pruned. Note: the installer is **not code-signed**, so Windows SmartScreen shows
an "unknown publisher" warning — supply an EV/OV code-signing cert to remove it (see
`desktop/PACKAGING.md`).

---

## Meetings SFU (LiveKit) — optional, scales meetings past ~5 people

By default meetings use a **P2P mesh** (each person sends video to every other person), which
degrades past ~5 participants. Enabling **LiveKit** routes media through an SFU so each person
uploads once — rooms scale to 20-50+. It's **fully optional and config-gated**: until you set the
three `LIVEKIT_*` vars, the app keeps using the mesh, unchanged. The `livekit` service is already
in `docker-compose.yml`; these steps turn it on.

**1. Generate an API key + secret** (any two random strings; keep them secret):
```bash
echo "LIVEKIT_API_KEY=$(openssl rand -hex 8)"
echo "LIVEKIT_API_SECRET=$(openssl rand -hex 24)"
```
Add those two lines to the server's `.env`, plus the public signaling URL:
```
LIVEKIT_URL=wss://livekit.bizgaze.com
LIVEKIT_API_KEY=<from above>
LIVEKIT_API_SECRET=<from above>
```
The app mints join tokens with the secret (server-side only); the same key/secret reach the
`livekit` container via `LIVEKIT_KEYS` (docker-compose reads them from this same `.env`).

**2. DNS**: point `livekit.bizgaze.com` (A record) at the server — `118.95.33.89`.

**3. NPM proxy host** for the signaling WebSocket (LiveKit media does NOT go through NPM):
- Domain `livekit.bizgaze.com` → **Forward to** `livekit:7880` (scheme `http`).
- **Websockets Support: ON**. Request an SSL cert (Let's Encrypt) + Force SSL.
- NPM reaches `livekit:7880` by container name — both are on `nginx_proxy_manager_default`.

**4. Media ports — NAT port-forward (REQUIRED here).** This box sits **behind NAT**: its only
interface is a private `192.168.88.61`; the public `118.95.33.89` (DNS) is mapped by an upstream
gateway. WebRTC media can't traverse NPM (L7), so the gateway/router must forward the media ports
to the box. To keep the ask minimal, LiveKit is configured for **one** UDP port + one TCP fallback:

Ask whoever controls the network/gateway to forward, from `118.95.33.89` → `192.168.88.61`:
| Port | Proto | Purpose |
|------|-------|---------|
| 50000 | UDP | WebRTC media (all participants mux over this one port) |
| 7881  | TCP | WebRTC-over-TCP fallback (restrictive client networks) |

`livekit.yaml` already pins `rtc.node_ip: 118.95.33.89` (auto-detect would pick the wrong outbound
IP behind this NAT). The host's local `ufw` is inactive, so no host-firewall change is needed — the
only requirement is the upstream port-forward above. Until it exists, signaling connects but media
won't flow (participants see each other's tiles but no video/audio).

**5. Deploy** (the livekit service is behind a `sfu` compose profile, so it stays dormant on a
normal deploy — start it explicitly):
```bash
cd /opt/bizgaze-support && ./deploy.sh                 # rebuilds/starts the app as usual
docker compose --profile sfu up -d                     # additionally starts the livekit container
docker compose ps                                      # expect both bizgaze-support AND bizgaze-livekit "Up"
```

**6. Verify**:
```bash
curl https://remote.bizgaze.com/api/meetings/config     # expect {"sfu":true,"url":"wss://livekit.bizgaze.com"}
curl -I https://livekit.bizgaze.com                      # 200/426 (WS endpoint reachable via NPM+TLS)
docker logs bizgaze-livekit --tail 30                    # "starting LiveKit server", no key errors
```
Then start a meeting in the app and confirm 3+ participants see each other. To roll back to mesh,
just remove the `LIVEKIT_*` vars from `.env` and redeploy — no code change.

---

## Verify

```bash
curl https://remote.bizgaze.com/api/ice
```

Response must list the public STUN entry **and** a TURN entry at
`global.relay.metered.ca`. If only STUN appears, `.env` isn't reaching the
container — check `docker exec bizgaze-support env | grep TURN`.

---

## Rollback

```bash
cd /opt/bizgaze-support && ./deploy.sh --rollback   # newest snapshot
# or restore a specific snapshot:
ls -1t /opt/bizgaze-support.backups/*.tgz
tar -xzf /opt/bizgaze-support.backups/<stamp>.tgz -C /opt/bizgaze-support && docker compose up -d --build
```

The `data.db` volume is never overwritten by a rebuild, so data is retained
regardless of code version.
