Go to file
Yashas 94e5e9520b feat: My calls — the human sees the transcript of the call they took
Third screen, scoped to the signed-in human: the calls Meera transferred to
them, with the handover brief, the live conversation as it happens, and the
co-pilot's quiet suggestions to relay.

The scoping is the server's, not this component's. Each transferred call is a
co-pilot session OWNED BY the human it was routed to, and agents-backend
authorizes every read with `session.user_id == token.sub`. A renderer app-user
token carries the platform user id in `sub`, so someone else's call is a 403 —
no filtering happens in the browser, and none could.

Worth knowing: this talks to agents-backend (/api/agent-sessions) rather than
the app's core/view services, but with the SAME token. agents-backend validates
it against the shared secret and falls back to a DB lookup for user-service
tokens, which omit org_user_id. No new endpoint, no CORS change — it is already
on the console's base URL.

Rendering notes:
  - "[live call] …" lines are speech the AI overheard on the bridged call, from
    both sides; they render as overheard rather than as chat.
  - A lone dash is the co-pilot deliberately staying silent. Dropped, because an
    empty suggestion box reads as a bug.
  - The server only lists sessions with >= 1 user message, and a co-pilot
    session's user messages ARE the overheard lines — so a transfer where the
    listener never attached will not appear here even though the bridge worked.

tsc -b and vite build clean.
2026-08-27 10:51:59 +05:30
src feat: My calls — the human sees the transcript of the call they took 2026-08-27 10:51:59 +05:30
.env.example feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
.gitignore feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
index.html feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
package-lock.json feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
package.json feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
postcss.config.js feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
README.md feat: rebrand to Mahindra, and ground the models in the real line-up 2026-08-26 23:36:19 +05:30
tailwind.config.js feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
tsconfig.app.json feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
tsconfig.json feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
tsconfig.node.json feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30
vite.config.ts feat: EV Sales Desk console — place a transferring call, watch it live 2026-08-26 22:58:25 +05:30

Mahindra EV Sales Desk Console

Two-screen operator console for the live call-transfer demo: place an outbound Mahindra EV sales call, and watch it transfer to a human colleague mid-call.

Backs onto Zino app 583 (org 900000104, dev). Line-up: BE 6, XEV 9e, XEV 9S. Built on the frontgen React scaffold — Vite + Tailwind + React Router + TanStack Query + src/zino-sdk.

The two screens

Place a call — lead name, model, and the two numbers: the customer Meera dials, and the colleague the transfer bridges to. Submitting starts a Lead Outreach workflow instance; its trigger places the call server-side.

Live monitor — polls the leads record view every 3s. Transferred to human is the transfer having fired.

Running locally

npm install
echo 'VITE_ZINO_API_URL=https://dev-apps.getzino.in' >> .env
npm run dev

Sign in with an app user of app 583 (e.g. salesops@mahindra-ev.demo / Mahindra@2026 (prefilled on the login screen)).

VITE_ZINO_API_URL is only read from .env in dev. A production build requires the config.js the server writes at placement time, and throws if it is absent — a promoted artifact must never fall back to the build machine's value and quietly call the wrong backend.

API notes worth keeping

The base URL is the root (https://dev-apps.getzino.in), not an app-scoped path, because login and the app APIs live at different depths:

Call Path
Login POST /usr/login
Start a lead POST /app/583/start
Leads list POST /app/583/view/recordview

src/api.ts bypasses two SDK methods on purpose — both are SDK bugs, not preferences:

  • workflows.startWorkflow() posts workflow_id, but core's StartRequest reads workflow_uuid and rejects the call outright.
  • views.getTabularView() issues GET /view/recordview — the legacy unscoped route. The app-scoped one is POST /app/{id}/view/recordview with the paging/filters in the body.

Auth still goes through the SDK, so there is one token and one session.

Why the browser holds no API key

Screen 1 submits a workflow activity as the signed-in user. The workflow's own trigger then places the call using the agent key held in its server-side config. The zvk_ key never reaches the browser.

The per-call transfer number

handover_number rides on the lead into the call's context, and agents-backend prefers it over anything configured on the agent — the agent's numbers are the fallback for calls that don't name one. Requires an agents-backend carrying the per-call override; without it the field is sent, silently ignored, and the call transfers to the agent's default number instead.

Known gap

The live transcript and the co-pilot's suggestions live in agents-backend, which the app-user token does not reach — the SDK talks to the renderer's workflow/view services. So the monitor shows call status live, but not the turn-by-turn feed. Closing that needs either a new endpoint or the trigger writing transcript lines onto the instance.