openvibers/openvibe.live Commits
The Rundown says what you can do with it: catch up on a stream, follow a topic, jump to the moment, get written up (a viewer: "still don't rly know wtf to do with this feature") (#270)
Changes
@@@ -13,6 +13,17 @@.rd-tab:hover { color: var(--text-primary); border-color: var(--border); }.rd-tab.is-on { color: var(--text-primary); background: var(--bg-tertiary); border-color: var(--border); }++/* What you can do here (rundown.js _rdUses): four entry points under the front page's masthead */++.rd-uses { list-style: none; margin: 0 0 18px; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }++.rd-use { display: flex; gap: 10px; align-items: flex-start; height: 100%; padding: 12px; border-radius: 14px; background: var(--bg-card);++ border: 1px solid var(--border); color: var(--text-secondary); text-decoration: none; line-height: 1.35; }++a.rd-use:hover { border-color: var(--accent); }++a.rd-use:hover b { color: var(--accent); }++.rd-use i { flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; font-size: 0.92rem;++ background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }++.rd-use b { display: block; margin-bottom: 2px; color: var(--text-primary); font-size: 0.95rem; }++.rd-use small { display: block; font-size: 0.85rem; }++/* Headings, layout */.rd-h1 { margin: 6px 0 8px; font-size: clamp(1.7rem, 5vw, 2.6rem); line-height: 1.08; font-weight: 900; letter-spacing: -0.02em; }.rd-h2 { display: flex; align-items: baseline; gap: 8px; margin: 26px 0 12px; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); }@@@ -195,10 +206,17 @@.rd-topic-card .rd-what { margin: 0; font-size: 0.94rem; color: var(--text-secondary); }@media (max-width: 960px) {++ .rd-uses { grid-template-columns: repeat(2, minmax(0, 1fr)); }.rd-grid { grid-template-columns: minmax(0, 1fr); }.rd-side { position: static; }}@media (max-width: 560px) {++ /* Two by two, titles only: the four lines of text would push the first episode below the fold. */++ .rd-uses { gap: 8px; margin-bottom: 14px; }++ .rd-use { align-items: center; padding: 9px 10px; gap: 8px; }++ .rd-use i { width: 28px; height: 28px; font-size: 0.8rem; }++ .rd-use b { margin: 0; font-size: 0.86rem; }++ .rd-use small { display: none; }.rd-card { padding: 12px 13px; }.rd-lead { padding: 14px; }.rd-issue-head { padding: 12px 13px 10px; }@@@ -177,6 +177,21 @@ function _rdMast(counts, active = 'latest') {<nav class="rd-tabs" aria-label="The Rundown">${tab('latest', '/rundown', 'Latest')}${tab('topics', '/rundown/topics', 'Topics')}</nav></header>`;}++// What a visitor can do here, under the masthead of the front page. The page said what it is and never what it is for++// ("still don't rly know wtf to do with this feature", a viewer, 2026-10-20). "Jump to the moment" opens the top take's.++function _rdUses(lead) {++ const item = (icon, title, text, href) => {++ const body = `<i class="fa-solid ${icon}" aria-hidden="true"></i><span><b>${title}</b><small>${text}</small></span>`;++ if (!href) return `<li><div class="rd-use">${body}</div></li>`;++ return `<li>${href.startsWith('#') ? `<a class="rd-use" href="${href}">${body}</a>` : _rdA(href, body, 'rd-use')}</li>`;++ };++ return `<ul class="rd-uses" aria-label="What you can do here">++ ${item('fa-forward', 'Catch up on a stream', 'Every stream becomes an episode: a recap, chapters and what was said.', '#rd-latest-h')}++ ${item('fa-hashtag', 'Follow a topic', 'See every streamer who talked about it this week, side by side.', '/rundown/topics')}++ ${item('fa-play', 'Jump to the moment', 'A take with a recording links to the second it was said.', lead && lead.vod_id ? videoHref('vod', lead.vod_id, Math.max(0, Math.floor(((lead.quotes || [])[0] || lead).sec || 0))) : null)}++ ${item('fa-tower-broadcast', 'Get written up', 'Go live, and the desk turns your stream into an episode here.', '/broadcast')}++ </ul>`;++}function _rdOnAir(list) {if (!list.length) return '';return `<section class="rd-onair" aria-labelledby="rd-onair-h"><h2 id="rd-onair-h" class="rd-h2"><span class="rd-dot" aria-hidden="true"></span> On air now</h2>@@@ -207,7 +222,7 @@ async function _rdHome(root) {const d = await api('/rundown');_rdNext = d.streams_next;const feed = d.streams || [];-- root.innerHTML = `${_rdMast(d.counts, 'latest')}${_rdOnAir(d.on_air || [])}++ root.innerHTML = `${_rdMast(d.counts, 'latest')}${_rdUses(d.lead)}${_rdOnAir(d.on_air || [])}<div class="rd-grid"><div class="rd-main">${d.lead ? _rdLead(d.lead) : ''}nnew file mode 100644@@@ -0,0 +1,44 @@++/**++ * The Rundown's front page says what a visitor can do there (public/js/rundown.js _rdUses): catch up on a stream (the++ * stream-by-stream list), follow a topic (/rundown/topics), jump to the moment (the top take's second on OpenVibe.Video)++ * and get written up (/broadcast). A viewer said of the page "still don't rly know wtf to do with this feature"++ * (2026-10-20): it said what it is, never what it is for.++ *++ * node test/rundown-uses.test.js++ */++'use strict';++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const vm = require('vm');++++const js = fs.readFileSync(path.join(__dirname, '../public/js/rundown.js'), 'utf8');++const css = fs.readFileSync(path.join(__dirname, '../public/css/features/rundown.css'), 'utf8');++const pick = (name) => {++ const start = js.indexOf(`function ${name}(`);++ assert.ok(start >= 0, `${name} is in rundown.js`);++ const next = js.indexOf('\nfunction ', start + 1);++ return js.slice(start, next < 0 ? undefined : next);++};++++const ctx = { videoHref: (kind, id, t) => `https://openvibe.video/${kind === 'clip' ? 'c' : 'v'}/${id}${t ? `?t=${t}` : ''}` };++vm.createContext(ctx);++vm.runInContext([pick('_rdEsc'), pick('_rdA'), pick('_rdUses')].join('\n'), ctx);++++const links = (html) => [...html.matchAll(/<a class="rd-use" href="([^"]+)"/g)].map((m) => m[1]);++const titles = (html) => [...html.matchAll(/<b>([^<]+)<\/b>/g)].map((m) => m[1]);++++const withLead = ctx._rdUses({ id: 7, vod_id: 5855, sec: 40, quotes: [{ text: 'x', sec: 95.6 }] });++assert.deepStrictEqual(titles(withLead), ['Catch up on a stream', 'Follow a topic', 'Jump to the moment', 'Get written up']);++assert.deepStrictEqual(links(withLead), ['#rd-latest-h', '/rundown/topics', 'https://openvibe.video/v/5855?t=95', '/broadcast'],++ 'each entry goes somewhere: the list, the topics, the top take\'s moment (its quote\'s second), going live');++assert.ok(withLead.includes('aria-label="What you can do here"'));++++const noLead = ctx._rdUses(null);++assert.deepStrictEqual(links(noLead), ['#rd-latest-h', '/rundown/topics', '/broadcast'], 'no recording to jump into: that entry is text, not a dead link');++assert.ok(noLead.includes('<div class="rd-use"><i class="fa-solid fa-play"'));++++assert.match(js, /root\.innerHTML = `\$\{_rdMast\(d\.counts, 'latest'\)\}\$\{_rdUses\(d\.lead\)\}/, 'the front page shows it under the masthead');++assert.match(css, /@media \(max-width: 560px\) \{[^}]*\.rd-uses \{[^}]*\}[\s\S]*?\.rd-use small \{ display: none; \}/, 'phones show titles only, two by two');++++console.log('rundown uses: all checks passed');
@@ -13,6 +13,17 @@
@@ -13,6 +13,17 @@
.rd-tab:hover { color: var(--text-primary); border-color: var(--border); }
.rd-tab:hover { color: var(--text-primary); border-color: var(--border); }
.rd-tab.is-on { color: var(--text-primary); background: var(--bg-tertiary); border-color: var(--border); }
.rd-tab.is-on { color: var(--text-primary); background: var(--bg-tertiary); border-color: var(--border); }
+/* What you can do here (rundown.js _rdUses): four entry points under the front page's masthead */
+.rd-uses { list-style: none; margin: 0 0 18px; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
+.rd-use { display: flex; gap: 10px; align-items: flex-start; height: 100%; padding: 12px; border-radius: 14px; background: var(--bg-card);
+ border: 1px solid var(--border); color: var(--text-secondary); text-decoration: none; line-height: 1.35; }
+a.rd-use:hover { border-color: var(--accent); }
+a.rd-use:hover b { color: var(--accent); }
+.rd-use i { flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; font-size: 0.92rem;
+ background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
+.rd-use b { display: block; margin-bottom: 2px; color: var(--text-primary); font-size: 0.95rem; }
+.rd-use small { display: block; font-size: 0.85rem; }
+
/* Headings, layout */
/* Headings, layout */
.rd-h1 { margin: 6px 0 8px; font-size: clamp(1.7rem, 5vw, 2.6rem); line-height: 1.08; font-weight: 900; letter-spacing: -0.02em; }
.rd-h1 { margin: 6px 0 8px; font-size: clamp(1.7rem, 5vw, 2.6rem); line-height: 1.08; font-weight: 900; letter-spacing: -0.02em; }
.rd-h2 { display: flex; align-items: baseline; gap: 8px; margin: 26px 0 12px; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); }
.rd-h2 { display: flex; align-items: baseline; gap: 8px; margin: 26px 0 12px; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); }
@@ -195,10 +206,17 @@
@@ -195,10 +206,17 @@
.rd-topic-card .rd-what { margin: 0; font-size: 0.94rem; color: var(--text-secondary); }
.rd-topic-card .rd-what { margin: 0; font-size: 0.94rem; color: var(--text-secondary); }
@media (max-width: 960px) {
@media (max-width: 960px) {
+ .rd-uses { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rd-grid { grid-template-columns: minmax(0, 1fr); }
.rd-grid { grid-template-columns: minmax(0, 1fr); }
.rd-side { position: static; }
.rd-side { position: static; }
}
}
@media (max-width: 560px) {
@media (max-width: 560px) {
+ /* Two by two, titles only: the four lines of text would push the first episode below the fold. */
+ .rd-uses { gap: 8px; margin-bottom: 14px; }
+ .rd-use { align-items: center; padding: 9px 10px; gap: 8px; }
+ .rd-use i { width: 28px; height: 28px; font-size: 0.8rem; }
+ .rd-use b { margin: 0; font-size: 0.86rem; }
+ .rd-use small { display: none; }
.rd-card { padding: 12px 13px; }
.rd-card { padding: 12px 13px; }
.rd-lead { padding: 14px; }
.rd-lead { padding: 14px; }
.rd-issue-head { padding: 12px 13px 10px; }
.rd-issue-head { padding: 12px 13px 10px; }
@@ -177,6 +177,21 @@ function _rdMast(counts, active = 'latest') {
@@ -177,6 +177,21 @@ function _rdMast(counts, active = 'latest') {
<nav class="rd-tabs" aria-label="The Rundown">${tab('latest', '/rundown', 'Latest')}${tab('topics', '/rundown/topics', 'Topics')}</nav>
<nav class="rd-tabs" aria-label="The Rundown">${tab('latest', '/rundown', 'Latest')}${tab('topics', '/rundown/topics', 'Topics')}</nav>
</header>`;
</header>`;
}
}
+// What a visitor can do here, under the masthead of the front page. The page said what it is and never what it is for
+// ("still don't rly know wtf to do with this feature", a viewer, 2026-10-20). "Jump to the moment" opens the top take's.
+function _rdUses(lead) {
+ const item = (icon, title, text, href) => {
+ const body = `<i class="fa-solid ${icon}" aria-hidden="true"></i><span><b>${title}</b><small>${text}</small></span>`;
+ if (!href) return `<li><div class="rd-use">${body}</div></li>`;
+ return `<li>${href.startsWith('#') ? `<a class="rd-use" href="${href}">${body}</a>` : _rdA(href, body, 'rd-use')}</li>`;
+ };
+ return `<ul class="rd-uses" aria-label="What you can do here">
+ ${item('fa-forward', 'Catch up on a stream', 'Every stream becomes an episode: a recap, chapters and what was said.', '#rd-latest-h')}
+ ${item('fa-hashtag', 'Follow a topic', 'See every streamer who talked about it this week, side by side.', '/rundown/topics')}
+ ${item('fa-play', 'Jump to the moment', 'A take with a recording links to the second it was said.', lead && lead.vod_id ? videoHref('vod', lead.vod_id, Math.max(0, Math.floor(((lead.quotes || [])[0] || lead).sec || 0))) : null)}
+ ${item('fa-tower-broadcast', 'Get written up', 'Go live, and the desk turns your stream into an episode here.', '/broadcast')}
+ </ul>`;
+}
function _rdOnAir(list) {
function _rdOnAir(list) {
if (!list.length) return '';
if (!list.length) return '';
return `<section class="rd-onair" aria-labelledby="rd-onair-h"><h2 id="rd-onair-h" class="rd-h2"><span class="rd-dot" aria-hidden="true"></span> On air now</h2>
return `<section class="rd-onair" aria-labelledby="rd-onair-h"><h2 id="rd-onair-h" class="rd-h2"><span class="rd-dot" aria-hidden="true"></span> On air now</h2>
@@ -207,7 +222,7 @@ async function _rdHome(root) {
@@ -207,7 +222,7 @@ async function _rdHome(root) {
const d = await api('/rundown');
const d = await api('/rundown');
_rdNext = d.streams_next;
_rdNext = d.streams_next;
const feed = d.streams || [];
const feed = d.streams || [];
- root.innerHTML = `${_rdMast(d.counts, 'latest')}${_rdOnAir(d.on_air || [])}
+ root.innerHTML = `${_rdMast(d.counts, 'latest')}${_rdUses(d.lead)}${_rdOnAir(d.on_air || [])}
<div class="rd-grid">
<div class="rd-grid">
<div class="rd-main">
<div class="rd-main">
${d.lead ? _rdLead(d.lead) : ''}
${d.lead ? _rdLead(d.lead) : ''}
new file mode 100644
new file mode 100644
@@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
+/**
+ * The Rundown's front page says what a visitor can do there (public/js/rundown.js _rdUses): catch up on a stream (the
+ * stream-by-stream list), follow a topic (/rundown/topics), jump to the moment (the top take's second on OpenVibe.Video)
+ * and get written up (/broadcast). A viewer said of the page "still don't rly know wtf to do with this feature"
+ * (2026-10-20): it said what it is, never what it is for.
+ *
+ * node test/rundown-uses.test.js
+ */
+'use strict';
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const js = fs.readFileSync(path.join(__dirname, '../public/js/rundown.js'), 'utf8');
+const css = fs.readFileSync(path.join(__dirname, '../public/css/features/rundown.css'), 'utf8');
+const pick = (name) => {
+ const start = js.indexOf(`function ${name}(`);
+ assert.ok(start >= 0, `${name} is in rundown.js`);
+ const next = js.indexOf('\nfunction ', start + 1);
+ return js.slice(start, next < 0 ? undefined : next);
+};
+
+const ctx = { videoHref: (kind, id, t) => `https://openvibe.video/${kind === 'clip' ? 'c' : 'v'}/${id}${t ? `?t=${t}` : ''}` };
+vm.createContext(ctx);
+vm.runInContext([pick('_rdEsc'), pick('_rdA'), pick('_rdUses')].join('\n'), ctx);
+
+const links = (html) => [...html.matchAll(/<a class="rd-use" href="([^"]+)"/g)].map((m) => m[1]);
+const titles = (html) => [...html.matchAll(/<b>([^<]+)<\/b>/g)].map((m) => m[1]);
+
+const withLead = ctx._rdUses({ id: 7, vod_id: 5855, sec: 40, quotes: [{ text: 'x', sec: 95.6 }] });
+assert.deepStrictEqual(titles(withLead), ['Catch up on a stream', 'Follow a topic', 'Jump to the moment', 'Get written up']);
+assert.deepStrictEqual(links(withLead), ['#rd-latest-h', '/rundown/topics', 'https://openvibe.video/v/5855?t=95', '/broadcast'],
+ 'each entry goes somewhere: the list, the topics, the top take\'s moment (its quote\'s second), going live');
+assert.ok(withLead.includes('aria-label="What you can do here"'));
+
+const noLead = ctx._rdUses(null);
+assert.deepStrictEqual(links(noLead), ['#rd-latest-h', '/rundown/topics', '/broadcast'], 'no recording to jump into: that entry is text, not a dead link');
+assert.ok(noLead.includes('<div class="rd-use"><i class="fa-solid fa-play"'));
+
+assert.match(js, /root\.innerHTML = `\$\{_rdMast\(d\.counts, 'latest'\)\}\$\{_rdUses\(d\.lead\)\}/, 'the front page shows it under the masthead');
+assert.match(css, /@media \(max-width: 560px\) \{[^}]*\.rd-uses \{[^}]*\}[\s\S]*?\.rd-use small \{ display: none; \}/, 'phones show titles only, two by two');
+
+console.log('rundown uses: all checks passed');