The operator watches the Live monitor and never sees My calls, so the recording
was reachable only by the colleague who took the call. Now both audiences get it,
by the route each already uses:
operator -> call_recording_url on the lead, via the record view
colleague -> GET /api/agent-sessions/:id/call, authorised by owning the session
Two paths because they are looking at two different things — a lead record and a
conversation — not because the data differs.
Placed ABOVE the summary in the drawer: the summary is a claim about the call and
this is the call, so on a demo about whether the AI captured things fairly the
evidence should not sit below the fold.
preload="none" on both. The drawer opens on every row click, and a WAV of a whole
phone call is not something to fetch speculatively.
tsc -b and vite build clean.
Same treatment as the BA operator console: the audio sits with the conversation
it belongs to, so "did the AI capture what the customer actually said" is
answerable by pressing play rather than by trusting the summary above it.
Reads the new /api/agent-sessions/:id/call, which is scoped to the human who
took the call — the recording lives on the voice session, owned by the telephony
leg, and is unreachable through the ordinary session API.
preload="none" so opening the screen doesn't pull a WAV of a whole phone call
before anyone asked to hear one. A 404 is a normal answer and renders nothing:
recordings are written at call END, so a live call shows its duration and end
reason with a note instead of a dead player.
tsc -b and vite build clean.
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.
Demo is for Mahindra, so the console carries their branding and their cars:
BE 6, XEV 9e and XEV 9S replace the placeholder models, in step with the voice
agent's own brief — a model offered in the dropdown that the agent has never
heard of is a call where she has to improvise.
Brand, persona (Meera) and the demo credentials move into config.ts rather than
being scattered through the pages. Accent colour follows.
Login is simplified: credentials are prefilled and the button takes focus, so
signing in during a demo is one keystroke. Both fields stay editable — this is
a dev demo tenant, not a real account.
useState is explicitly typed <string>: DEMO_LOGIN is `as const`, so inferring
from it narrowed the state to a literal type and setEmail/setPassword refused
any other value.
tsc -b and vite build both clean.
The push that carried the initial commit happened before frontgen.projects had
a row for this repo, so the webhook matched no project and did nothing. The
build only fires on a NEW push.
Two screens over Zino app 583:
Place a call — lead name, model, and the two numbers that matter: the
customer Aria dials, and the colleague the transfer bridges to. Submits the
Add Lead workflow activity as the signed-in user; the workflow's trigger
places the call server-side, so the zvk_ agent key never reaches the browser.
Live monitor — polls the leads record view every 3s. "Transferred to human"
is call_end_reason == handover, called out on its own because it is the thing
the demo exists to show.
src/api.ts deliberately bypasses two SDK methods; both are SDK bugs:
- workflows.startWorkflow() posts workflow_id, but core's StartRequest reads
workflow_uuid and rejects the call.
- views.getTabularView() does GET /view/recordview, the legacy unscoped route;
the app-scoped one is POST /app/{id}/view/recordview with the query in the
body.
Auth still goes through the SDK, so there is one token and one session.
Base URL is the root, not an app path: login is /usr/login while the app APIs
are under /app/583, so they sit at different depths.
Frontgen scaffold generated by the service's own ScaffoldReactProject, so the
build pipeline conventions (relative base, BASE_HREF placeholder, runtime
config.js) are byte-identical rather than reimplemented.
vite build + tsc -b both clean.