# Reception Console (Enswitch / Telviva)

A lightweight, dependency-free Reception Console that talks directly to the
**Enswitch JSON API** — the same API layer behind `portal4.telviva.com`.
It reproduces the core of the legacy Vision console (extension grid with
live status, active calls list, click-to-dial, transfer, hang up) with the
Telviva One visual language.

Files:
- `index.html` — the whole app (HTML/CSS/vanilla JS, no build step).
- `proxy-server.js` — optional local CORS proxy (see below).

## 1. Run it locally

Just open `index.html` in a browser, or serve it:

```bash
python3 -m http.server 8080
# then visit http://localhost:8080
```

You'll land on a login screen. Sign in with your normal `portal4.telviva.com`
username/password. "Advanced connection settings" lets you set the API base
URL (defaults to `https://portal4.telviva.com`) and the extension to place
calls from.

## 2. The CORS problem (read this before it looks "broken")

I built this against Enswitch's **documented, public JSON API**
(`https://integrics.com/enswitch/guides/4.1/en/dev/json/`), specifically:

| Function | Used for |
|---|---|
| `user/login` | Authenticating and getting a short-lived JWT `auth_token` |
| `phones/list/panel` | The extension roster (name, description, DND, forwarding) |
| `calls/active/list` | Live active call legs, refreshed every 3s |
| `calls/make` | Click-to-dial from the reception line |
| `calls/redirect` | Transferring a call leg to another extension/number |
| `calls/hangup` | Ending a call leg |

I don't have network access to actually hit `portal4.telviva.com` from this
environment, so **I haven't been able to test-authenticate against your live
system** — the request/response shapes above come straight from Enswitch's
own reference docs, not from guessing, but your instance may have
customisations (custom fields, role restrictions) worth checking.

The more likely practical blocker: browsers enforce CORS, and most Enswitch
installs don't send `Access-Control-Allow-Origin` headers, so a fetch
straight from the browser to `portal4.telviva.com` will likely fail with a
network/CORS error (the console will surface this in the connection pill
and a toast, rather than fail silently). Two ways to fix that:

- **Ask Telviva/Integrics to enable CORS** for your console's origin on the
  Enswitch installation (this is a per-install web server config).
- **Use the included proxy** — run it locally (or on a server you control)
  and point the console at it instead:
  ```bash
  npm install express node-fetch@2
  PBX_HOST=https://portal4.telviva.com node proxy-server.js
  ```
  Then set "PBX / API base URL" on the login screen to
  `http://localhost:8787/pbx`. The proxy only adds CORS headers and forwards
  requests byte-for-byte — it never stores or logs credentials.

## 3. Changelog

**Fixed:**
- **Duplicate extensions** — some Enswitch installs return more than one row
  per extension from `phones/list/panel` (e.g. a base line plus a
  "logged-in-as virtual phone" row). The grid now dedupes by extension
  number, keeping whichever row has the most fields populated.
- **Department filter + sort** — the toolbar above the grid now has a sort
  dropdown (Name / Extension / Status) with an ascending/descending toggle,
  alongside the existing department chips.
  ⚠️ The department chips are currently a **best-effort guess** — they're
  derived from a `" - "`-split prefix of each phone's `description` field,
  since `phones/list/panel` has no dedicated department field. Telviva
  One's own directory (image 2) shows a real `Department:` value per
  contact, which is more likely coming from a custom field or a separate
  directory service — if you can tell me which one, I'll wire the chips to
  the real source instead of guessing at `description`.
- **Session dropping after a while** — this was the `auth_token` expiring
  (Enswitch tokens are short-lived by design; see `auth_token_expiry` in the
  JSON API docs). The console now re-mints the token ~30s before it expires
  using the credentials you signed in with, and additionally retries once
  with a fresh login on any 401 before giving up — so it should no longer
  boot you back to the login screen on its own. It only asks you to sign in
  again if that retry also fails (e.g. password actually changed) or you
  hit Log out.

## 4. Embedded webphone (`index2.html`, beta)

Confirmed via Sources search: `one.telviva.com` bundles **JsSIP**
(`https://jssip.net`), a standard, documented SIP-over-WebSocket library —
this is not a proprietary protocol. There's also a separate WebSocket at
`wss://one.telviva.com/rendezvous/...` running a `RendezvousClient` — that
one is Telviva's own presence/status sync channel (busy/DND propagation,
chat), not the call signaling channel, so it's not relevant to the webphone
bridge.

`index2.html` is a variant of `index.html` with a real JsSIP `UA` wired in:
registration status pill, a full in-call panel (mute, hold, blind transfer,
hang up), incoming-call answer/reject, and DTMF-ready session handling. The
extension grid and dialer now call through the embedded webphone whenever
it's registered, and fall back to `calls/make` (same as index.html)
otherwise — so it's safe to leave the SIP fields blank and it behaves
exactly like index.html.

**What's still needed: the SIP registrar's WebSocket URL.** Searching
Sources for JsSIP's own API surface (`UA(`, `wsServers`) returns too many
matches because those are library internals bundled among everything else.
Two more targeted approaches:

1. **Regex search for the literal URL.** In Sources → Search (Ctrl+Shift+F
   / Cmd+Opt+F), enable regex mode and search `wss?:\/\/[^"']+`. Minifiers
   rename variables but essentially never rewrite string literals, so this
   returns only real URLs — a short list to eyeball, rather than hundreds
   of code matches. Look for one that isn't `/rendezvous/`.
2. **Network tab, if #1 comes up empty** (the URL may be injected from a
   runtime config rather than baked into the bundle): clear filters, click
   the **Socket** category, enable **Preserve log**, reload, and place a
   test call. A second WS connection distinct from `/rendezvous/...` should
   appear. Its Messages tab will show plain-text SIP (`REGISTER sip:...`,
   `INVITE sip:...`) rather than JSON.

Once you have it, in `index2.html`'s login screen → Advanced settings, fill
in:
- **SIP WebSocket server** — the `wss://...` URL you found
- **SIP domain** — the realm the line registers against (often the same as
  your Enswitch host, e.g. `telviva-42`, but confirm from the `REGISTER`
  line's `To`/`From` header if you captured one)
- **SIP username / extension (registration AOR)** — the full SIP account,
  which is *not* the same as the short internal line used for
  `calls/make`. Keep both fields — "SIP username" and "my line" below it —
  filled in even though they look similar.
- **SIP password** — the line's own SIP credential, *not* your portal
  login. Use the **Fetch** button next to that field to pull it straight
  from Enswitch (`phones/list` with `password=1`) if your portal user has
  admin role — saves hunting for it manually. Note this looks up
  `phones/list` by the *SIP username*, and only falls back to "my line" if
  that lookup finds nothing — the two numbering schemes may not share a
  `phones` row, in which case you'll need to enter the SIP password by
  hand from wherever your handset/softphone provisioning already has it.

**Confirmed working values for this account**, pulled from
`https://one.telviva.com/settings/support-info` (Settings → Support info in
Telviva One — worth checking there directly rather than digging through
DevTools, if you have it):

| Field | Value |
|---|---|
| RTC url (→ SIP WebSocket server) | `wss://rtc6.telviva.com:4433` |
| SIP server (→ domain is the part after `@`) | `sip4.telviva.com` |
| SIP username / extension | `2852294` |
| Enswitch host | `telviva-42` (this is the *portal* host, not the SIP domain — don't mix the two up) |

These are pre-filled as defaults in `index2.html`'s login screen. They're
specific to this account/session, though — `rtc6`/`sip4` look like
load-balanced or region-specific server names, so another user's Support
info page may show a different number.

I haven't been able to test-register against your instance (no network
access from this sandbox), so treat first connection as a debugging pass —
watch the browser console and the SIP status pill for `registrationFailed`
details if it doesn't connect on the first try.

## 5. What's implemented vs. what's a starting point

**Working end-to-end (against the documented API):**
- Username/password login with token-based session (`user/login`, `token=1`)
- Extension grid: name, extension, department chip filter, search
- Status derivation: **On a call** (cross-referenced live against active
  call legs), **DND** (`phones` `dnd` field), **Available**
- Forwarding indicator when `use_forwarding`/`forwarding` are set
- Active calls panel with live duration timer, polling every 3s
- Click-to-call (`calls/make`) using a configurable "my line"
- Transfer flow: click transfer on a call, then click an extension card
  (`calls/redirect`)
- Hang up (`calls/hangup`)

**Deliberately left as extension points** — these need either your specific
field/role setup or a decision from your team, so I didn't guess:
- **Offline / unregistered status** — needs `phones/registrations/list`
  per phone, which is one extra call per extension; worth batching/caching
  rather than polling per-card.
- **"Not answering" status** (seen in your legacy screenshot) — this looks
  like it's derived from recent CDR history rather than live state; would
  use the `cdrs` category.
- **Tags / "Favourite People"** (seen in Telviva One's directory, image 2)
  — that's presentation-layer data, likely stored in a custom field or your
  own directory service rather than core Enswitch phone fields.
- **Real-time push instead of polling** — Enswitch exposes an `events`
  category in the JSON API; if your install supports webhook/event
  subscriptions it would replace the 3–6s polling with instant updates.
- **Auth session persistence** — the token is kept in memory only (cleared
  on refresh) since this build must avoid browser storage APIs; a real
  deployment would likely want a short-lived httpOnly cookie set by a thin
  backend instead of a page-visible token.

## 4. Embedding into Telviva One (`one.telviva.com`)

Two realistic paths, roughly in order of effort:

1. **Iframe embed (fastest):** host `index.html` behind your auth/SSO on
   any internal URL and drop it into Telviva One as an iframe panel/tab.
   Works today with zero changes to Telviva One itself, aside from CORS/CSP
   allow-listing the console's origin.
2. **Native component:** port the same API calls into a React (or
   whatever Telviva One's webphone is built in) component that lives
   alongside the existing webphone, sharing its session/auth and letting
   the console's "call" actions drive the webphone's active SIP line
   directly instead of `calls/make`. This is the better long-term UX (no
   second phone line spun up for the receptionist), but I'd need to see
   how the webphone is structured to do it properly.

If you can share a DOM inspect / component tree of the Telviva One webphone
(or its repo, if I can get read access), I can adapt this into a native
component that calls into the existing SIP session instead of dialling out
via a separate line.

## 5. `index3.html` — extension grid overhaul + transfer fixes

Everything from `index2.html`, plus:

**Bug fix — stale call after blind transfer.** The embedded call info used
to sit on screen indefinitely after a successful blind transfer, because
nothing terminated the local leg — it was waiting on a `BYE` from the far
end that isn't reliably sent. Standard blind-transfer behavior (what a
physical deskphone does) is to drop your own leg as soon as the PBX accepts
the `REFER`, so that's what happens now: the local session terminates on
the refer request's `accepted` event, with a timed fallback if a given
JsSIP build doesn't expose that event.

**New — attended transfer.** Hold the current call → place a consult call
to the target → talk privately → **Complete transfer** (`session.refer(consultSession)`,
JsSIP's documented mechanism for building the `Replaces` header — confirmed
via JsSIP's own issue tracker, not guessed) or **Cancel & return** to go
back to the original call. Available both as buttons in the in-call panel
(arms "click an extension to consult") and directly on each extension
card, which acts on whatever call is currently live in the embedded
webphone.

**Department filter, from real Enswitch data.** Chips are now built from
`people.group2` (falling back to `group1`), joined to each phone via
`phones.owner`. Two things worth knowing:
- The lightweight `phones/list/panel` (used for the live 6s status poll)
  may not include the `owner` field, so `index3.html` separately pulls the
  full `phones/list` once a minute purely to build a phone→owner map, and
  `people/list` once a minute for the person records themselves. If chips
  come up empty, check that your API user's role can see `people/list`.
- I could not find `group1`/`group2` in Enswitch's published `people/update`
  field reference — only your own admin-panel knowledge confirmed the field
  name. If chips don't populate even though `people/list` succeeds, the
  field might be named differently in your version; happy to adjust once
  you can check one raw `people/list` record's shape.

**Feature codes instead of telephone-line numbers.** Cards now try to show
each person's short feature code rather than the full `phones.name`. This
calls `codes/list`, which **is not in Enswitch's public JSON API docs** —
only `codes/callers/list` and `codes/import` are documented. Every other
category follows the same list/get/create/update pattern, so it's a
reasonable bet that it exists, but I couldn't confirm the exact field names
for the code↔phone mapping. It's wired defensively: if the call fails, or
the returned fields don't match what the code guesses (`dnumber`/`target`/
`phone`/`number` for the destination, `dtype`/`type` for the routing type),
cards silently fall back to showing the phone's own line number — nothing
breaks, but the feature codes may not appear until this is verified against
a real response.

**Visual changes:**
- Full card background now uses the pastel status colors (green/red/amber),
  not just the small badge — easier to scan at a glance.
- The call button is a proper SVG icon (green, matching "Available") instead
  of the 📞 emoji — the emoji was rendering with its own built-in red/black
  color regardless of CSS, which is why it looked red before; that wasn't a
  styling choice, just an emoji quirk.
- In-call panel is bigger: Mute/Hold/Blind transfer/Attended transfer as a
  2×2 button grid, plus a full-width "End call" button, instead of small
  icon buttons.
- "Connected — 2852207" now shows the person's name and feature code where
  known (e.g. "Connected — Jane Doe (2207)"), falling back to the raw
  number if no match is found in the directory.

**Removed:** the "Call from (my line)" field. Since the embedded webphone
now handles calling directly using its own registered identity, that field
was redundant — its only remaining job (a fallback `snumber` for
`calls/make` if the webphone isn't registered) is now filled automatically
from the SIP username.

**Added:** pressing Enter in the dial box now places the call.
