openvibers/openvibe.bot Commits

Bot: Arrange shows only to the robot's owner (#411)

OP OpenVibers committed 1 days ago · 5eff6afbf3 · 3 files, +11 −3

* Bot: make your own controls, with the Arrange editor on the panel and the overlay

* Bot: keep known parts across robots, fill the arranged cells and show the model

* Merge origin/main: overlay controls stay in their corners

* Migration: the large pad still fits beside Stop on a phone

* Network calls: a suggestion may take longer than a panel read

* Bot: the released Contracts tag, the reviewed SDK and Shared commits

* STATUS: the reviewed SDK and Shared commits

* CI: allow a manual run on a branch

* Merge origin/main: headlight actions and the Elements role check

* Arrange: Reset to default clears the person's layout in Network

* Pin the Shared commit whose Reset clears the layout

* Merge origin/main: pin the released 0.410.0, 0.152.0 and 4.214.0

* STATUS: updated 2026-11-12

* Arrange shows only to the robot's owner

* Merge origin/main after #405: the owner-only Arrange follows the editor

Changes

ddiff --git a/server/web/render.js b/server/web/render.js
iindex 2a84b51..ac24bc7 100644
---- a/server/web/render.js
++++ b/server/web/render.js
@@@ -755,7 +755,7 @@ function renderPanel({ robot, profile, role, allowed_commands = [], holdResendMs
const estop = ESTOP_ROLES.has(role) ? `<button type="button" class="estop" data-estop>${icon('stop')}<span>E-stop</span></button>`
: (embed && role === 'watcher' ? `<button type="button" class="estop" data-estop hidden>${icon('stop')}<span>E-stop</span></button>` : '');
const clear = role === 'owner' ? `<button type="button" class="estop-clear" data-estop-clear${latched ? '' : ' hidden'}>Clear e-stop</button>` : '';
-- const arrangeButton = arrange && arrange.src
++ const arrangeButton = arrange && arrange.src && role === 'owner'
? `<button type="button" class="arrange-btn" data-arrange data-arrange-src="${esc(arrange.src || '')}" title="Arrange the controls">${icon('sliders')}<span>Arrange</span></button>` : '';
const body = `<header class="estop-banner" data-estop-banner data-latched="${latched}">
${embed ? '' : tryIt ? topbar('<span class="crumb">Try the panel</span>', '/') : topbar(`<span class="crumb">${esc(robot.name)}</span>`)}
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex 19bda20..6df9c6e 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -972,7 +972,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
hasStick: !!overlayStickMarkup(profile, role === 'watcher' ? [] : domain.control.allowedFor(robot, role, profile)),
canDrive,
});
-- const storedRaw = signedIn && token ? await layoutStore.activeLayout({ config, token, surface: catalog.id, device }) : null;
++ const storedRaw = role === 'owner' && token ? await layoutStore.activeLayout({ config, token, surface: catalog.id, device }) : null;
const fallbackLayout = surface.overlayDefaultLayout(catalog, device);
const stored = usable(catalog, storedRaw, fallbackLayout);
arrange = {
@@@ -983,7 +983,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
};
} else {
const catalog = surface.controlsCatalog(profile);
-- const storedRaw = signedIn && token ? await layoutStore.activeLayout({ config, token, surface: catalog.id, device }) : null;
++ const storedRaw = role === 'owner' && token ? await layoutStore.activeLayout({ config, token, surface: catalog.id, device }) : null;
const fallbackLayout = surface.defaultLayout(catalog, device);
const stored = usable(catalog, storedRaw, fallbackLayout);
let src = null;
ddiff --git a/test/arrange-panel.test.js b/test/arrange-panel.test.js
iindex 76e4a1c..8ff84ff 100644
---- a/test/arrange-panel.test.js
++++ b/test/arrange-panel.test.js
@@@ -41,6 +41,14 @@ assert.ok(/data-ov-arranged="true"/.test(withLayout), 'a stored layout draws the
assert.ok(/grid-column:1 \/ span 2/.test(withLayout), 'the stick is drawn in its cells');
assert.ok(/data-ov-part="battery"[^>]*hidden/.test(withLayout) || /hidden[^>]*data-ov-part="battery"/.test(withLayout), 'a hidden readout is hidden before first paint');
++// Only the owner gets Arrange: a framed, signed-out watcher of a public robot sees the controls, not the editor.
++const watched = render.renderPanel({
++ robot, profile, role: 'watcher', allowed_commands: [], mode: 'embed', view: 'panel', signedIn: false,
++ arrange: { catalog, parts: surface.partsOf(profile), layout: defaults, device: 'phone', signedIn: false, layoutSource: 'default', src: '/shared/ov-arrange.js' },
++});
++assert.ok(!watched.includes('data-arrange'), 'a watcher has no Arrange button');
++assert.ok(watched.includes('data-ov-part="stop"'), 'but the parts are still there');
++
const overlayCatalog = surface.overlayCatalog(profile, { slider: { name: 'head' }, hasStick: true, canDrive: false });
const overlay = render.renderPanel({
robot, profile, role: 'watcher', allowed_commands: [], mode: 'embed', view: 'overlay', signedIn: false,