# Web Responder & Click-to-Call — Technical Reference

> For day-to-day operations and the authoritative deploy process, see
> `docs/webresponder-runbook.md`. This doc is the technical reference for the
> two features and their NS integration.

## What It Does

A Flask app serving two features, both driven by per-config entries in
`domains.json` keyed by a free-form **`id`** (`<ns-domain>-<suffix>`):

1. **SMS Responder** — NS calls the `/webresponder` webhook (via a dial rule);
   the app texts the caller hours/directions and returns confirmation XML.
2. **Click-to-Call** — `/widget.js` serves an embeddable button; `/click-to-call`
   originates a queued callback via the NS legacy API.

A Zoho form per feature posts to `/update-sms` or `/update-domain` to upsert
config. **`/update-sms` also auto-provisions** the NS routing objects.

---

## Server

| Item | Value |
|---|---|
| Host | `ns-scripts.pressone.net` (hostname `ponjsecdev1`) |
| SSH | `root@ns-scripts.pressone.net` (password in 1Password) |
| Deploy path | `/opt/ns-scripts/webresponder/` |
| App | Flask via gunicorn (`-w 2 -b 127.0.0.1:5100`), systemd unit `ns-webresponder` |
| Reverse proxy | **Caddy** (`server-config/Caddyfile` → symlinked `/etc/caddy/Caddyfile`) |
| Static server | nginx Docker container on `:8081` |
| Public URL | `https://ns-scripts.pressone.net` |

---

## Files in This Repo

| File | Purpose |
|---|---|
| `webresponder/app.py` | Flask app — all routes + NS provisioning helpers |
| `webresponder/domains.json` | Per-config data (**gitignored** on server; template `domains.example.json`) |
| `webresponder/tests/` | pytest suite (~66 tests) |
| `webresponder/ns-webresponder.service` | systemd unit |
| `server-config/Caddyfile` | reverse-proxy routing |

---

## Routes

| Route | Method | Looks up by | Notes |
|---|---|---|---|
| `/webresponder` | GET/POST | `?id=` | SMS webhook; returns confirmation XML |
| `/click-to-call` | POST | `id` in JSON | rate-limited; validates US phone |
| `/widget.js` | GET | `?id=` | branded button JS |
| `/update-domain` | POST | `id` form field | **Click-to-call** Zoho form |
| `/update-sms` | POST | `id` form field | **SMS** Zoho form + auto-provisioning |

All app routes must be added to `server-config/Caddyfile` or they 404 (fall
through to the static server).

---

## `domains.json` Schema

```json
{
  "pressone.net-sms": {
    "api_domain": "pressone.net",
    "user": "2000",
    "ns_system_user": "8001",
    "from_number": "19294873999",
    "message": "Our hours are Mon-Fri 9am-5pm. We are at 123 Main St, Tampa FL."
  },
  "pressone.net-sales": {
    "api_domain": "pressone.net",
    "queue": "9002@pressone.net",
    "branding": { "button_text": "Call Us Now", "button_color": "#1a73e8", "company_name": "PressONE", "logo_url": "" }
  }
}
```

| Field | Feature | What it is |
|---|---|---|
| key (`id`) | both | `<ns-domain>-<suffix>`; used in webhook/widget URLs |
| `api_domain` | both | the real NS domain |
| `user` | SMS | extension that **owns the SMS DID** (sends the text) |
| `ns_system_user` | SMS | routing Simple User auto-created by provisioning (≠ `user`) |
| `from_number` | SMS | SMS DID, digits only |
| `message` | SMS | SMS body |
| `queue` | C2C | call queue, **`EXT@DOMAIN`** format |
| `branding` | C2C | button text/color/company/logo |

**No restart needed** when `domains.json` changes — re-read on every request.

---

## NS API Details

| Item | Value |
|---|---|
| API server | `ucportal.pressone.net` |
| Auth | Bearer `NS_TOKEN` in `/opt/ns-scripts/webresponder/.env` |
| SMS send (v2) | `POST .../v2/domains/{api_domain}/users/{user}/messagesessions/{uuid}/messages` |
| Click-to-call (legacy) | `POST .../ns-api/?object=queuedcall&action=create` (multipart; returns 202) |
| SMS inventory | `GET .../v2/domains/{domain}/smsnumbers` (find `user` via `dest`) |

### SMS Auto-Provisioning (`/update-sms`)

After saving config, `provision_sms_responder()` runs three **idempotent**
(409 / "already exists" → success), **best-effort** steps and returns a
per-step status (`{"user":..,"dial_rule":..,"answer_rule":..}`). Config is saved
regardless; partial failures still return 200.

1. **Create system user** — `POST .../v2/domains/{api_domain}/users` —
   Simple User `{ns_system_user}`, `service-code=system-webresponder`,
   `email-address=operations@pressone.net` (hard-coded).
2. **Create dial rule** — `POST .../v2/domains/{api_domain}/dialplans/{api_domain}/dialrules`
   (note the **separate `{dialplan}` segment** — plan named after the domain),
   application **`To Web`**, matches `webresponder{ns_system_user}`,
   `dial-rule-parameter=https://ns-scripts.pressone.net/webresponder?id={id}`.
3. **Set answer rule** — `PUT .../v2/domains/{api_domain}/users/{ns_system_user}/answerrules/*`
   forward-always → `webresponder{ns_system_user}`; falls back to
   `POST .../answerrules` if the `*` timeframe doesn't exist yet.

Routing chain: `call → {ns_system_user}@{api_domain} → answer rule
forward-always → webresponder{ns_system_user} → dial rule (To Web) → webhook
GET ?id={id} → SMS sent`.

---

## What NS Sends to the SMS Webhook

| Param | Example | Used? |
|---|---|---|
| `NmsAni` | `+19734321440` | ✅ SMS destination (`+` stripped) |
| `id` (query) | `pressone.net-sms` | ✅ config lookup key |
| `Digits` / `OrigCallID` / `NmsDnis` | … | logged only |

### XML Responses

**Success:** `<Response><Say>We've texted you our hours and directions…</Say><Hangup/></Response>`
**Error:** `<Response><Say>Sorry, we could not send directions at this time.</Say><Hangup/></Response>`

---

## Environment Variables (`.env` on server, gitignored)

| Var | Purpose |
|---|---|
| `NS_TOKEN` | NS Bearer token (SMS + queuedcall + provisioning). App won't start without it. |
| `UPDATE_SECRET` | shared secret the Zoho forms send to `/update-sms` and `/update-domain`. Required. |

---

## Caddy Config

`server-config/Caddyfile` (symlinked to `/etc/caddy/Caddyfile`). The
`ns-scripts.pressone.net` block forwards app routes to `:5100`, everything else
to the static container on `:8081`:

```
ns-scripts.pressone.net {
    handle /webresponder*   { reverse_proxy localhost:5100 }
    handle /widget.js*      { reverse_proxy localhost:5100 }
    handle /click-to-call*  { reverse_proxy localhost:5100 }
    handle /update-domain*  { reverse_proxy localhost:5100 }
    handle /update-sms*     { reverse_proxy localhost:5100 }
    handle                  { reverse_proxy localhost:8081 }
}
```

After editing: `caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy`.

---

## Service Management

```bash
systemctl status ns-webresponder
systemctl restart ns-webresponder        # after app.py or .env changes
journalctl -u ns-webresponder -f
journalctl -u ns-webresponder -n 50 --no-pager
```

---

## Running Tests Locally

```bash
cd /Users/shripald/ClaudProjects/Netsapiens-Scripts/webresponder
venv/bin/pytest -v
```

---

## Deploying a Code Change

Git discipline — **edit local → commit → push → pull on server → restart.**
Never hand-edit files on the server.

```bash
# On the Mac
cd ~/ClaudProjects/Netsapiens-Scripts
cd webresponder && venv/bin/pytest -q && cd ..
git add -A && git commit -m "feat(webresponder): describe change" && git push

# On the server
ssh root@ns-scripts.pressone.net
cd /opt/ns-scripts && git pull
systemctl restart ns-webresponder            # for app.py / .env changes
# caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy   # for Caddyfile changes
journalctl -u ns-webresponder -n 20 --no-pager
```

`domains.json` is gitignored, so pulls don't clobber live config.
