openvibers/openvibe.bot Commits

Desk turn (Desks plan D4): GET /api/v1/machines/:id/desk-turn says whether a person is using a desk and when they last did, so an agent that handed its person a step waits for them (#178)

OP OpenVibers committed 17 days ago · 6f8cb0751c · 3 files, +42 −1

Changes

ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex 24ccaf3..0317b9f 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -457,6 +457,15 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
if (result.result === 'refused') fail(commandRefusalStatus(result.code), result.code, result.reason || null);
res.json({ machine_id: device.id, ...result });
}));
++ // Who has a desk right now (Desks plan D4): an agent that asked its person to take over a step (a sign-in, a
++ // CAPTCHA, a payment) waits for them to take it, then to stop. Read only; the person's own input is what moves it.
++ r.get('/machines/:id/desk-turn', commandControl, wrap(async (req, res) => {
++ const { device } = await computerMachine(req);
++ const desk = typeof req.query.desk === 'string' && /^(main|private|desk-[a-z0-9][a-z0-9-]{0,23})$/.test(req.query.desk) ? req.query.desk : null;
++ if (!desk) fail(422, 'bot.invalid_input', 'desk is main, private or desk-<name>');
++ res.set('Cache-Control', 'no-store');
++ res.json({ machine_id: device.id, desk, ...hub.deskTurn(device.id, desk) });
++ }));
// The live screen (plan T14): the machine's owner, signed in as themselves, watches it on their own private stream.
// Never an agent, a watch or another service: screen_stream is not a bot.command@1 kind, and only this route sends it.
const screenOwner = async (req) => {
ddiff --git a/server/realtime.js b/server/realtime.js
iindex 401c3ed..d0adc99 100644
---- a/server/realtime.js
++++ b/server/realtime.js
@@@ -119,6 +119,7 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
const inflight = new Map(); // operator key → device-facing id, while the command is pending
// The person always wins a screen (machines): `${device}|${main|private}` → until when a person's input holds it.
const personTurns = new Map();
++ const personLast = new Map(); // the same key → when a person last used that screen (ms), for desk turns
const timers = [];
let closed = false;
@@@ -777,8 +778,9 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
const turnKey = `${deviceId}|${deskOf(value)}`; // each desk is its own screen: a person on one never holds another
const person = conn.principal.kind === 'user' || conn.principal.kind === 'share';
if (person) {
-- if (personTurns.size > 5000) personTurns.clear();
++ if (personTurns.size > 5000) { personTurns.clear(); personLast.clear(); }
personTurns.set(turnKey, now() + config.device.personTurnMs);
++ personLast.set(turnKey, now());
} else if ((personTurns.get(turnKey) || 0) > now()) {
const left = Math.ceil((personTurns.get(turnKey) - now()) / 1000);
return refuse('bot.person_driving', `a person is using this screen right now: try again in ${left} s, after they stop`, summary);
@@@ -1031,6 +1033,16 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
},
bindJobs(j) { jobs = j; },
handleUpgrade, attachSim, detachSim,
++ /**
++ * Who has a screen right now (Desks plan D4): whether a person is using it (their input holds agents off it for
++ * BOT_PERSON_TURN_MS after each action) and when they last did. An agent that handed a desk to its person waits
++ * for them to take it, then to stop, before it goes on.
++ */
++ deskTurn(deviceId, desk) {
++ const key = `${deviceId}|${desk}`;
++ const last = personLast.get(key) || null;
++ return { person_driving: (personTurns.get(key) || 0) > now(), last_person_at: last ? new Date(last).toISOString() : null };
++ },
/** A one-time ticket for the embedded panel's Drive (POST /panel/:id/ticket), or null. */
issueControlTicket(principal, robotId) { return tickets.issue(principal, robotId); },
isOnline, deviceState, sendToDevice, sendToRobotDevices, broadcast, closeDevice, robotState, refreshConfig, pushEstop, closeWatchers, dropSubject, operatorCommand,
ddiff --git a/test/machine-computer.test.js b/test/machine-computer.test.js
iindex 8111741..5d80fa4 100644
---- a/test/machine-computer.test.js
++++ b/test/machine-computer.test.js
@@@ -140,6 +140,26 @@ const { boot, check, done } = require('./helpers/app');
withDesk.c.close();
});
++ await check('desk turn (Desks plan D4): an agent sees whether its person is using a desk and when they last did; read only', async () => {
++ const m = await machine(alex, { ...COMPUTER, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 } });
++ const token = actor();
++ await t.call('POST', `/api/v1/machines/${m.deviceId}/agent-computer`, { user: alex, body: { on: true } });
++ const turn = async (desk) => t.call('GET', `/api/v1/machines/${m.deviceId}/desk-turn?desk=${desk}`, asActor(token, alex.subject));
++ const before = await turn('desk-task-a');
++ assert.deepStrictEqual([before.status, before.json.person_driving, before.json.last_person_at], [200, false, null], 'nobody yet');
++ assert.strictEqual((await cmd(m.deviceId, { kind: 'input', value: { action: 'move', x: 5, y: 5, width: 1280, height: 800, desk: 'desk-task-a' } }, { user: alex })).json.result, 'ack');
++ const during = await turn('desk-task-a');
++ assert.strictEqual(during.json.person_driving, true, 'the person is using it');
++ assert.ok(Date.parse(during.json.last_person_at) <= Date.now());
++ assert.strictEqual((await turn('desk-task-b')).json.person_driving, false, 'another desk is another screen');
++ await t.wait(450); // BOT_PERSON_TURN_MS is 400 in this boot
++ const after = await turn('desk-task-a');
++ assert.deepStrictEqual([after.json.person_driving, after.json.last_person_at], [false, during.json.last_person_at], 'they stopped; when they last did stays');
++ assert.strictEqual((await turn('desk-../x')).status, 422);
++ await t.call('POST', `/api/v1/machines/${m.deviceId}/agent-computer`, { user: alex, body: { on: false } });
++ m.c.close();
++ });
++
await check('named desks (contracts 0.215.0, Node v0.19.0): one per agent task, each its own screen; an older Node runs one', async () => {
const multi = await machine(alex, { ...COMPUTER, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 } });
const single = await machine(alex, { ...COMPUTER, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true } });