openvibers/openvibe.shared Commits
The Dock's agent view: Pause, Curtain, Mute and End (#309)
Changes
@@@ -1,5 +1,9 @@# Changelog++## 4.124.0 — 2026-11-07++++- **The Dock's agent view gets the four controls** (Agent Streams S5, Co-pilot): Pause and Resume, Curtain now, Mute and Unmute, and End, each sent through Codes to Live. The owner's own view may always resume; Live still refuses a moderator resuming the owner's pause. The line says "Paused by @name" when someone else paused the stream.++## 4.123.0 — 2026-11-06- **The "not on the list" gate from markup**: `<div data-ov-share-gate data-kind data-id data-signed-in-as data-sign-in-url>` is started by `ov-share.js` when it loads (and by `OVShare.startGates()` for markup added later), once per element. Sites no longer write an inline script for it: an inline script needs a CSP nonce on most sites, and a value written into one with an HTML-escaping template turns its quotes into `"` and breaks the script, so the gate never drew. The values are plain attributes, escaped like any other.@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.123.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.124.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.123.0",++ "version": "4.124.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -5,7 +5,9 @@// tab opens:// OpenVibeDockAgent.mount(container, { token, base, active }) → { refresh(), destroy() }//--// `base` is OpenVibe.Codes' API (GET /api/v1/me/agent-view, the same POST for Curtain now and End, and the frame).++// `base` is OpenVibe.Codes' API (GET /api/v1/me/agent-view, the same POST for pause, Curtain now, mute and End, and++// the frame). Agent Streams S5 adds the four controls: a moderator cannot resume the owner's pause on Live, and the++// Dock is the owner's own view, so it may always resume.// Codes mints the view ticket server-side with the person's own token — an iframe on another site has no Live cookie —// so the frame it answers is already Live's /embed/agent/<ags_id>?vt=wvt_…: the picture, the "AI agent" label, the// curtain state and the viewer count. The embed renews its own ticket while it plays.@@@ -23,6 +25,10 @@const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;const G = {eye: '<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"/><circle cx="12" cy="12" r="3"/><path d="M4 4l16 16"/>',++ pause: '<rect x="7" y="5" width="3.5" height="14" rx="1"/><rect x="13.5" y="5" width="3.5" height="14" rx="1"/>',++ play: '<path d="M8 5.5v13l10-6.5z"/>',++ mute: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9l4 4m0-4l-4 4"/>',++ sound: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9.5a4 4 0 0 1 0 5"/>',stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',};@@@ -89,7 +95,7 @@const base = String(o.base || 'https://openvibe.codes').replace(/\/+$/, '');const t = typeof o.token === 'function' ? await o.token() : o.token;const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };-- const r = await fetch(base + path, { ...init, headers, credentials: 'omit', body: init.json ? '{}' : undefined });++ const r = await fetch(base + path, { ...init, headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });const b = await r.json().catch(() => ({}));if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });return b;@@@ -156,12 +162,21 @@meta.textContent = '';const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];if (live && agent.viewers > 0) bits.push(`${agent.viewers} watching`);++ if (agent.paused) bits.push(agent.paused_by ? `Paused by @${agent.paused_by}` : 'Paused');if (live && agent.curtain && agent.curtain.up) bits.push('Private screen');bits.push(`worked ${mins(agent.worked_seconds)}`);meta.textContent = bits.join(' · ');-- // The owner's buttons.++ // S5: the four controls, for the owner (the Dock is their own view, so resume is always theirs).acts.innerHTML = '';++ if (!ended) {++ const pause = h('button', null);++ pause.type = 'button';++ pause.dataset.act = 'pause';++ pause.innerHTML = `${svg(agent.paused ? G.play : G.pause)}${agent.paused ? 'Resume' : 'Pause'}`;++ pause.title = agent.paused && agent.paused_by ? `Paused by @${agent.paused_by}` : 'Pause the agent at the next safe point';++ acts.append(pause);++ }if (live && !(agent.curtain && agent.curtain.up)) {const b = h('button', null);b.type = 'button';@@@ -170,6 +185,14 @@b.title = 'Hide the screen now; your desk lifts it when it is clear';acts.append(b);}++ if (!ended) {++ const mute = h('button', null);++ mute.type = 'button';++ mute.dataset.act = 'mute';++ mute.innerHTML = `${svg(agent.voice_muted ? G.sound : G.mute)}${agent.voice_muted ? 'Unmute' : 'Mute'}`;++ mute.title = agent.voice_muted ? 'Let the agent speak again' : 'Mute the agent\'s voice';++ acts.append(mute);++ }if (!ended) {const b = h('button', null);b.type = 'button';@@@ -212,11 +235,11 @@if (!gone && agent && agent.state !== 'ended') timer = setTimeout(refresh, POLL_MS);}-- async function control(what, btn) {++ async function control(what, btn, body = {}) {if (!btn || btn.disabled) return;btn.disabled = true;try {-- const doc = await api(`/api/v1/me/agent-view/${what}`, { method: 'POST', json: true });++ const doc = await api(`/api/v1/me/agent-view/${what}`, { method: 'POST', json: true, body });agent = doc && doc.agent ? doc.agent : agent;if (what === 'end') frameUrl = '';paint();@@@ -231,7 +254,9 @@acts.addEventListener('click', (e) => {const b = e.target.closest('button[data-act]');if (!b) return;-- if (b.dataset.act === 'curtain') control('curtain', b);++ if (b.dataset.act === 'pause') control('pause', b, { paused: !(agent && agent.paused) });++ else if (b.dataset.act === 'curtain') control('curtain', b);++ else if (b.dataset.act === 'mute') control('mute', b, { muted: !(agent && agent.voice_muted) });else if (b.dataset.act === 'end') control('end', b);});@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.123.0",++ "version": "4.124.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -95,13 +95,15 @@ const server = http.createServer((req, res) => {res.writeHead(200, { 'content-type': 'image/jpeg' });return res.end(JPEG);}-- const ctl = /^\/api\/v1\/me\/agent-view\/(curtain|end)$/.exec(u.pathname);++ const ctl = /^\/api\/v1\/me\/agent-view\/(pause|curtain|mute|end)$/.exec(u.pathname);if (ctl && req.method === 'POST') {assert.strictEqual(req.headers.authorization, 'Bearer tok_user', `${ctl[1]} is pressed with the person's own token`);controls.push(ctl[1]);-- agentView = ctl[1] === 'curtain'-- ? { ...agentView, curtain: { up: true } }-- : { ...agentView, state: 'ended', embed_url: null, last_frame_url: '/api/v1/me/agent-view/frame' };++ const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');++ agentView = ctl[1] === 'pause' ? { ...agentView, paused: !!body.paused, paused_by: body.paused ? 'ada' : null }++ : ctl[1] === 'curtain' ? { ...agentView, curtain: { up: true } }++ : ctl[1] === 'mute' ? { ...agentView, voice_muted: !!body.muted }++ : { ...agentView, state: 'ended', paused: false, paused_by: null, embed_url: null, last_frame_url: '/api/v1/me/agent-view/frame' };return json(200, { agent: agentView });}const m = /^\/api\/v1\/me\/actions\/(\w+)\/(\w+)\/(approve|decline)$/.exec(u.pathname);@@@ -309,18 +311,23 @@ async function drag(p, from, to) {assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line');assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min');-- assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['curtain', 'end'], 'the owner\'s two buttons');++ assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end'], 'the owner\'s four controls (S5)');assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list');// Curtain now: Live is asked, and the line says so.await p.evaluate("document.querySelector('.ovda-acts button[data-act=curtain]').click()");await until(p, "document.querySelector('.ovda-meta').textContent.includes('Private screen')", 'the curtain in the line');assert.deepStrictEqual(controls, ['curtain'], 'Curtain now reached Codes, which puts it to Live');assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda-acts button[data-act=curtain]')"), false, 'no Curtain now while the curtain is up');++ // S5: Pause and Mute reach Codes, which puts them to Live; the line says whose pause it is.++ await p.evaluate("document.querySelector('.ovda-acts button[data-act=pause]').click()");++ await until(p, "document.querySelector('.ovda-acts button[data-act=pause]').textContent.includes('Resume')", 'the pause in the button');++ await p.evaluate("document.querySelector('.ovda-acts button[data-act=mute]').click()");++ await until(p, "document.querySelector('.ovda-acts button[data-act=mute]').textContent.includes('Unmute')", 'the mute in the button');// End: the stream becomes the last frame, and the list is still there under it.await p.evaluate("document.querySelector('.ovda-acts button[data-act=end]').click()");const stillSrc = await until(p, "document.querySelector('.ovda img') && document.querySelector('.ovda img').getAttribute('src')", 'the still after End');assert.ok(stillSrc.startsWith('blob:'), 'the still is read with the token, never a bare <img> URL');-- assert.deepStrictEqual(controls, ['curtain', 'end'], 'End reached Codes, which puts it to Live');++ assert.deepStrictEqual(controls, ['curtain', 'pause', 'mute', 'end'], 'Pause, Mute and End reached Codes, which puts them to Live');assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda iframe')"), false, 'no player once it has ended');assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Ended · worked 12 min');assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the agents list is untouched under the stream');
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
# Changelog
# Changelog
+## 4.124.0 — 2026-11-07
+
+- **The Dock's agent view gets the four controls** (Agent Streams S5, Co-pilot): Pause and Resume, Curtain now, Mute and Unmute, and End, each sent through Codes to Live. The owner's own view may always resume; Live still refuses a moderator resuming the owner's pause. The line says "Paused by @name" when someone else paused the stream.
+
## 4.123.0 — 2026-11-06
## 4.123.0 — 2026-11-06
- **The "not on the list" gate from markup**: `<div data-ov-share-gate data-kind data-id data-signed-in-as data-sign-in-url>` is started by `ov-share.js` when it loads (and by `OVShare.startGates()` for markup added later), once per element. Sites no longer write an inline script for it: an inline script needs a CSP nonce on most sites, and a value written into one with an HTML-escaping template turns its quotes into `"` and breaks the script, so the gate never drew. The values are plain attributes, escaped like any other.
- **The "not on the list" gate from markup**: `<div data-ov-share-gate data-kind data-id data-signed-in-as data-sign-in-url>` is started by `ov-share.js` when it loads (and by `OVShare.startGates()` for markup added later), once per element. Sites no longer write an inline script for it: an inline script needs a CSP nonce on most sites, and a value written into one with an HTML-escaping template turns its quotes into `"` and breaks the script, so the gate never drew. The values are plain attributes, escaped like any other.
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.123.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.124.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.123.0",
+ "version": "4.124.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -5,7 +5,9 @@
@@ -5,7 +5,9 @@
// tab opens:
// tab opens:
// OpenVibeDockAgent.mount(container, { token, base, active }) → { refresh(), destroy() }
// OpenVibeDockAgent.mount(container, { token, base, active }) → { refresh(), destroy() }
//
//
-// `base` is OpenVibe.Codes' API (GET /api/v1/me/agent-view, the same POST for Curtain now and End, and the frame).
+// `base` is OpenVibe.Codes' API (GET /api/v1/me/agent-view, the same POST for pause, Curtain now, mute and End, and
+// the frame). Agent Streams S5 adds the four controls: a moderator cannot resume the owner's pause on Live, and the
+// Dock is the owner's own view, so it may always resume.
// Codes mints the view ticket server-side with the person's own token — an iframe on another site has no Live cookie —
// Codes mints the view ticket server-side with the person's own token — an iframe on another site has no Live cookie —
// so the frame it answers is already Live's /embed/agent/<ags_id>?vt=wvt_…: the picture, the "AI agent" label, the
// so the frame it answers is already Live's /embed/agent/<ags_id>?vt=wvt_…: the picture, the "AI agent" label, the
// curtain state and the viewer count. The embed renews its own ticket while it plays.
// curtain state and the viewer count. The embed renews its own ticket while it plays.
@@ -23,6 +25,10 @@
@@ -23,6 +25,10 @@
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const G = {
const G = {
eye: '<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"/><circle cx="12" cy="12" r="3"/><path d="M4 4l16 16"/>',
eye: '<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"/><circle cx="12" cy="12" r="3"/><path d="M4 4l16 16"/>',
+ pause: '<rect x="7" y="5" width="3.5" height="14" rx="1"/><rect x="13.5" y="5" width="3.5" height="14" rx="1"/>',
+ play: '<path d="M8 5.5v13l10-6.5z"/>',
+ mute: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9l4 4m0-4l-4 4"/>',
+ sound: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9.5a4 4 0 0 1 0 5"/>',
stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',
stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',
open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',
open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',
};
};
@@ -89,7 +95,7 @@
@@ -89,7 +95,7 @@
const base = String(o.base || 'https://openvibe.codes').replace(/\/+$/, '');
const base = String(o.base || 'https://openvibe.codes').replace(/\/+$/, '');
const t = typeof o.token === 'function' ? await o.token() : o.token;
const t = typeof o.token === 'function' ? await o.token() : o.token;
const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };
const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };
- const r = await fetch(base + path, { ...init, headers, credentials: 'omit', body: init.json ? '{}' : undefined });
+ const r = await fetch(base + path, { ...init, headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });
const b = await r.json().catch(() => ({}));
const b = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });
if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });
return b;
return b;
@@ -156,12 +162,21 @@
@@ -156,12 +162,21 @@
meta.textContent = '';
meta.textContent = '';
const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];
const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];
if (live && agent.viewers > 0) bits.push(`${agent.viewers} watching`);
if (live && agent.viewers > 0) bits.push(`${agent.viewers} watching`);
+ if (agent.paused) bits.push(agent.paused_by ? `Paused by @${agent.paused_by}` : 'Paused');
if (live && agent.curtain && agent.curtain.up) bits.push('Private screen');
if (live && agent.curtain && agent.curtain.up) bits.push('Private screen');
bits.push(`worked ${mins(agent.worked_seconds)}`);
bits.push(`worked ${mins(agent.worked_seconds)}`);
meta.textContent = bits.join(' · ');
meta.textContent = bits.join(' · ');
- // The owner's buttons.
+ // S5: the four controls, for the owner (the Dock is their own view, so resume is always theirs).
acts.innerHTML = '';
acts.innerHTML = '';
+ if (!ended) {
+ const pause = h('button', null);
+ pause.type = 'button';
+ pause.dataset.act = 'pause';
+ pause.innerHTML = `${svg(agent.paused ? G.play : G.pause)}${agent.paused ? 'Resume' : 'Pause'}`;
+ pause.title = agent.paused && agent.paused_by ? `Paused by @${agent.paused_by}` : 'Pause the agent at the next safe point';
+ acts.append(pause);
+ }
if (live && !(agent.curtain && agent.curtain.up)) {
if (live && !(agent.curtain && agent.curtain.up)) {
const b = h('button', null);
const b = h('button', null);
b.type = 'button';
b.type = 'button';
@@ -170,6 +185,14 @@
@@ -170,6 +185,14 @@
b.title = 'Hide the screen now; your desk lifts it when it is clear';
b.title = 'Hide the screen now; your desk lifts it when it is clear';
acts.append(b);
acts.append(b);
}
}
+ if (!ended) {
+ const mute = h('button', null);
+ mute.type = 'button';
+ mute.dataset.act = 'mute';
+ mute.innerHTML = `${svg(agent.voice_muted ? G.sound : G.mute)}${agent.voice_muted ? 'Unmute' : 'Mute'}`;
+ mute.title = agent.voice_muted ? 'Let the agent speak again' : 'Mute the agent\'s voice';
+ acts.append(mute);
+ }
if (!ended) {
if (!ended) {
const b = h('button', null);
const b = h('button', null);
b.type = 'button';
b.type = 'button';
@@ -212,11 +235,11 @@
@@ -212,11 +235,11 @@
if (!gone && agent && agent.state !== 'ended') timer = setTimeout(refresh, POLL_MS);
if (!gone && agent && agent.state !== 'ended') timer = setTimeout(refresh, POLL_MS);
}
}
- async function control(what, btn) {
+ async function control(what, btn, body = {}) {
if (!btn || btn.disabled) return;
if (!btn || btn.disabled) return;
btn.disabled = true;
btn.disabled = true;
try {
try {
- const doc = await api(`/api/v1/me/agent-view/${what}`, { method: 'POST', json: true });
+ const doc = await api(`/api/v1/me/agent-view/${what}`, { method: 'POST', json: true, body });
agent = doc && doc.agent ? doc.agent : agent;
agent = doc && doc.agent ? doc.agent : agent;
if (what === 'end') frameUrl = '';
if (what === 'end') frameUrl = '';
paint();
paint();
@@ -231,7 +254,9 @@
@@ -231,7 +254,9 @@
acts.addEventListener('click', (e) => {
acts.addEventListener('click', (e) => {
const b = e.target.closest('button[data-act]');
const b = e.target.closest('button[data-act]');
if (!b) return;
if (!b) return;
- if (b.dataset.act === 'curtain') control('curtain', b);
+ if (b.dataset.act === 'pause') control('pause', b, { paused: !(agent && agent.paused) });
+ else if (b.dataset.act === 'curtain') control('curtain', b);
+ else if (b.dataset.act === 'mute') control('mute', b, { muted: !(agent && agent.voice_muted) });
else if (b.dataset.act === 'end') control('end', b);
else if (b.dataset.act === 'end') control('end', b);
});
});
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.123.0",
+ "version": "4.124.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -95,13 +95,15 @@ const server = http.createServer((req, res) => {
@@ -95,13 +95,15 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'image/jpeg' });
res.writeHead(200, { 'content-type': 'image/jpeg' });
return res.end(JPEG);
return res.end(JPEG);
}
}
- const ctl = /^\/api\/v1\/me\/agent-view\/(curtain|end)$/.exec(u.pathname);
+ const ctl = /^\/api\/v1\/me\/agent-view\/(pause|curtain|mute|end)$/.exec(u.pathname);
if (ctl && req.method === 'POST') {
if (ctl && req.method === 'POST') {
assert.strictEqual(req.headers.authorization, 'Bearer tok_user', `${ctl[1]} is pressed with the person's own token`);
assert.strictEqual(req.headers.authorization, 'Bearer tok_user', `${ctl[1]} is pressed with the person's own token`);
controls.push(ctl[1]);
controls.push(ctl[1]);
- agentView = ctl[1] === 'curtain'
- ? { ...agentView, curtain: { up: true } }
- : { ...agentView, state: 'ended', embed_url: null, last_frame_url: '/api/v1/me/agent-view/frame' };
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
+ agentView = ctl[1] === 'pause' ? { ...agentView, paused: !!body.paused, paused_by: body.paused ? 'ada' : null }
+ : ctl[1] === 'curtain' ? { ...agentView, curtain: { up: true } }
+ : ctl[1] === 'mute' ? { ...agentView, voice_muted: !!body.muted }
+ : { ...agentView, state: 'ended', paused: false, paused_by: null, embed_url: null, last_frame_url: '/api/v1/me/agent-view/frame' };
return json(200, { agent: agentView });
return json(200, { agent: agentView });
}
}
const m = /^\/api\/v1\/me\/actions\/(\w+)\/(\w+)\/(approve|decline)$/.exec(u.pathname);
const m = /^\/api\/v1\/me\/actions\/(\w+)\/(\w+)\/(approve|decline)$/.exec(u.pathname);
@@ -309,18 +311,23 @@ async function drag(p, from, to) {
@@ -309,18 +311,23 @@ async function drag(p, from, to) {
assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');
assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min');
- assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['curtain', 'end'], 'the owner\'s two buttons');
+ assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end'], 'the owner\'s four controls (S5)');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list');
// Curtain now: Live is asked, and the line says so.
// Curtain now: Live is asked, and the line says so.
await p.evaluate("document.querySelector('.ovda-acts button[data-act=curtain]').click()");
await p.evaluate("document.querySelector('.ovda-acts button[data-act=curtain]').click()");
await until(p, "document.querySelector('.ovda-meta').textContent.includes('Private screen')", 'the curtain in the line');
await until(p, "document.querySelector('.ovda-meta').textContent.includes('Private screen')", 'the curtain in the line');
assert.deepStrictEqual(controls, ['curtain'], 'Curtain now reached Codes, which puts it to Live');
assert.deepStrictEqual(controls, ['curtain'], 'Curtain now reached Codes, which puts it to Live');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda-acts button[data-act=curtain]')"), false, 'no Curtain now while the curtain is up');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda-acts button[data-act=curtain]')"), false, 'no Curtain now while the curtain is up');
+ // S5: Pause and Mute reach Codes, which puts them to Live; the line says whose pause it is.
+ await p.evaluate("document.querySelector('.ovda-acts button[data-act=pause]').click()");
+ await until(p, "document.querySelector('.ovda-acts button[data-act=pause]').textContent.includes('Resume')", 'the pause in the button');
+ await p.evaluate("document.querySelector('.ovda-acts button[data-act=mute]').click()");
+ await until(p, "document.querySelector('.ovda-acts button[data-act=mute]').textContent.includes('Unmute')", 'the mute in the button');
// End: the stream becomes the last frame, and the list is still there under it.
// End: the stream becomes the last frame, and the list is still there under it.
await p.evaluate("document.querySelector('.ovda-acts button[data-act=end]').click()");
await p.evaluate("document.querySelector('.ovda-acts button[data-act=end]').click()");
const stillSrc = await until(p, "document.querySelector('.ovda img') && document.querySelector('.ovda img').getAttribute('src')", 'the still after End');
const stillSrc = await until(p, "document.querySelector('.ovda img') && document.querySelector('.ovda img').getAttribute('src')", 'the still after End');
assert.ok(stillSrc.startsWith('blob:'), 'the still is read with the token, never a bare <img> URL');
assert.ok(stillSrc.startsWith('blob:'), 'the still is read with the token, never a bare <img> URL');
- assert.deepStrictEqual(controls, ['curtain', 'end'], 'End reached Codes, which puts it to Live');
+ assert.deepStrictEqual(controls, ['curtain', 'pause', 'mute', 'end'], 'Pause, Mute and End reached Codes, which puts them to Live');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda iframe')"), false, 'no player once it has ended');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovda iframe')"), false, 'no player once it has ended');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Ended · worked 12 min');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Ended · worked 12 min');
assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the agents list is untouched under the stream');
assert.strictEqual(await p.evaluate("document.querySelectorAll('.ovac-item').length"), 3, 'the agents list is untouched under the stream');