The operator model: certifications, levels and presence
The screen loads the signed-in operator withapi.me(). The operator record it
renders has these fields:
Certifications are per skill, with a level. The screen renders each one as a
skill · level chip and states the matching rule directly to the operator: a
job offer only reaches you if you hold its required cert at level.
Until api.me() resolves, the screen renders an empty operator
(id: '', no certs, presence: 'ready'). No presence is broadcast while
ME.id is empty — the broadcast effect returns early.
Ready vs Aux and what each broadcasts
Presence is local UI state with two values, toggled by the two buttons in the screen header:- Ready — the operator is in the dispatchable pool.
- Aux — the operator is out of the pool, with a reason selected from a
dropdown:
Break,Training,Debrief,Meeting.
availableis the only presence signal. Aux is published asavailable: false, not as a separate state.certsis flattened to skill names only. The level is not in the presence payload, so a dispatcher reading the pool sees which skills an operator holds, not at what level.- The selected Aux reason (
Break,Training, …) is not part of the payload. It is displayed locally on this screen only.
paused — you are in Aux instead of listening, and every row’s Attach
button is disabled.
How a job is matched: certification filter, then lowest RTT
Matching happens off this screen. The Ready screen’s job is to advertise the operator into the pool and to render whatever offer arrives. The order is:- Certification filter. Only operators holding the job’s required cert are candidates. The screen states this to the operator as the reason an offer would or would not reach them.
- Lowest RTT. The dispatching ops console reads the presence pool of
available operators and runs
claim_lowest_rttover it, so the candidate with the lowest round-trip time to that robot wins.
operatorId, and the ring only belongs to an operator when
offer.operatorId equals their own id.
In the current screen, the offer handler presents any offer that arrives on
the channel rather than comparing
offer.operatorId to ME.id — the demo
channel is shared between tabs, and per-operator targeting on the client is
noted in the source as a refinement. Server-side, the ACD has already picked
the operator before it rings.The ring offer: fields, accept and decline
An offer opens a modal over the whole screen. The offer object exposes:
Accept acknowledges the offer and moves the operator straight into the
cockpit on the assigned session:
Where the session id comes from and how it reaches the cockpit
The operator console never mints a session id. The id is created by the match that produced the offer — aPOST /v1/sessions match on the engine rings this
console — and arrives as offer.sessionId.
From there it travels by URL. Accepting navigates to
/cockpit?robot=<robotId>&session=<sessionId>, both URL-encoded, so the cockpit
attaches to the session the platform assigned rather than opening a new one.
The Attach button on an Available-jobs row is a different path: it navigates
to /cockpit?robot=<robot> with no session parameter. Only the accepted
ring offer carries an assigned session id into the cockpit.
The two ring transports
RingChannel is created once per screen (held in a ref) and carries both
presence and offers. The screen wires two delivery paths into the same modal:
- BroadcastChannel. Works cross-tab with no engine running. This is the path used for local and demo flows.
- Engine realtime.
ring.connectRealtime(tenant, url)subscribes to the engine’smod_realtimechannelprivate-cti-<orgId>, so a real match on the engine rings this console.
VITE_TELEOP_REALTIME_URL, falling back to the
platform’s default relay host.
The tenant argument is the org id from getOrgId() — the same id every
other teleop call sends — and not a build-time constant. The BFF’s teleopSign
signs a private-cti-<orgId> subscribe only for an org the caller is a member
of, and a compile-time constant cannot be checked against a membership.
On unmount, the screen calls ring.close().
The available jobs queue
api.availableJobs() backs the queue table. Each row exposes:
The queue is empty by default. An empty queue renders an explicit message rather
than bare headers, because headers with nothing under them read as “stuck
loading”: no robots need an operator right now, and offers appear the moment a
robot you are certified for raises a distress call. While the operator is in
Aux, the message appends that no offers will be made until they go Ready.
Shift metrics on this screen
The This shift card shows four counters, labelled:
These four values are literals in the screen source, not results of any
procedure the screen calls.
api.me() and api.availableJobs() are the only
data the screen fetches. Treat the tile as a placeholder layout for shift
counters, and do not read the displayed numbers as live operator telemetry.
Related
- Authentication — how the org-scoped token that
authorises the
private-cti-<orgId>subscribe is minted - Telemetry — the metric and trace streams the gateway emits for sessions

