openvibers/openvibe.media Commits

Show up to four next-step cards after the watch page overview (#105)

OP OpenVibers committed 30 days ago · 09f9a5fd91 · 2 files, +44 −1

* Show up to four next-step cards after the watch page overview

Asked: T11 step 2 — OpenVibe.Media: next-step cards on watch pages (D94, the first placement named by Shared docs/adr/0002-traffic-between-products.md). Spec: /home/workstation/openvibe/agents/ds/prep/T11.md, section 'T11 step 2' (lines 125-152), which the plan tag attaches. ONE pull request from current …
Harness job media-t11-step-2-openvibe-media-012352 (plan T11).

* Media: the third next-step card is going live from the browser (no OBS, no follower minimum)

Changes

ddiff --git a/server/public/pages.js b/server/public/pages.js
iindex 0dcb354..7163879 100644
---- a/server/public/pages.js
++++ b/server/public/pages.js
@@@ -15,6 +15,7 @@
const path = require('path');
const config = require('../config');
const pageFrame = require('./page-frame');
++const showcase = require('openvibe-shared/showcase');
const { esc, abs, snip, appUrl, isoDate, isoDuration, fmtDuration, fmtDate, SITE_NAME, NETWORK_URL } = pageFrame;
@@@ -153,6 +154,15 @@ function renderWatchPage(kind, record, readiness = null) {
<p class="state-detail">${esc(stateText)}</p>
</div>`;
++ // Next steps on other OpenVibe products (Shared ADR 0002, D94): real pages that work signed out, no
++ // identity in the URL; the kit keeps at most four and appends ?ov_from=media.
++ const nextSteps = showcase.nextSteps({ id: 'after-watch', title: 'What next?', items: [
++ ownedByLive ? { what: 'Watch on OpenVibe.Live', text: 'Chat, the transcript and the people who were there.', href: canonical, to: 'live', from: 'media' } : null,
++ sourceVod ? { what: 'See the VOD this clip came from', text: 'The full stream, auto-recorded.', href: sourceVod, from: 'media' } : null,
++ playable && !isVod ? { what: 'Go live from your browser', text: 'No OBS and no follower minimum: stream on OpenVibe.Live and clip the best moments.', href: 'https://openvibe.live/', to: 'live', from: 'media' } : null,
++ { what: 'Browse everything on OpenVibe.Media', text: 'Every public video, clip and file on the network.', href: `${config.publicUrl}/`, from: 'media' },
++ ].filter(Boolean) });
++
const body = `
<h1>${esc(title)}</h1>
<p class="meta">${metaBits.map(b => `<span>${b}</span>`).join('<span aria-hidden="true">·</span>')}</p>
@@@ -163,7 +173,8 @@ function renderWatchPage(kind, record, readiness = null) {
${isVod ? `<a class="btn ghost" href="${esc(`${self}/transcript.json`)}">Transcript (JSON)</a>` : ''}
</div>
${record.description ? `<p class="desc">${esc(snip(record.description, 2000))}</p>` : ''}
-- ${record.ai_overview && record.ai_overview !== record.description ? `<section><h2 class="h2">Overview</h2><p class="desc">${esc(snip(record.ai_overview, 3000))}</p></section>` : ''}`;
++ ${record.ai_overview && record.ai_overview !== record.description ? `<section><h2 class="h2">Overview</h2><p class="desc">${esc(snip(record.ai_overview, 3000))}</p></section>` : ''}
++ ${nextSteps}`;
const css = `
.player { background: #000; border-radius: 12px; overflow: hidden; border: 1px solid var(--line); margin: 0 0 1rem; }
ddiff --git a/test/public-pages.test.js b/test/public-pages.test.js
iindex 99d9d35..8e7e5d5 100644
---- a/test/public-pages.test.js
++++ b/test/public-pages.test.js
@@@ -73,6 +73,38 @@ const http = require('http');
assert.ok(html.includes('"@type":"WebPage"') && html.includes('"url":"https://openvibe.live/vod/42"'), 'its WebPage names the canonical');
assert.ok(!unlisted.includes('name="ai-summary"'), 'unlisted pages carry none');
++ // ── Next-step cards (Shared ADR 0002, D94): real, signed-out targets, ov_from=media, at most four ──
++ const config = require('../server/config');
++ const cards = (page) => [...page.matchAll(/<a class="sc-next-main" href="([^"]+)"/g)].map((m) => m[1].replace(/&amp;/g, '&'));
++ const checkCards = (page, label) => {
++ const hrefs = cards(page);
++ assert.ok(hrefs.length >= 1 && hrefs.length <= 4, `${label}: one to four cards (${hrefs.length})`);
++ hrefs.forEach((href) => {
++ assert.ok(/^https:\/\/openvibe\.[a-z]+(\/|$)/.test(href) || href.startsWith('/') || href.startsWith(`${config.publicUrl}/`),
++ `${label}: ${href} is an OpenVibe zone, site-relative or this site`);
++ assert.strictEqual(new URL(href, config.publicUrl).search, '?ov_from=media', `${label}: ${href} carries ov_from=media and no other query`);
++ });
++ return hrefs;
++ };
++ // A playable public Live-owned VOD: the Live card (the canonical) and the index; no clip-cutting card on a VOD.
++ assert.ok(html.includes('class="sc-sec sc-next"') && html.includes('<ol class="sc-next-list">'), 'a playable public VOD page carries the next-step cards');
++ assert.deepStrictEqual(checkCards(html, 'Live VOD'), ['https://openvibe.live/vod/42?ov_from=media', 'https://media.test/?ov_from=media'],
++ 'a Live-owned item\'s card is its canonical; the index card is always there');
++ // A VOD another app owns: only the index.
++ assert.deepStrictEqual(checkCards(pages.renderWatchPage('vod', { ...vod, id: 43, app_id: 'games' }), 'games VOD'), ['https://media.test/?ov_from=media']);
++ // A playable person-cut clip on Live: Live, cut your own, the index.
++ assert.deepStrictEqual(checkCards(pages.renderWatchPage('clip', { ...vod, id: 9 }), 'Live clip'),
++ ['https://openvibe.live/clip/9?ov_from=media', 'https://openvibe.live/?ov_from=media', 'https://media.test/?ov_from=media']);
++ // An AI clip with a source VOD: every card applies, and four is the most that render.
++ const aiClip = pages.renderWatchPage('clip', { ...vod, id: 10, auto_generated: 1, vod_id: 42 });
++ assert.deepStrictEqual(checkCards(aiClip, 'AI clip'), ['https://openvibe.live/clip/10?ov_from=media', 'https://media.test/v/42?ov_from=media',
++ 'https://openvibe.live/?ov_from=media', 'https://media.test/?ov_from=media'], 'a clip with a source VOD shows the VOD card');
++ assert.ok(aiClip.includes('See the VOD this clip came from'));
++ assert.strictEqual((aiClip.match(/class="sc-next-item"/g) || []).length, 4, 'at most four cards');
++ // A clip without a player offers no clip-cutting card.
++ const pending = pages.renderWatchPage('clip', { ...vod, id: 11 }, { playable: false, reason: 'processing' });
++ assert.ok(!cards(pending).includes('https://openvibe.live/?ov_from=media'), 'no "go live" card without a player');
++
// Paste pages are Community's: there is no viewer here (public/routes.js only redirects).
assert.ok(!/\bfree\b/i.test(html + pageFrame.baseCss()), 'no cost claims in served HTML');