openvibers/openvibe.shared Commits
4.209.0: the Quest guide on any page, and the current Live step in the Dock (#469)
Changes
@@@ -1,5 +1,15 @@# Changelog++## 4.209.0 — 2026-11-11++++**The Quest guide on any page, and the current Live step in the Dock's agent view.**++++- **The Quest guide for browser pages (#464).** Pages can start the Quest guide from an opaque `qstep` link once the host++ resolves the step; it waits for late controls and checks the Events-backed completion read, and the shared navbar++ starts it from its own sign-in.++- **The Dock's agent view shows the current Live step (#466)** when the stream supplies one; older streams keep showing++ the task without a step line.++## 4.208.0 — 2026-11-11**One move engine for every floating thing: the picture-in-picture's drag, flick, tuck, pinch, corner and place, now@@@ -5,7 +5,7 @@**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt**Install and test:** `npm ci && npm test`--**Status:** alpha, v4.208.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.209.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,13 +4,14 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.208.0",++ "version": "4.209.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)","contracts": "openvibe-contracts v0.399.0 (devDependency: the release manifest contract)","sdk": "openvibe-sdk v0.147.0","features": [++ "the Quest guide on any page and the current Live step in the Dock's agent view (4.209.0)","one move engine for a floating thing: OVFloat.attach gives the PiP's drag, flick, tuck and kept place to any box, and a panel window adopts it (4.208.0)","the menu Feed searches Community with examples and uses the one composer for pictures and files; the Chat summary opens the Vibe tab (4.206.0)","the one step view reads AG-UI (phases, titled steps, sources, thinking, compact groups, card mode) with the one composer to talk to a run, and crawlable first HTML for every site (4.205.0)",@@@ -16,7 +16,7 @@// curtain state and the viewer count. The embed renews its own ticket while it plays.//// Nothing live: the last frame through the same API (an <img> cannot carry a token), with the task and the worked time.--// There is no step line yet: steps are Agent Streams S2, and this view shows none until Live serves them.++// Live's S2 current step comes through Codes when available; an older stream simply has no step line.// Everything is set as text, never markup; the frame's address is the one Codes answered, as an https Live address.// ═══════════════════════════════════════════════════════════════(function (root) {@@@ -97,10 +97,11 @@container.classList.add('ovda');const stage = h('div', 'ovda-stage');const line = h('div', 'ovda-line');++ const step = h('div', 'ovda-meta ovda-step');const meta = h('div', 'ovda-meta');const acts = h('div', 'ovda-acts');const head = h('div', 'ovda-head');-- head.append(line, meta, acts);++ head.append(line, meta, step, acts);const body = h('div');body.append(stage, head);container.append(body);@@@ -178,6 +179,11 @@else title.textContent = text;line.append(title);++ const current = agent.current_step;++ step.textContent = current && typeof current.title === 'string' && current.title.trim()++ ? `Step: ${current.title} · ${current.status || 'started'}` : '';++ step.hidden = !step.textContent;++// The meta: the state, the viewers, the curtain, the worked time.meta.textContent = '';const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];@@@ -89,7 +89,7 @@}// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.-- const DOCK_REV = { 'dock-vibe.js': '061fd680f4f7', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': '8ed09c416a48', 'dock-agent.js': 'c1281ef69ea8', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };++ const DOCK_REV = { 'dock-vibe.js': '061fd680f4f7', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': '8ed09c416a48', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };function load(src, test) {return new Promise((ok) => {if (test()) return ok(true);@@@ -73,6 +73,7 @@ const BROWSER = Object.freeze(['items.js', // what people wear (OpenVibe.Inventory name effects, plan T21); draw with items.css'profile-panel.js', // a person's stuff on any site (worn items, badges, achievements), from the Network profile'quest.js', // the daily quest card any site can show (OpenVibeQuest.mount); draw with quest.css++ 'guide.js', // the opaque quest step guide (OpenVibeQuestGuide.startFromLocation)'tooltip.js','player.js', // the network's one video player look: enhances video[data-ov-player]; draw with player.css'ov-sync.js', // one timeline for many players (seven domains B4): OVSync.attach(media, { room, socket }); no dependencies@@@ -1,7 +1,9 @@'use strict';++(function (root) {++/** Point at a real hooked control without activating it or covering it. The host owns pointer styling. */--function startQuestGuide({ step, nextHref, root = document, onDone = () => {} } = {}) {++function startQuestGuide({ step, nextHref, root = document, onDone = () => {}, checkDone = null, pollMs = 5000 } = {}) {if (!step || !/^[a-z][a-z0-9_.-]{1,63}$/.test(step.hook || '') || !step.event_type) return () => {};const control = root.querySelector(`[data-ov-guide="${CSS.escape(step.hook)}"]`);if (!control) return () => {};@@@ -15,6 +17,7 @@ function startQuestGuide({ step, nextHref, root = document, onDone = () => {} }const done = (event) => {if (finished || !event || event.detail?.event_type !== step.event_type) return;finished = true;++ if (poll) clearInterval(poll);note.replaceChildren(root.createTextNode('Done. '));if (nextHref) {const link = root.createElement('a'); link.href = nextHref; link.textContent = 'Next step'; note.append(link);@@@ -22,12 +25,40 @@ function startQuestGuide({ step, nextHref, root = document, onDone = () => {} }onDone(step);};root.addEventListener('ov:quest:step-done', done);++ const poll = typeof checkDone === 'function' && !step.done ? setInterval(async () => {++ try { const latest = await checkDone(); if (latest?.step?.done) done({ detail: { event_type: step.event_type } }); }++ catch { /* the Events receipt may still be arriving; the next read retries */ }++ }, pollMs) : null;++ if (poll && typeof poll.unref === 'function') poll.unref();const dismiss = (event) => {if (event.key === 'Escape' || (event.type === 'pointerdown' && !note.contains(event.target) && !control.contains(event.target))) stop();};-- function stop() { root.removeEventListener('ov:quest:step-done', done); root.removeEventListener('keydown', dismiss); root.removeEventListener('pointerdown', dismiss); note.remove(); }++ function stop() { if (poll) clearInterval(poll); root.removeEventListener('ov:quest:step-done', done); root.removeEventListener('keydown', dismiss); root.removeEventListener('pointerdown', dismiss); note.remove(); }root.addEventListener('keydown', dismiss); root.addEventListener('pointerdown', dismiss);++ if (step.done) done({ detail: { event_type: step.event_type } });return stop;}--module.exports = { startQuestGuide };++/** Resolve only a short opaque step id. The host supplies an authenticated reader. */++async function startFromLocation({ load, location = root.location, document: page = root.document, onDone } = {}) {++ if (typeof load !== 'function' || !location || !page) return () => {};++ const id = new URL(location.href).searchParams.get('qstep');++ if (!id || !/^[A-Za-z0-9_-]{12,48}$/.test(id)) return () => {};++ const answer = await load(id);++ if (!answer || !answer.step || !answer.step.hook) return () => {};++ // Many sites render the target after their page data arrives. Watch briefly instead of losing the guide.++ const selector = `[data-ov-guide="${CSS.escape(answer.step.hook)}"]`;++ if (!page.querySelector(selector) && page.defaultView?.MutationObserver) await new Promise((resolve) => {++ const observer = new page.defaultView.MutationObserver(() => {++ if (page.querySelector(selector)) { observer.disconnect(); clearTimeout(timer); resolve(); }++ });++ const timer = setTimeout(() => { observer.disconnect(); resolve(); }, 15000);++ observer.observe(page.documentElement, { childList: true, subtree: true });++ });++ return startQuestGuide({ step: answer.step, nextHref: answer.next_href, root: page, onDone, checkDone: () => load(id) });++}++++const api = { startQuestGuide, startFromLocation };++if (typeof module === 'object' && module.exports) module.exports = api;++if (root && root.document) root.OpenVibeQuestGuide = api;++})(typeof window !== 'undefined' ? window : globalThis);@@@ -607,7 +607,7 @@* serves its own pinned copy at /shared/ gets its own), else the network's copy.*/// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '0dbfc679c2a2', 'dock.js': '2dc02f5569ac', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4b39796c5145', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': 'cdacf9b1154b', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '0dbfc679c2a2', 'dock.js': '0ec4ae476c63', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4b39796c5145', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': 'cdacf9b1154b', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;// ov-tip.js loads at the first hover over a tooltip's owner.@@@ -1939,6 +1939,26 @@}}++ let _questGuideStarted = false;++ function startQuestGuide() {++ if (_questGuideStarted || !_config.token || !root.location || !/[?&]qstep=/.test(root.location.search || '')) return;++ _questGuideStarted = true;++ const script = document.createElement('script');++ script.src = siblingV('guide.js');++ script.onload = () => {++ if (!root.OpenVibeQuestGuide) return;++ void root.OpenVibeQuestGuide.startFromLocation({ load: async (id) => {++ try {++ const response = await fetch(`https://openvibe.quest/api/v1/quests/today/steps/${encodeURIComponent(id)}`, {++ headers: { Authorization: `Bearer ${_config.token}` }, credentials: 'omit',++ });++ return response.ok ? response.json() : null;++ } catch { return null; }++ } });++ };++ document.head.appendChild(script);++ }++function refreshAuthState() {if (_authInFlight) return _authInFlight;_authInFlight = resolveSessionUser()@@@ -1950,6 +1970,7 @@render();recordHistory();enableHandoff();++ startQuestGuide();} else {maybeSilentLogin();}@@@ -2631,6 +2652,7 @@if (_config.auth === 'external') { if (_config.user) { recordHistory(); enableHandoff(); } }else if (!_config.user) refreshAuthState();else { recordHistory(); enableHandoff(); }++ startQuestGuide();return el;},@@@ -2688,7 +2710,7 @@/** The signed-in user changed (sites with auth: 'external'). Pass null for signed out. */openWallet,refreshWallet() { _walletAt = 0; loadWalletChips(); },-- setUser(user, token) { if (root.OpenVibeWallet && (_config.user !== user || (token !== undefined && token !== _config.token))) root.OpenVibeWallet.close(); _config.user = user || null; if (token !== undefined) _config.token = token; _walletLoaded = false; return render(); },++ setUser(user, token) { if (root.OpenVibeWallet && (_config.user !== user || (token !== undefined && token !== _config.token))) root.OpenVibeWallet.close(); _config.user = user || null; if (token !== undefined) _config.token = token; _walletLoaded = false; const result = render(); startQuestGuide(); return result; },/** Single-page apps: mark the link whose `page` matches as active. */setActive(page) { _activePage = page || null; renderAppBar(); if (!_navEl) return; _navEl.querySelectorAll('.ovnav-link[data-page]').forEach(a => a.classList.toggle('active', a.getAttribute('data-page') === _activePage)); },/** Change a status chip's text (navbar and menu header): setChip('coins', '1,234'). */@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.208.0",++ "version": "4.209.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.208.0",++ "version": "4.209.0","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.208.0",++ "version": "4.209.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -191,6 +191,12 @@return api;}-- root.OpenVibeQuest = { mount };-- if (typeof module === 'object' && module.exports) module.exports = { mount };++ /** Mount every explicit quest host in a page. The site supplies its authenticated reader. */++ function mountAll({ load, root: page = document } = {}) {++ if (typeof load !== 'function') throw new TypeError('OpenVibeQuest.mountAll: load() must return the quest data');++ return [...page.querySelectorAll('[data-ov-quest]')].map((host) => mount(host, { load: () => load(host) }));++ }++++ root.OpenVibeQuest = { mount, mountAll };++ if (typeof module === 'object' && module.exports) module.exports = { mount, mountAll };})(typeof window !== 'undefined' ? window : globalThis);@@@ -460,6 +460,7 @@ async function drag(p, from, to) {const view = () => ({id: AGENT_ID, state: 'live', title: 'Fix the flaky upload test',task: { source: 'codes', ref: 'tsk_1', title: 'Fix the flaky upload test', url: 'https://openvibe.codes/run/tasks/tsk_1' },++ current_step: { title: 'Run the checkout tests', status: 'started' },curtain: { up: false }, viewers: 2, worked_seconds: 720, visibility: 'private',page_url: `https://openvibe.live/@ada/agents/${AGENT_ID}`,embed_url: `https://openvibe.live/embed/agent/${AGENT_ID}?vt=wvt_test`, last_frame_url: null,@@@ -475,6 +476,7 @@ async function drag(p, from, to) {const src = await until(p, "document.querySelector('.ovda iframe') && document.querySelector('.ovda iframe').getAttribute('src')", 'the embed');assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line'));++ await eventually(async () => assert.ok((await p.evaluate("document.querySelector('.ovda-step').textContent")).includes('Run the checkout tests'), 'the current step'));await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min'));await eventually(async () => assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end', 'float'], 'the owner\'s four controls (S5), and Float while the screen plays (4.154.0)'));await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list'));@@@ -387,6 +387,15 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));}++ // A signed-in destination starts the shared guide only when an opaque step link is present.++ {++ const { navbar } = loadNavbar({ hostname: 'openvibe.chat', href: 'https://openvibe.chat/?qstep=abc123def456' });++ const scripts = [];++ document.head.appendChild = (el) => scripts.push(el.src);++ navbar.init({ service: 'chat', token: 'person-token', user: USER });++ assert.strictEqual(scripts.filter((src) => /guide\.js/.test(src)).length, 1);++ }++// The top-level SDK route reads Network's first-party session even where third-party cookies are blocked; a guest// comes back with sso=none. It runs when there is a reason to think the person has a session: this site signed them// in before ('account'), or they came from another OpenVibe site. A first visit from elsewhere keeps the quiet@@@ -3,14 +3,16 @@// OpenVibe.Live home card, marks the next step, tolerates a missing `more`, and redraws on refresh. The guide's// own test stays here too: it points at a hooked control and finishes on its event.const assert = require('node:assert/strict');++const fs = require('node:fs');++const vm = require('node:vm');const { parseHTML } = require('linkedom');--const { startQuestGuide } = require('../guide');++const { startQuestGuide, startFromLocation } = require('../guide');const files = require('../files');const { window, document } = parseHTML('<!doctype html><html><head></head><body></body></html>');global.window = window;global.document = document;--const { mount } = require('../quest');++const { mount, mountAll } = require('../quest');const step = (site, sentence, extra = {}) => ({ site: `openvibe.${site}`, sentence, href: `https://openvibe.${site}/`, ...extra });const quest = (extra = {}) => ({@@@ -29,7 +31,14 @@ function host() {(async () => {assert.ok(files.isBrowserFile('quest.js'), 'sites serve quest.js');++ assert.ok(files.isBrowserFile('guide.js'), 'sites serve guide.js');assert.ok(files.isBrowserFile('quest.css'), 'sites serve quest.css');++ const declarativeHost = host();++ declarativeHost.setAttribute('data-ov-quest', '');++ const [declarative] = mountAll({ load: async () => ({ signed_in: false, quest: quest() }) });++ assert.strictEqual(declarative, declarativeHost._ovq);++ await declarative.refresh();++ assert.equal(declarative.el.hidden, false);// Signed out: today's quest, the three doors, no streak and no progress.const out = mount(host(), { load: async () => ({ signed_in: false, quest: quest(), more: { quests: 24, sites: 8 } }) });@@@ -144,5 +153,38 @@ function host() {stop();assert.equal(note.removed, true);++ let requested = null;++ const stopFromLink = await startFromLocation({++ location: { href: 'https://openvibe.pics/?qstep=AbC_0123456789' }, document: guideRoot,++ load: async (id) => { requested = id; return { step: { hook: 'pics.upload', help: 'Choose a picture', event_type: 'mediahub.picture.uploaded' } }; },++ });++ assert.equal(requested, 'AbC_0123456789');++ assert.equal(control.arrived, true);++ stopFromLink();++ let mountedLater = false;++ const delayedRoot = { ...guideRoot, querySelector: () => mountedLater ? control : null,++ documentElement: {}, defaultView: { MutationObserver: class {++ constructor(notify) { this.notify = notify; }++ observe() { mountedLater = true; this.notify(); }++ disconnect() {}++ } } };++ const stopDelayed = await startFromLocation({ location: { href: 'https://openvibe.pics/?qstep=AbC_0123456789' },++ document: delayedRoot, load: async () => ({ step: { hook: 'pics.upload', help: 'Choose a picture', event_type: 'mediahub.picture.uploaded' } }) });++ assert.equal(mountedLater, true);++ stopDelayed();++ let observed = false;++ const stopPolled = startQuestGuide({ root: guideRoot,++ step: { hook: 'pics.upload', help: 'Choose a picture', event_type: 'mediahub.picture.uploaded' },++ checkDone: async () => ({ step: { done: true } }), pollMs: 1, onDone: () => { observed = true; } });++ await new Promise((resolve) => setTimeout(resolve, 10));++ assert.equal(observed, true, 'the guide follows the Events-backed step read');++ stopPolled();++ requested = null;++ await startFromLocation({ location: { href: 'https://openvibe.pics/?qstep=plain text' }, document: guideRoot, load: async (id) => { requested = id; } });++ assert.equal(requested, null);++ const browser = { document: guideRoot };++ vm.runInNewContext(fs.readFileSync(require.resolve('../guide'), 'utf8'), { window: browser });++ assert.equal(typeof browser.OpenVibeQuestGuide.startFromLocation, 'function');++process.stdout.write('quest card and guide passed\n');})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });
@@ -1,5 +1,15 @@
@@ -1,5 +1,15 @@
# Changelog
# Changelog
+## 4.209.0 — 2026-11-11
+
+**The Quest guide on any page, and the current Live step in the Dock's agent view.**
+
+- **The Quest guide for browser pages (#464).** Pages can start the Quest guide from an opaque `qstep` link once the host
+ resolves the step; it waits for late controls and checks the Events-backed completion read, and the shared navbar
+ starts it from its own sign-in.
+- **The Dock's agent view shows the current Live step (#466)** when the stream supplies one; older streams keep showing
+ the task without a step line.
+
## 4.208.0 — 2026-11-11
## 4.208.0 — 2026-11-11
**One move engine for every floating thing: the picture-in-picture's drag, flick, tuck, pinch, corner and place, now
**One move engine for every floating thing: the picture-in-picture's drag, flick, tuck, pinch, corner and place, now
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
**Install and test:** `npm ci && npm test`
-**Status:** alpha, v4.208.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.209.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,13 +4,14 @@
@@ -4,13 +4,14 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.208.0",
+ "version": "4.209.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)",
"contracts": "openvibe-contracts v0.399.0 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.399.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.147.0",
"sdk": "openvibe-sdk v0.147.0",
"features": [
"features": [
+ "the Quest guide on any page and the current Live step in the Dock's agent view (4.209.0)",
"one move engine for a floating thing: OVFloat.attach gives the PiP's drag, flick, tuck and kept place to any box, and a panel window adopts it (4.208.0)",
"one move engine for a floating thing: OVFloat.attach gives the PiP's drag, flick, tuck and kept place to any box, and a panel window adopts it (4.208.0)",
"the menu Feed searches Community with examples and uses the one composer for pictures and files; the Chat summary opens the Vibe tab (4.206.0)",
"the menu Feed searches Community with examples and uses the one composer for pictures and files; the Chat summary opens the Vibe tab (4.206.0)",
"the one step view reads AG-UI (phases, titled steps, sources, thinking, compact groups, card mode) with the one composer to talk to a run, and crawlable first HTML for every site (4.205.0)",
"the one step view reads AG-UI (phases, titled steps, sources, thinking, compact groups, card mode) with the one composer to talk to a run, and crawlable first HTML for every site (4.205.0)",
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
// 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.
//
//
// Nothing live: the last frame through the same API (an <img> cannot carry a token), with the task and the worked time.
// Nothing live: the last frame through the same API (an <img> cannot carry a token), with the task and the worked time.
-// There is no step line yet: steps are Agent Streams S2, and this view shows none until Live serves them.
+// Live's S2 current step comes through Codes when available; an older stream simply has no step line.
// Everything is set as text, never markup; the frame's address is the one Codes answered, as an https Live address.
// Everything is set as text, never markup; the frame's address is the one Codes answered, as an https Live address.
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
(function (root) {
(function (root) {
@@ -97,10 +97,11 @@
@@ -97,10 +97,11 @@
container.classList.add('ovda');
container.classList.add('ovda');
const stage = h('div', 'ovda-stage');
const stage = h('div', 'ovda-stage');
const line = h('div', 'ovda-line');
const line = h('div', 'ovda-line');
+ const step = h('div', 'ovda-meta ovda-step');
const meta = h('div', 'ovda-meta');
const meta = h('div', 'ovda-meta');
const acts = h('div', 'ovda-acts');
const acts = h('div', 'ovda-acts');
const head = h('div', 'ovda-head');
const head = h('div', 'ovda-head');
- head.append(line, meta, acts);
+ head.append(line, meta, step, acts);
const body = h('div');
const body = h('div');
body.append(stage, head);
body.append(stage, head);
container.append(body);
container.append(body);
@@ -178,6 +179,11 @@
@@ -178,6 +179,11 @@
else title.textContent = text;
else title.textContent = text;
line.append(title);
line.append(title);
+ const current = agent.current_step;
+ step.textContent = current && typeof current.title === 'string' && current.title.trim()
+ ? `Step: ${current.title} · ${current.status || 'started'}` : '';
+ step.hidden = !step.textContent;
+
// The meta: the state, the viewers, the curtain, the worked time.
// The meta: the state, the viewers, the curtain, the worked time.
meta.textContent = '';
meta.textContent = '';
const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];
const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];
@@ -89,7 +89,7 @@
@@ -89,7 +89,7 @@
}
}
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
- const DOCK_REV = { 'dock-vibe.js': '061fd680f4f7', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': '8ed09c416a48', 'dock-agent.js': 'c1281ef69ea8', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };
+ const DOCK_REV = { 'dock-vibe.js': '061fd680f4f7', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': '8ed09c416a48', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };
function load(src, test) {
function load(src, test) {
return new Promise((ok) => {
return new Promise((ok) => {
if (test()) return ok(true);
if (test()) return ok(true);
@@ -73,6 +73,7 @@ const BROWSER = Object.freeze([
@@ -73,6 +73,7 @@ const BROWSER = Object.freeze([
'items.js', // what people wear (OpenVibe.Inventory name effects, plan T21); draw with items.css
'items.js', // what people wear (OpenVibe.Inventory name effects, plan T21); draw with items.css
'profile-panel.js', // a person's stuff on any site (worn items, badges, achievements), from the Network profile
'profile-panel.js', // a person's stuff on any site (worn items, badges, achievements), from the Network profile
'quest.js', // the daily quest card any site can show (OpenVibeQuest.mount); draw with quest.css
'quest.js', // the daily quest card any site can show (OpenVibeQuest.mount); draw with quest.css
+ 'guide.js', // the opaque quest step guide (OpenVibeQuestGuide.startFromLocation)
'tooltip.js',
'tooltip.js',
'player.js', // the network's one video player look: enhances video[data-ov-player]; draw with player.css
'player.js', // the network's one video player look: enhances video[data-ov-player]; draw with player.css
'ov-sync.js', // one timeline for many players (seven domains B4): OVSync.attach(media, { room, socket }); no dependencies
'ov-sync.js', // one timeline for many players (seven domains B4): OVSync.attach(media, { room, socket }); no dependencies
@@ -1,7 +1,9 @@
@@ -1,7 +1,9 @@
'use strict';
'use strict';
+(function (root) {
+
/** Point at a real hooked control without activating it or covering it. The host owns pointer styling. */
/** Point at a real hooked control without activating it or covering it. The host owns pointer styling. */
-function startQuestGuide({ step, nextHref, root = document, onDone = () => {} } = {}) {
+function startQuestGuide({ step, nextHref, root = document, onDone = () => {}, checkDone = null, pollMs = 5000 } = {}) {
if (!step || !/^[a-z][a-z0-9_.-]{1,63}$/.test(step.hook || '') || !step.event_type) return () => {};
if (!step || !/^[a-z][a-z0-9_.-]{1,63}$/.test(step.hook || '') || !step.event_type) return () => {};
const control = root.querySelector(`[data-ov-guide="${CSS.escape(step.hook)}"]`);
const control = root.querySelector(`[data-ov-guide="${CSS.escape(step.hook)}"]`);
if (!control) return () => {};
if (!control) return () => {};
@@ -15,6 +17,7 @@ function startQuestGuide({ step, nextHref, root = document, onDone = () => {} }
@@ -15,6 +17,7 @@ function startQuestGuide({ step, nextHref, root = document, onDone = () => {} }
const done = (event) => {
const done = (event) => {
if (finished || !event || event.detail?.event_type !== step.event_type) return;
if (finished || !event || event.detail?.event_type !== step.event_type) return;
finished = true;
finished = true;
+ if (poll) clearInterval(poll);
note.replaceChildren(root.createTextNode('Done. '));
note.replaceChildren(root.createTextNode('Done. '));
if (nextHref) {
if (nextHref) {
const link = root.createElement('a'); link.href = nextHref; link.textContent = 'Next step'; note.append(link);
const link = root.createElement('a'); link.href = nextHref; link.textContent = 'Next step'; note.append(link);
@@ -22,12 +25,40 @@ function startQuestGuide({ step, nextHref, root = document, onDone = () => {} }
@@ -22,12 +25,40 @@ function startQuestGuide({ step, nextHref, root = document, onDone = () => {} }
onDone(step);
onDone(step);
};
};
root.addEventListener('ov:quest:step-done', done);
root.addEventListener('ov:quest:step-done', done);
+ const poll = typeof checkDone === 'function' && !step.done ? setInterval(async () => {
+ try { const latest = await checkDone(); if (latest?.step?.done) done({ detail: { event_type: step.event_type } }); }
+ catch { /* the Events receipt may still be arriving; the next read retries */ }
+ }, pollMs) : null;
+ if (poll && typeof poll.unref === 'function') poll.unref();
const dismiss = (event) => {
const dismiss = (event) => {
if (event.key === 'Escape' || (event.type === 'pointerdown' && !note.contains(event.target) && !control.contains(event.target))) stop();
if (event.key === 'Escape' || (event.type === 'pointerdown' && !note.contains(event.target) && !control.contains(event.target))) stop();
};
};
- function stop() { root.removeEventListener('ov:quest:step-done', done); root.removeEventListener('keydown', dismiss); root.removeEventListener('pointerdown', dismiss); note.remove(); }
+ function stop() { if (poll) clearInterval(poll); root.removeEventListener('ov:quest:step-done', done); root.removeEventListener('keydown', dismiss); root.removeEventListener('pointerdown', dismiss); note.remove(); }
root.addEventListener('keydown', dismiss); root.addEventListener('pointerdown', dismiss);
root.addEventListener('keydown', dismiss); root.addEventListener('pointerdown', dismiss);
+ if (step.done) done({ detail: { event_type: step.event_type } });
return stop;
return stop;
}
}
-module.exports = { startQuestGuide };
+/** Resolve only a short opaque step id. The host supplies an authenticated reader. */
+async function startFromLocation({ load, location = root.location, document: page = root.document, onDone } = {}) {
+ if (typeof load !== 'function' || !location || !page) return () => {};
+ const id = new URL(location.href).searchParams.get('qstep');
+ if (!id || !/^[A-Za-z0-9_-]{12,48}$/.test(id)) return () => {};
+ const answer = await load(id);
+ if (!answer || !answer.step || !answer.step.hook) return () => {};
+ // Many sites render the target after their page data arrives. Watch briefly instead of losing the guide.
+ const selector = `[data-ov-guide="${CSS.escape(answer.step.hook)}"]`;
+ if (!page.querySelector(selector) && page.defaultView?.MutationObserver) await new Promise((resolve) => {
+ const observer = new page.defaultView.MutationObserver(() => {
+ if (page.querySelector(selector)) { observer.disconnect(); clearTimeout(timer); resolve(); }
+ });
+ const timer = setTimeout(() => { observer.disconnect(); resolve(); }, 15000);
+ observer.observe(page.documentElement, { childList: true, subtree: true });
+ });
+ return startQuestGuide({ step: answer.step, nextHref: answer.next_href, root: page, onDone, checkDone: () => load(id) });
+}
+
+const api = { startQuestGuide, startFromLocation };
+if (typeof module === 'object' && module.exports) module.exports = api;
+if (root && root.document) root.OpenVibeQuestGuide = api;
+})(typeof window !== 'undefined' ? window : globalThis);
@@ -607,7 +607,7 @@
@@ -607,7 +607,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '0dbfc679c2a2', 'dock.js': '2dc02f5569ac', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4b39796c5145', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': 'cdacf9b1154b', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };
+ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': '771efd0e3963', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': '0dbfc679c2a2', 'dock.js': '0ec4ae476c63', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4b39796c5145', 'history.js': 'f9fb9e096c65', 'bottom.js': 'b9f708d04350', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': '52439265b980', 'overlay.js': 'c72c0e31505d', 'ov-home.js': 'cdacf9b1154b', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': 'b5a78ad67810', 'ov-wallet.css': 'be8daa24c3a8', 'purse.js': '749120bfd24b', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'soft.js': '98ab9b8080b1' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
// ov-tip.js loads at the first hover over a tooltip's owner.
@@ -1939,6 +1939,26 @@
@@ -1939,6 +1939,26 @@
}
}
}
}
+ let _questGuideStarted = false;
+ function startQuestGuide() {
+ if (_questGuideStarted || !_config.token || !root.location || !/[?&]qstep=/.test(root.location.search || '')) return;
+ _questGuideStarted = true;
+ const script = document.createElement('script');
+ script.src = siblingV('guide.js');
+ script.onload = () => {
+ if (!root.OpenVibeQuestGuide) return;
+ void root.OpenVibeQuestGuide.startFromLocation({ load: async (id) => {
+ try {
+ const response = await fetch(`https://openvibe.quest/api/v1/quests/today/steps/${encodeURIComponent(id)}`, {
+ headers: { Authorization: `Bearer ${_config.token}` }, credentials: 'omit',
+ });
+ return response.ok ? response.json() : null;
+ } catch { return null; }
+ } });
+ };
+ document.head.appendChild(script);
+ }
+
function refreshAuthState() {
function refreshAuthState() {
if (_authInFlight) return _authInFlight;
if (_authInFlight) return _authInFlight;
_authInFlight = resolveSessionUser()
_authInFlight = resolveSessionUser()
@@ -1950,6 +1970,7 @@
@@ -1950,6 +1970,7 @@
render();
render();
recordHistory();
recordHistory();
enableHandoff();
enableHandoff();
+ startQuestGuide();
} else {
} else {
maybeSilentLogin();
maybeSilentLogin();
}
}
@@ -2631,6 +2652,7 @@
@@ -2631,6 +2652,7 @@
if (_config.auth === 'external') { if (_config.user) { recordHistory(); enableHandoff(); } }
if (_config.auth === 'external') { if (_config.user) { recordHistory(); enableHandoff(); } }
else if (!_config.user) refreshAuthState();
else if (!_config.user) refreshAuthState();
else { recordHistory(); enableHandoff(); }
else { recordHistory(); enableHandoff(); }
+ startQuestGuide();
return el;
return el;
},
},
@@ -2688,7 +2710,7 @@
@@ -2688,7 +2710,7 @@
/** The signed-in user changed (sites with auth: 'external'). Pass null for signed out. */
/** The signed-in user changed (sites with auth: 'external'). Pass null for signed out. */
openWallet,
openWallet,
refreshWallet() { _walletAt = 0; loadWalletChips(); },
refreshWallet() { _walletAt = 0; loadWalletChips(); },
- setUser(user, token) { if (root.OpenVibeWallet && (_config.user !== user || (token !== undefined && token !== _config.token))) root.OpenVibeWallet.close(); _config.user = user || null; if (token !== undefined) _config.token = token; _walletLoaded = false; return render(); },
+ setUser(user, token) { if (root.OpenVibeWallet && (_config.user !== user || (token !== undefined && token !== _config.token))) root.OpenVibeWallet.close(); _config.user = user || null; if (token !== undefined) _config.token = token; _walletLoaded = false; const result = render(); startQuestGuide(); return result; },
/** Single-page apps: mark the link whose `page` matches as active. */
/** Single-page apps: mark the link whose `page` matches as active. */
setActive(page) { _activePage = page || null; renderAppBar(); if (!_navEl) return; _navEl.querySelectorAll('.ovnav-link[data-page]').forEach(a => a.classList.toggle('active', a.getAttribute('data-page') === _activePage)); },
setActive(page) { _activePage = page || null; renderAppBar(); if (!_navEl) return; _navEl.querySelectorAll('.ovnav-link[data-page]').forEach(a => a.classList.toggle('active', a.getAttribute('data-page') === _activePage)); },
/** Change a status chip's text (navbar and menu header): setChip('coins', '1,234'). */
/** Change a status chip's text (navbar and menu header): setChip('coins', '1,234'). */
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.208.0",
+ "version": "4.209.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.208.0",
+ "version": "4.209.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.208.0",
+ "version": "4.209.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": {
@@ -191,6 +191,12 @@
@@ -191,6 +191,12 @@
return api;
return api;
}
}
- root.OpenVibeQuest = { mount };
- if (typeof module === 'object' && module.exports) module.exports = { mount };
+ /** Mount every explicit quest host in a page. The site supplies its authenticated reader. */
+ function mountAll({ load, root: page = document } = {}) {
+ if (typeof load !== 'function') throw new TypeError('OpenVibeQuest.mountAll: load() must return the quest data');
+ return [...page.querySelectorAll('[data-ov-quest]')].map((host) => mount(host, { load: () => load(host) }));
+ }
+
+ root.OpenVibeQuest = { mount, mountAll };
+ if (typeof module === 'object' && module.exports) module.exports = { mount, mountAll };
})(typeof window !== 'undefined' ? window : globalThis);
})(typeof window !== 'undefined' ? window : globalThis);
@@ -460,6 +460,7 @@ async function drag(p, from, to) {
@@ -460,6 +460,7 @@ async function drag(p, from, to) {
const view = () => ({
const view = () => ({
id: AGENT_ID, state: 'live', title: 'Fix the flaky upload test',
id: AGENT_ID, state: 'live', title: 'Fix the flaky upload test',
task: { source: 'codes', ref: 'tsk_1', title: 'Fix the flaky upload test', url: 'https://openvibe.codes/run/tasks/tsk_1' },
task: { source: 'codes', ref: 'tsk_1', title: 'Fix the flaky upload test', url: 'https://openvibe.codes/run/tasks/tsk_1' },
+ current_step: { title: 'Run the checkout tests', status: 'started' },
curtain: { up: false }, viewers: 2, worked_seconds: 720, visibility: 'private',
curtain: { up: false }, viewers: 2, worked_seconds: 720, visibility: 'private',
page_url: `https://openvibe.live/@ada/agents/${AGENT_ID}`,
page_url: `https://openvibe.live/@ada/agents/${AGENT_ID}`,
embed_url: `https://openvibe.live/embed/agent/${AGENT_ID}?vt=wvt_test`, last_frame_url: null,
embed_url: `https://openvibe.live/embed/agent/${AGENT_ID}?vt=wvt_test`, last_frame_url: null,
@@ -475,6 +476,7 @@ async function drag(p, from, to) {
@@ -475,6 +476,7 @@ async function drag(p, from, to) {
const src = await until(p, "document.querySelector('.ovda iframe') && document.querySelector('.ovda iframe').getAttribute('src')", 'the embed');
const src = await until(p, "document.querySelector('.ovda iframe') && document.querySelector('.ovda iframe').getAttribute('src')", 'the embed');
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');
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line'));
+ await eventually(async () => assert.ok((await p.evaluate("document.querySelector('.ovda-step').textContent")).includes('Run the checkout tests'), 'the current step'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min'));
await eventually(async () => assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end', 'float'], 'the owner\'s four controls (S5), and Float while the screen plays (4.154.0)'));
await eventually(async () => assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end', 'float'], 'the owner\'s four controls (S5), and Float while the screen plays (4.154.0)'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list'));
await eventually(async () => assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list'));
@@ -387,6 +387,15 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
@@ -387,6 +387,15 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));
}
}
+ // A signed-in destination starts the shared guide only when an opaque step link is present.
+ {
+ const { navbar } = loadNavbar({ hostname: 'openvibe.chat', href: 'https://openvibe.chat/?qstep=abc123def456' });
+ const scripts = [];
+ document.head.appendChild = (el) => scripts.push(el.src);
+ navbar.init({ service: 'chat', token: 'person-token', user: USER });
+ assert.strictEqual(scripts.filter((src) => /guide\.js/.test(src)).length, 1);
+ }
+
// The top-level SDK route reads Network's first-party session even where third-party cookies are blocked; a guest
// The top-level SDK route reads Network's first-party session even where third-party cookies are blocked; a guest
// comes back with sso=none. It runs when there is a reason to think the person has a session: this site signed them
// comes back with sso=none. It runs when there is a reason to think the person has a session: this site signed them
// in before ('account'), or they came from another OpenVibe site. A first visit from elsewhere keeps the quiet
// in before ('account'), or they came from another OpenVibe site. A first visit from elsewhere keeps the quiet
@@ -3,14 +3,16 @@
@@ -3,14 +3,16 @@
// OpenVibe.Live home card, marks the next step, tolerates a missing `more`, and redraws on refresh. The guide's
// OpenVibe.Live home card, marks the next step, tolerates a missing `more`, and redraws on refresh. The guide's
// own test stays here too: it points at a hooked control and finishes on its event.
// own test stays here too: it points at a hooked control and finishes on its event.
const assert = require('node:assert/strict');
const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const vm = require('node:vm');
const { parseHTML } = require('linkedom');
const { parseHTML } = require('linkedom');
-const { startQuestGuide } = require('../guide');
+const { startQuestGuide, startFromLocation } = require('../guide');
const files = require('../files');
const files = require('../files');
const { window, document } = parseHTML('<!doctype html><html><head></head><body></body></html>');
const { window, document } = parseHTML('<!doctype html><html><head></head><body></body></html>');
global.window = window;
global.window = window;
global.document = document;
global.document = document;
-const { mount } = require('../quest');
+const { mount, mountAll } = require('../quest');
const step = (site, sentence, extra = {}) => ({ site: `openvibe.${site}`, sentence, href: `https://openvibe.${site}/`, ...extra });
const step = (site, sentence, extra = {}) => ({ site: `openvibe.${site}`, sentence, href: `https://openvibe.${site}/`, ...extra });
const quest = (extra = {}) => ({
const quest = (extra = {}) => ({
@@ -29,7 +31,14 @@ function host() {
@@ -29,7 +31,14 @@ function host() {
(async () => {
(async () => {
assert.ok(files.isBrowserFile('quest.js'), 'sites serve quest.js');
assert.ok(files.isBrowserFile('quest.js'), 'sites serve quest.js');
+ assert.ok(files.isBrowserFile('guide.js'), 'sites serve guide.js');
assert.ok(files.isBrowserFile('quest.css'), 'sites serve quest.css');
assert.ok(files.isBrowserFile('quest.css'), 'sites serve quest.css');
+ const declarativeHost = host();
+ declarativeHost.setAttribute('data-ov-quest', '');
+ const [declarative] = mountAll({ load: async () => ({ signed_in: false, quest: quest() }) });
+ assert.strictEqual(declarative, declarativeHost._ovq);
+ await declarative.refresh();
+ assert.equal(declarative.el.hidden, false);
// Signed out: today's quest, the three doors, no streak and no progress.
// Signed out: today's quest, the three doors, no streak and no progress.
const out = mount(host(), { load: async () => ({ signed_in: false, quest: quest(), more: { quests: 24, sites: 8 } }) });
const out = mount(host(), { load: async () => ({ signed_in: false, quest: quest(), more: { quests: 24, sites: 8 } }) });
@@ -144,5 +153,38 @@ function host() {
@@ -144,5 +153,38 @@ function host() {
stop();
stop();
assert.equal(note.removed, true);
assert.equal(note.removed, true);
+ let requested = null;
+ const stopFromLink = await startFromLocation({
+ location: { href: 'https://openvibe.pics/?qstep=AbC_0123456789' }, document: guideRoot,
+ load: async (id) => { requested = id; return { step: { hook: 'pics.upload', help: 'Choose a picture', event_type: 'mediahub.picture.uploaded' } }; },
+ });
+ assert.equal(requested, 'AbC_0123456789');
+ assert.equal(control.arrived, true);
+ stopFromLink();
+ let mountedLater = false;
+ const delayedRoot = { ...guideRoot, querySelector: () => mountedLater ? control : null,
+ documentElement: {}, defaultView: { MutationObserver: class {
+ constructor(notify) { this.notify = notify; }
+ observe() { mountedLater = true; this.notify(); }
+ disconnect() {}
+ } } };
+ const stopDelayed = await startFromLocation({ location: { href: 'https://openvibe.pics/?qstep=AbC_0123456789' },
+ document: delayedRoot, load: async () => ({ step: { hook: 'pics.upload', help: 'Choose a picture', event_type: 'mediahub.picture.uploaded' } }) });
+ assert.equal(mountedLater, true);
+ stopDelayed();
+ let observed = false;
+ const stopPolled = startQuestGuide({ root: guideRoot,
+ step: { hook: 'pics.upload', help: 'Choose a picture', event_type: 'mediahub.picture.uploaded' },
+ checkDone: async () => ({ step: { done: true } }), pollMs: 1, onDone: () => { observed = true; } });
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ assert.equal(observed, true, 'the guide follows the Events-backed step read');
+ stopPolled();
+ requested = null;
+ await startFromLocation({ location: { href: 'https://openvibe.pics/?qstep=plain text' }, document: guideRoot, load: async (id) => { requested = id; } });
+ assert.equal(requested, null);
+ const browser = { document: guideRoot };
+ vm.runInNewContext(fs.readFileSync(require.resolve('../guide'), 'utf8'), { window: browser });
+ assert.equal(typeof browser.OpenVibeQuestGuide.startFromLocation, 'function');
+
process.stdout.write('quest card and guide passed\n');
process.stdout.write('quest card and guide passed\n');
})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });
})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });