openvibers/openvibe.shared Commits

openvibe-shared 4.226.0: the menu's feed draws the one feed's new kinds (#505)

OP OpenVibers committed 17 h ago · a32f2fead0 · 8 files, +109 −10

A report is its number with its trend; a folded run (jobs, deals) lists its first three and how many more;
what a count means is said (14 jobs, 41 voted, watched); the Rundown is about its actor; a card without a
person says its text once.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 45a3205..b6ef5d5 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,17 @@
# Changelog
++## 4.226.0 — 2026-11-12
++
++- **The menu's feed draws the one feed's new kinds (feed.item@1)** (the one-feed job's finding: they fell through to a
++ bare card):
++ - **A report** is its number, large, with which way it moved and from what (▲ 12%, was 1,075).
++ - **A folded run** (new jobs, new deals) lists its first three, each with its picture where there is one, rows
++ lined up, then "and 11 more".
++ - **What a count means** is said: "14 jobs", "41 voted"; `counts.watchers` reads "watched".
++ - **The Rundown** is about its actor (feed.item@1's `subject` is a person's id, so the old reading found nothing).
++ - A card without a person no longer says its text twice.
++ - `menu-feed-v2-chrome` checks each.
++
## 4.225.0 — 2026-11-12
**The menu's Vibe is one place: All · Chats · Tasks · Code, no switch** (the owner, 11-12, crossing out the Chat · Code ·
ddiff --git a/README.md b/README.md
iindex 68e46ee..aba5f54 100644
---- a/README.md
++++ b/README.md
@@@ -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.225.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.226.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex c871c44..7533ac6 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.225.0",
++ "version": "4.226.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)",
ddiff --git a/navbar.js b/navbar.js
iindex 7ef5a14..787e168 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -604,7 +604,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': 'c96584376da1', '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': '146282739c06', 'dock.js': '1ae182836461', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '437395e64eda', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'a7636d0bcc02', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'c8da956d1e28', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': '293808b32ad7' };
++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', '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': '146282739c06', 'dock.js': '1ae182836461', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '437395e64eda', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '660e68d4f243', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'c8da956d1e28', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': '293808b32ad7' };
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.
ddiff --git a/ov-home.js b/ov-home.js
iindex ff7b803..47f5d4e 100644
---- a/ov-home.js
++++ b/ov-home.js
@@@ -546,6 +546,7 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
const ICON = {
pencil: [['path', { d: 'M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16z' }], ['path', { d: 'M13.5 6.5l4 4' }]],
check: [['path', { d: 'M5 12.5l4.5 4.5L19 7.5' }]],
++ briefcase: [['rect', { x: 3.5, y: 7.2, width: 17, height: 12.3, rx: 2.4 }], ['path', { d: 'M8.8 7.2V5.6a1.8 1.8 0 0 1 1.8-1.8h2.8a1.8 1.8 0 0 1 1.8 1.8v1.6M3.5 12.6h17' }]],
bell: [['path', { d: 'M6 14.6V9.5a6.1 6.1 0 0 1 12.2 0v5.1l1.5 2.1h-15.3Z' }], ['path', { d: 'M10 18.7a2.1 2.1 0 0 0 4 0' }]],
history: [['path', { d: 'M3.8 12.1a8.3 8.3 0 1 0 2.5-5.9L3.8 8.5M3.8 4.2v4.2h4.2' }], ['path', { d: 'M12 7.9v4.3l3.1 1.9' }]],
attach: [['path', { d: 'M12 5v14M5 12h14' }]],
@@@ -1548,7 +1549,7 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
}
const ACTS = new Set(['member', 'chat', 'event', 'quest', 'wiki', 'stream', 'update']);
function countsRow(n) {
-- const parts = [['views', 'eye', 'views'], ['likes', 'heart', 'likes'], ['comments', 'reply', 'replies']]
++ const parts = [['views', 'eye', 'views'], ['watchers', 'eye', 'watched'], ['likes', 'heart', 'likes'], ['comments', 'reply', 'replies']]
.filter(([k]) => Number(n && n[k]) > 0);
if (!parts.length) return null;
const row = h('span', 'ovfd-counts');
@@@ -1591,7 +1592,9 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
// 4.223.0 (the owner, 11-12: "the rundown should look visually distinct versus the live feeds"): its own face and
// name, whom it is about, and its picture marked as the Rundown's.
face.textContent = ''; face.className = 'ovfd-face is-rundown'; face.append(glyph('page'));
-- const about = c.subject && c.subject.name ? String(c.subject.name) : (/^Rundown:\s*([^:]{1,40}?)\s*(?:prep|:)/i.exec(String(c.title || '')) || [])[1] || '';
++ // feed.item@1 (Contracts 0.415.0): `subject` is a person's id, so whom it is about is its actor's name.
++ const about = (c.subject && typeof c.subject === 'object' && c.subject.name ? String(c.subject.name) : '') || (c.actor && c.actor.name && !/^system$/i.test(String(c.actor.name)) ? String(c.actor.name) : '')
++ || (/^Rundown:\s*([^:]{1,40}?)\s*(?:prep|:)/i.exec(String(c.title || '')) || [])[1] || '';
who.append(h('b', '', 'The Rundown'));
meta.append(siteIcon('live', 18), document.createTextNode(` ${[about ? `about ${about}` : 'from today’s streams', when(c.at)].filter(Boolean).join(' · ')}`));
a.classList.add('ovfd-k-rundown');
@@@ -1622,11 +1625,64 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
if (kind === 'live') title = title.replace(/^.{1,60}? went live:?\s*/i, '') || title;
if (kind === 'rundown') title = title.replace(/^Rundown:\s*/i, '') || title;
if (title) a.append(h('b', 'ovfd-title', title.slice(0, 200)));
-- if (c.text && (kind === 'post' || !thumb)) a.append(h('p', 'ovfd-text', String(c.text).slice(0, 500)));
++ if (kind === 'report') { const st = statOf(c); if (st) a.append(st); }
++ // A card without a person says its text in the line under its name already (not twice).
++ const textInMeta = !c.actor && kind !== 'rundown' && !!c.text;
++ if (c.text && (kind === 'post' || !thumb) && kind !== 'report' && !textInMeta) a.append(h('p', 'ovfd-text', String(c.text).slice(0, 500)));
++ const many = groupList(c);
++ if (many) a.append(many);
++ const people = peopleOf(kind, c);
const counts = countsRow(c.counts);
-- if (counts) a.append(counts);
++ if (people || counts) { const row = counts || h('span', 'ovfd-counts'); if (people) row.prepend(people); a.append(row); }
return a;
}
++ /** 4.226.0, the one feed's new kinds (feed.item@1). A report is its number, large, with which way it moved. */
++ function statOf(c) {
++ const n = c.counts && Number(c.counts.people);
++ const t = c.trend && typeof c.trend === 'object' ? c.trend : null;
++ if (!Number.isFinite(n) && !t) return null;
++ const box = h('span', 'ovfd-stat');
++ if (Number.isFinite(n)) box.append(h('b', 'ovfd-stat-n', num(n)));
++ if (c.text) box.append(h('small', 'ovfd-stat-l', String(c.text).slice(0, 120)));
++ if (t && ['up', 'down', 'flat'].includes(t.direction)) {
++ const pct = Number.isFinite(Number(t.pct)) ? `${Math.round(Number(t.pct))}%` : '';
++ const was = Number.isFinite(Number(t.previous)) ? `was ${num(Number(t.previous))}` : '';
++ const pill = h('span', `ovfd-trend is-${t.direction}`, [t.direction === 'up' ? '▲' : t.direction === 'down' ? '▼' : '=', pct].filter(Boolean).join(' '));
++ pill.setAttribute('aria-label', `${t.direction === 'flat' ? 'about the same' : `${t.direction} ${pct}`}${was ? `, ${was}` : ''}`);
++ box.append(pill);
++ if (was) box.append(h('small', 'ovfd-stat-was', was));
++ }
++ return box;
++ }
++ /** A folded run of the same kind (new jobs, new deals): its first three, each to its own page. */
++ function groupList(c) {
++ const g = c.group && Array.isArray(c.group.items) ? c.group.items : [];
++ if (g.length < 2) return null;
++ const list = h('span', 'ovfd-group');
++ const pic = (x) => { const th = x && x.media && safe(x.media.thumb); return th && /^https:/.test(th) ? th : ''; };
++ const anyPic = g.slice(0, 3).some((x) => !!pic(x));
++ g.slice(0, 3).forEach((x) => {
++ const row = h('span', 'ovfd-group-i');
++ const th = pic(x);
++ // Rows line up: where one has a picture, the others hold its place.
++ if (th) { const img = h('img'); img.alt = ''; img.loading = 'lazy'; img.decoding = 'async'; img.src = th; row.append(img); }
++ else if (anyPic) row.append(h('i', 'ovfd-group-ph'));
++ row.append(h('span', '', String(x.title || '').slice(0, 120)));
++ list.append(row);
++ });
++ const more = Number(c.group.count) - Math.min(3, g.length);
++ if (more > 0) list.append(h('small', 'ovfd-group-more', `and ${num(more)} more`));
++ return list;
++ }
++ /** What `counts.people` means for a kind: jobs, voters, people online; nothing where it means nothing. */
++ function peopleOf(kind, c) {
++ const n = c.counts && Number(c.counts.people);
++ if (!Number.isFinite(n) || n <= 0 || kind === 'report') return null;
++ const word = kind === 'work' ? (n === 1 ? 'job' : 'jobs') : kind === 'poll' ? 'voted' : kind === 'deal' ? (n === 1 ? 'deal' : 'deals') : kind === 'member' ? 'joined' : 'people';
++ const sp = h('span', ''); sp.setAttribute('aria-label', `${num(n)} ${word}`); sp.title = `${num(n)} ${word}`;
++ sp.append(glyph(kind === 'work' ? 'briefcase' : kind === 'poll' ? 'check' : 'user'), document.createTextNode(`${count(n)} ${word}`));
++ return sp;
++ }
/** Several streamers who went live (and are not live now), as one card: their faces, then each name to their channel. */
function livesCard(items) {
const box = h('div', 'ovfd-card ovfd-lives');
@@@ -1794,6 +1850,19 @@ html[data-ov-motion=off] .ovh-face.is-glyph:has(.is-working) { animation: none;
.ovfd-chip[aria-selected=true]{background:var(--text-primary,#e8ecf3);color:var(--bg-primary,#0b1220);border-color:transparent}
.ovfd-list{display:grid;gap:10px;padding:4px 0}
@media (min-width:861px){.ovfd{max-width:720px;margin-inline:auto}}
++.ovfd-stat{display:grid;grid-template-columns:auto 1fr;align-items:baseline;column-gap:10px;row-gap:2px;padding:4px 2px}
++.ovfd-stat-n{font-family:'OV Brand',Nunito,Inter,sans-serif;font-size:34px;font-weight:800;line-height:1.05;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
++.ovfd-stat-l{font-size:13px;color:var(--text-secondary,#a8b3c4);align-self:center}
++.ovfd-trend{grid-column:1;justify-self:start;font-size:12.5px;font-weight:750;padding:1px 8px;border-radius:999px;font-variant-numeric:tabular-nums}
++.ovfd-trend.is-up{color:#34d399;background:color-mix(in srgb,#10b981 16%,transparent)}.ovfd-trend.is-down{color:#f87171;background:color-mix(in srgb,#ef4444 14%,transparent)}.ovfd-trend.is-flat{color:var(--text-secondary,#a8b3c4);background:color-mix(in srgb,var(--text-primary,#e8ecf3) 8%,transparent)}
++.ovfd-stat-was{font-size:12px;color:var(--text-muted,#93a0b4);align-self:center}
++.ovfd-group{display:grid;gap:4px;padding:2px 0}
++.ovfd-group-i{display:flex;align-items:center;gap:8px;min-width:0;font-size:13.5px;color:var(--text-secondary,#a8b3c4)}
++.ovfd-group-i img,.ovfd-group-ph{flex:none;width:36px;height:36px;border-radius:8px;object-fit:cover}
++.ovfd-group-ph{background:color-mix(in srgb,var(--text-primary,#e8ecf3) 6%,transparent)}
++.ovfd-group:not(:has(img)) .ovfd-group-i::before{content:'';flex:none;width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.55}
++.ovfd-group-i span{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
++.ovfd-group-more{font-size:12px;color:var(--text-muted,#93a0b4)}
.ovfd-card{display:grid;gap:8px;padding:10px;border-radius:16px;background:color-mix(in srgb,var(--text-primary,#e8ecf3) 4%,transparent);box-shadow:inset 0 0 0 1px var(--border,rgba(255,255,255,.08));color:inherit;text-decoration:none;min-width:0}
@media (hover:hover){.ovfd-card:hover{background:color-mix(in srgb,var(--text-primary,#e8ecf3) 7%,transparent)}}
.ovfd-head{display:flex;align-items:center;gap:8px;min-width:0}
ddiff --git a/package-lock.json b/package-lock.json
iindex a38dfc9..69a91ff 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.225.0",
++ "version": "4.226.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.225.0",
++ "version": "4.226.0",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex 34f3787..264b70d 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.225.0",
++ "version": "4.226.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/menu-feed-v2-chrome.test.js b/test/menu-feed-v2-chrome.test.js
iindex d6592a1..209f092 100644
---- a/test/menu-feed-v2-chrome.test.js
++++ b/test/menu-feed-v2-chrome.test.js
@@@ -44,6 +44,10 @@ const page = (signed) => `<!doctype html><html lang="en"><head><meta charset="ut
const mk = (i, k) => ({ id: k + i + '-' + page, kind: k, live_now: k === 'live', url: 'https://openvibe.video/c/' + i, title: (k === 'post' ? '<b>bold</b> ' : '') + k + ' ' + i + ' page ' + page, text: 'words', at: new Date(Date.now() - i * 60000).toISOString(), site: k === 'pic' ? 'pics' : 'video', site_name: 'Video', actor: { name: 'ada' }, media: k === 'post' || k === 'update' ? {} : { thumb: 'https://openvibe.media/t/x.webp', ratio: 1.7778, length: '0:30' }, counts: { views: 1200, likes: 3 } });
if (kind === 'event') { const at = (h) => new Date(Date.now() - h * 3600e3).toISOString(); const y = new Date(Date.now() - 86400e3); y.setHours(12, 0, 0, 0);
return J({ items: [
++ { id: 'r', kind: 'report', url: 'https://openvibe.report/network', title: 'Peak today: 1,204 people online at 9 PM', text: 'Online peak across the network', at: at(0.03), site: 'report', site_name: 'Report', actor: null, subject: null, media: null, counts: { people: 1204 }, trend: { direction: 'up', pct: 12, previous: 1075 }, label: 'System' },
++ { id: 'j', kind: 'work', url: 'https://openvibe.work/jobs', title: '14 new jobs this hour', text: 'Remote: GIS engineer, and more', at: at(0.04), site: 'work', site_name: 'Work', actor: null, subject: null, media: null, counts: { people: 14 }, group: { count: 14, items: [{ id: 'j1', url: 'https://openvibe.work/j/1', title: 'GIS engineer', media: null }, { id: 'j2', url: 'https://openvibe.work/j/2', title: 'Product manager', media: { thumb: 'https://openvibe.media/t/j2.jpg' } }, { id: 'j3', url: 'https://openvibe.work/j/3', title: 'Infra lead', media: null }, { id: 'j4', url: 'https://openvibe.work/j/4', title: 'Designer', media: null }] } },
++ { id: 'v', kind: 'poll', url: 'https://openvibe.community/forum/status/poll', title: 'Which feed kind do you open first?', text: 'A poll', at: at(0.05), site: 'community', site_name: 'Community', actor: { name: 'Alex', avatar: null }, subject: 'usr_01JQ8Z6K6M0V8Y2Q3R4S5T6V7W', media: null, counts: { people: 41 } },
++ { id: 'rd', kind: 'rundown', url: 'https://openvibe.live/rundown/e/3005', title: 'Rundown: The Case Against Smooth Apologies', text: 'What the episode was about.', at: at(0.06), site: 'live', site_name: 'Live', actor: { name: 'Maticus', avatar: null }, subject: 'usr_01JQ8Z6K6M0V8Y2Q3R4S5T6V7W', media: null, counts: {} },
{ id: 'm', kind: 'member', url: 'https://openvibe.network/stats', title: '3 people joined today', text: '520 people on OpenVibe so far', at: at(0.01), site: 'network', site_name: 'Network', actor: null, media: null, counts: {} },
{ id: 'c', kind: 'chat', url: 'https://openvibe.chat/rooms/general', title: '#general: 15 messages this hour', text: '', at: at(0.02), site: 'chat', site_name: 'Chat', actor: null, media: null, counts: {} },
{ id: 'q', kind: 'quest', url: 'https://openvibe.quest/', title: 'Finished a quest: First Post', text: '', at: y.toISOString(), site: 'quest', site_name: 'Quest', actor: { name: 'Josh', avatar: null }, media: null, counts: {} },
@@@ -118,6 +122,20 @@ const server = http.createServer((req, res) => {
assert.ok(/^Nov1[12]$/.test(acts.date), `an event with a start: the date tile (${acts.date})`);
assert.ok(acts.badge, 'a quest: the trophy on the face');
assert.strictEqual(acts.selfImages, 0, 'no picture means no image (an empty address drew the page itself)');
++ // 4.226.0, the one feed's new kinds (feed.item@1): a report is its number with its trend; a folded run lists its
++ // first three; what counts.people means is said; the Rundown is about its actor (its subject is an id).
++ const kinds = await p.evaluate(`(() => { const q = (s) => document.querySelector(s);
++ return { stat: (q('.ovfd-k-report .ovfd-stat-n') || {}).textContent, trend: (q('.ovfd-k-report .ovfd-trend') || {}).textContent, was: (q('.ovfd-k-report .ovfd-stat-was') || {}).textContent,
++ up: !!q('.ovfd-k-report .ovfd-trend.is-up'), group: [...document.querySelectorAll('.ovfd-k-work .ovfd-group-i')].map((r) => r.textContent), more: (q('.ovfd-k-work .ovfd-group-more') || {}).textContent,
++ groupImgs: document.querySelectorAll('.ovfd-k-work .ovfd-group-i img').length, jobs: (q('.ovfd-k-work .ovfd-counts') || {}).textContent, voted: (q('.ovfd-k-poll .ovfd-counts') || {}).textContent,
++ about: (q('.ovfd-k-rundown .ovfd-who small') || {}).textContent || '' }; })()`);
++ assert.deepStrictEqual([kinds.stat, kinds.trend, kinds.was, kinds.up], ['1,204', '▲ 12%', 'was 1,075', true], 'a report: its number, which way it moved, and from what');
++ assert.deepStrictEqual(kinds.group, ['GIS engineer', 'Product manager', 'Infra lead'], 'a folded run: its first three');
++ assert.deepStrictEqual([kinds.more, kinds.groupImgs], ['and 11 more', 1], 'and how many more; a picture where there is one');
++ assert.strictEqual(kinds.jobs, '14 jobs', 'what the count means for jobs');
++ assert.strictEqual(kinds.voted, '41 voted', 'and for a poll');
++ assert.match(kinds.about, /about Maticus/, `the Rundown is about its actor (${kinds.about})`);
++ if (process.env.OV_CAPTURE_FEED) { await p.evaluate(`document.querySelector('.ovfd-k-report').scrollIntoView({ block: 'start' }), true`); await new Promise((r) => setTimeout(r, 400)); const shot = await p.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(process.env.OV_CAPTURE_FEED, Buffer.from(shot.data, 'base64')); }
console.log('menu-feed-v2-chrome: chips, cards by kind, text only, the next page, caught up, a chip by kind, the stream alone, activity rows and day lines ok');
} finally { await browser.close(); server.close(); fs.rmSync(tmpDir, { recursive: true, force: true }); }
})().catch((e) => { console.error(e); process.exit(1); });