openvibers/openvibe.community Commits

Empty states: the board, Pulse, Search, submissions and paste grids use the kit (#391)

OP OpenVibers committed 1 days ago · 40872152ef · 8 files, +119 −19

* Empty states: the board, Pulse, Search, submissions and paste grids use the kit

* Draw the kit empty state on the board, Pulse, Search, submissions and paste grids

Changes

ddiff --git a/server/render/board.js b/server/render/board.js
iindex 05c40c1..2d5940e 100644
---- a/server/render/board.js
++++ b/server/render/board.js
@@@ -61,7 +61,7 @@ ${groups.length ? groups.map((g) => `<section class="board-group" id="g-${esc(g.
<h2 class="board-group-h" id="gh-${esc(g.slug || 'more')}">${esc(g.name)}</h2>
${g.description ? `<p class="muted small board-group-p">${esc(g.description)}</p>` : ''}
${boardTable(g.spaces, g.name)}
--</section>`).join('') : '<p class="empty">No forums yet.</p>'}
++</section>`).join('') : kit.empty({ icon: 'chat', title: 'No forums yet', text: 'No forum has been started here yet.', action: canModerate ? { href: '/forum/new', label: 'New forum', icon: 'plus' } : { href: '/pulse', label: 'See what is happening', icon: 'live' } })}
${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post.</p>'}`;
return renderPage({
title: 'Forum',
@@@ -70,6 +70,7 @@ ${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post
active: 'spaces',
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }])],
++ styles: ['kit.css'],
body,
});
}
@@@ -160,7 +161,7 @@ ${tags}${statuses}
<section data-results>
${threads.length ? `<table class="board-table topic-table"><caption class="sr-only">Topics in ${esc(space.name)}</caption>
<thead><tr><th scope="col" class="topic-icon"><span class="sr-only">Kind</span></th><th scope="col">Topic</th><th scope="col" class="num">Replies</th><th scope="col" class="num">Views</th><th scope="col">Last post</th></tr></thead>
-- <tbody>${threads.map((t) => topicRow(t, space)).join('')}</tbody></table>` : `<p class="empty">${filtered ? `Nothing here yet. <a href="${esc(href({ category: null, status: null }))}">See every topic</a>.` : viewer.can_start !== false ? `No topics yet — <a href="/forum/${esc(space.slug)}/new">start the first one</a>.` : 'Nothing here yet.'}</p>`}
++ <tbody>${threads.map((t) => topicRow(t, space)).join('')}</tbody></table>` : filtered ? kit.empty({ icon: 'search', title: 'Nothing here yet', text: 'No topic matches these filters.', action: { href: href({ category: null, status: null }), label: 'See every topic', icon: 'grid' } }) : viewer.can_start !== false ? kit.empty({ icon: 'chat', title: 'No topics yet', text: 'This forum has no topic yet.', action: { href: `/forum/${esc(space.slug)}/new`, label: space.thread_kind === 'request' ? 'Make the first request' : 'Start the first topic', icon: 'plus' } }) : kit.empty({ icon: 'chat', title: 'Nothing here yet', text: 'No topic has been started here yet.', action: { href: '/forum', label: 'See the other forums', icon: 'grid' } })}
${pagerHtml}
</section>
${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moderators, { me, error: modError }) : ''}
@@@ -173,6 +174,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moder
active: 'spaces',
feeds: feedable ? [{ title: `OpenVibe.Community — ${space.name}`, href: `/forum/${space.slug}/feed.xml` }] : [],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }])],
++ styles: ['kit.css'],
body,
});
}
ddiff --git a/server/render/forum.js b/server/render/forum.js
iindex 61f60aa..90744cb 100644
---- a/server/render/forum.js
++++ b/server/render/forum.js
@@@ -449,7 +449,7 @@ function moderationLogPage({ space, entries }) {
</header>
${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">Moderation actions in ${esc(space.name)}</caption>
<thead><tr><th scope="col">When</th><th scope="col">What</th><th scope="col">Action</th><th scope="col">Rule</th><th scope="col">Reason</th><th scope="col">By</th></tr></thead>
-- <tbody>${rows}</tbody></table>` : '<p class="empty">No moderation actions in this forum yet.</p>'}
++ <tbody>${rows}</tbody></table>` : kit.empty({ icon: 'shield', title: 'No moderation actions yet', text: 'Nothing has been moderated in this forum yet.' })}
<footer class="space-foot"><a class="small" href="/forum/${esc(space.slug)}">Back to ${esc(space.name)}</a>${space.visibility === 'public' && !space.members_only ? `<span class="sep"> · </span><a class="small" href="/forum/${esc(space.slug)}/feed.xml">RSS</a>` : ''}</footer>`;
return renderPage({
title: `Moderation log · ${space.name}`,
@@@ -459,6 +459,7 @@ ${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">
active: 'spaces',
footerVariant: 'compact',
pageCss: ['modlog.css'],
++ styles: ['kit.css'],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }, { name: 'Moderation log', url: `/forum/${space.slug}/log` }])],
body,
});
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex cdae472..ea404ed 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -135,8 +135,11 @@ function pasteCard(p, { eager = false } = {}) {
</div>
</article>`;
}
--function cardGrid(pastes, empty = 'Nothing here yet.') {
-- if (!pastes || !pastes.length) return `<p class="empty">${esc(empty)}</p>`;
++function cardGrid(pastes, empty = null) {
++ if (!pastes || !pastes.length) {
++ const e = empty || { title: 'Nothing here yet', text: 'No pastes here yet.' };
++ return kit.empty({ icon: e.icon || 'grid', title: e.title, text: e.text, action: e.action || null });
++ }
// The first picture in the grid is the one above the fold: it loads at high priority, the rest stay lazy.
const firstShot = pastes.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.locked);
return `<div class="grid">${pastes.map((p, i) => pasteCard(p, { eager: i === firstShot })).join('\n')}</div>`;
@@@ -414,7 +417,7 @@ function browsePage(result, languages) {
${(q || lang || sort !== 'new' || from !== 'people') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}
</form>
<section id="results" data-results>
-- ${cardGrid(pastes, q ? 'No pastes match that search.' : 'No pastes yet.')}
++ ${cardGrid(pastes, q ? { title: 'No matches', text: 'No pastes match that search.', action: { href: '/pastes', label: 'See all pastes', icon: 'grid' } } : { title: 'No pastes yet', text: 'No paste has been shared here yet.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } })}
${pager}
</section>`;
return renderPage({
@@@ -424,6 +427,7 @@ function browsePage(result, languages) {
agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
robots: q ? 'noindex,follow' : 'index,follow',
active: 'pastes',
++ styles: ['kit.css'],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],
body,
});
@@@ -555,7 +559,7 @@ ${p.burn_after_read ? '' : `<section class="section">
</section>`}
${!locked && related && related.length ? `<section class="section" id="related">
<div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
-- ${cardGrid(related)}
++ ${cardGrid(related, { title: 'No related pastes yet', text: 'Related pastes appear here when this one is linked with others.' })}
</section>` : ''}
<script src="${ovServe.url('ov-share.js')}" defer></script>${locked ? `
<script>window.OpenVibeLockerConfig = { network: ${JSON.stringify(config.networkUrl)} };</script>
@@@ -753,11 +757,11 @@ function myPage({ user, pastes, total, error }) {
const body = `
<header class="page-head">
<h1>My pastes</h1>
-- <p class="muted">${esc(user.display_name || user.username)} · ${num(total || (pastes || []).length)} ${total === 1 ? 'paste' : 'pastes'} · <a href="/new">new paste</a> · <a href="${NETWORK_URL}/my" rel="noopener">account</a></p>
++ <p class="muted">${esc(user.display_name || user.username)} · ${num(total || (pastes || []).length)} ${total === 1 ? 'paste' : 'pastes'} · ${kit.button({ href: '/new', label: 'New paste', icon: 'plus', kind: 'quiet', small: true })} · <a href="${NETWORK_URL}/my" rel="noopener">account</a></p>
</header>
${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}
<section data-results>
-- ${cardGrid(pastes, 'You have not posted anything yet. Your public, unlisted and private pastes all show up here.')}
++ ${cardGrid(pastes, { title: 'No pastes yet', text: 'You have not posted anything yet. Your public, unlisted and private pastes all show up here.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } })}
</section>`;
return renderPage({
title: 'My pastes',
@@@ -766,6 +770,7 @@ ${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}
robots: 'noindex,nofollow',
active: 'my',
footerVariant: 'compact',
++ styles: ['kit.css'],
body,
});
}
ddiff --git a/server/render/pulse.js b/server/render/pulse.js
iindex 3ad64d0..918491e 100644
---- a/server/render/pulse.js
++++ b/server/render/pulse.js
@@@ -11,6 +11,7 @@ const { renderPage } = require('./layout');
const { escapeHtml: esc } = require('./highlight');
const { timeTag } = require('./pages');
const { who } = require('./forum');
++const kit = require('openvibe-shared/kit');
const FILTERS = [['', 'Everything'], ['user', 'People'], ['ai', 'AI'], ['system', 'System']];
const SERVICE_NAMES = { community: 'Community', live: 'Live', media: 'Media', network: 'Network', tools: 'Tools', games: 'Games', wiki: 'Wiki', blog: 'Blog', reviews: 'Reviews' };
@@@ -52,7 +53,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin
<nav class="tabs sort-tabs" aria-label="Filter by origin">${tabs}</nav>
<section data-results${after ? '' : ' data-pulse-live'}>
<p class="pulse-new" hidden><button type="button" class="btn btn-sm" data-pulse-show></button></p>
-- ${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : '<p class="empty">Nothing here yet.</p>'}
++ ${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : kit.empty({ icon: 'live', title: 'Nothing here yet', text: origin ? 'Nothing from this part of the network has arrived yet.' : 'New public clips, threads, pastes and posts land here as they happen.', action: origin ? { href: '/pulse', label: 'Everything', icon: 'live' } : { href: '/forum', label: 'Visit the forum', icon: 'chat' } })}
${after || nextCursor ? `<nav class="pager" aria-label="Pages">${after ? `<a href="${esc(href(origin))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Latest</a>` : '<span></span>'}<span></span>${nextCursor ? `<a rel="next" href="${esc(href(origin, nextCursor))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}
</section>`;
return renderPage({
@@@ -64,6 +65,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin
active: 'pulse',
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],
++ styles: ['kit.css'],
body,
});
}
ddiff --git a/server/render/search.js b/server/render/search.js
iindex 6a9b2af..810e0a5 100644
---- a/server/render/search.js
++++ b/server/render/search.js
@@@ -6,6 +6,7 @@
*/
const { renderPage } = require('./layout');
const { escapeHtml: esc } = require('./highlight');
++const kit = require('openvibe-shared/kit');
// Search escapes the text and adds only <mark>; keep exactly that, whatever arrives.
const snippet = (html) => esc(String(html || '')).replace(/&lt;(\/?)mark&gt;/g, '<$1mark>');
@@@ -28,9 +29,9 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavai
const opt = (v, label) => `<option value="${v}"${type === v ? ' selected' : ''}>${label}</option>`;
const more = nextCursor ? `<nav class="pager"><a class="btn" href="/search?${new URLSearchParams({ q, ...(type ? { type } : {}), cursor: nextCursor })}">More results</a></nav>` : '';
let section;
-- if (unavailable) section = `<p class="empty">Search is not answering right now. <a href="/pastes?q=${encodeURIComponent(q)}">Filter pastes for “${esc(q)}”</a> instead, or try again in a minute.</p>`;
-- else if (!q) section = '<p class="empty">Search every public thread and paste on OpenVibe.Community.</p>';
-- else if (!results.length) section = `<p class="empty">Nothing public matches “${esc(q)}”.</p>`;
++ if (unavailable) section = kit.empty({ icon: 'search', title: 'Search is not answering', text: 'OpenVibe.Search is not answering right now. Try again in a minute, or filter pastes instead.', action: q ? { href: `/pastes?q=${encodeURIComponent(q)}`, label: 'Filter pastes', icon: 'search' } : { href: '/pastes', label: 'Browse pastes', icon: 'grid' } });
++ else if (!q) section = kit.empty({ icon: 'search', title: 'Search Community', text: 'Search every public thread and paste on OpenVibe.Community.', action: { href: '/forum', label: 'Browse the forum', icon: 'chat' } });
++ else if (!results.length) section = kit.empty({ icon: 'search', title: 'Nothing matches', text: `Nothing public matches “${q}”.`, action: { href: `https://search.openvibe.network/?q=${encodeURIComponent(q)}`, label: 'Search the whole network', icon: 'search' } });
else section = `<ol class="search-results">${results.map(resultItem).join('\n')}</ol>${more}`;
const body = `
<header class="page-head">
@@@ -49,6 +50,7 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavai
canonicalPath: '/search',
robots: 'noindex,follow',
active: 'search',
++ styles: ['kit.css'],
body,
});
}
ddiff --git a/server/render/submissions.js b/server/render/submissions.js
iindex f67fef3..ee56be2 100644
---- a/server/render/submissions.js
++++ b/server/render/submissions.js
@@@ -11,6 +11,7 @@ const { escapeHtml: esc } = require('./highlight');
const { renderMarkdown, markdownToText } = require('./markdown');
const { timeTag } = require('./pages');
const { who } = require('./forum');
++const kit = require('openvibe-shared/kit');
const KIND_LABELS = { clip: 'Clip', art: 'Art', idea: 'Idea', report: 'Report' };
const STATUS_LABELS = { pending: 'Pending review', accepted: 'Accepted', rejected: 'Not accepted', withdrawn: 'Withdrawn' };
@@@ -37,8 +38,8 @@ function itemHtml(s) {
</li>`;
}
--function listBlock(items, { empty, prev, next }) {
-- return `${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : `<p class="empty">${esc(empty)}</p>`}
++function listBlock(items, { empty, action = null, prev, next }) {
++ return `${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : kit.empty({ icon: 'mail', title: empty.title, text: empty.text, action })}
${prev || next ? `<nav class="pager" aria-label="Pages">${prev ? `<a href="${esc(prev)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newest</a>` : '<span></span>'}<span></span>${next ? `<a rel="next" href="${esc(next)}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}`;
}
@@@ -72,7 +73,11 @@ function submissionsPage({ items, kind = '', mine = false, after = null, nextCur
</header>
<nav class="tabs sort-tabs" aria-label="Filter by kind">${tabs}</nav>
<section data-results>
-- ${listBlock(items, { empty: mine ? 'You have not submitted anything yet.' : 'Nothing accepted yet.', prev: after ? listHref({ kind, mine }) : null, next: nextCursor ? listHref({ kind, mine, after: nextCursor }) : null })}
++ ${listBlock(items, {
++ empty: mine ? { title: 'No submissions yet', text: 'You have not submitted anything yet.' } : { title: 'Nothing accepted yet', text: 'No submission has been accepted here yet.' },
++ action: user ? { href: '#submit', label: 'Submit something', icon: 'plus' } : { href: '/auth/login?next=%2Fsubmissions', label: 'Sign in to submit', icon: 'plus' },
++ prev: after ? listHref({ kind, mine }) : null, next: nextCursor ? listHref({ kind, mine, after: nextCursor }) : null,
++ })}
</section>
${submitForm({ user, values, error })}`;
return renderPage({
@@@ -81,6 +86,7 @@ ${submitForm({ user, values, error })}`;
canonicalPath: listHref({ kind, after }),
robots: mine || after ? 'noindex,follow' : 'index,follow',
active: 'submissions',
++ styles: ['kit.css'],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Submissions', url: '/submissions' }])],
body,
});
@@@ -141,9 +147,9 @@ function reviewQueuePage({ items, after = null, nextCursor = null }) {
<p class="muted">Submissions waiting for a decision, newest first. Open one to accept or reject it.</p>
</header>
<section data-results>
-- ${listBlock(items, { empty: 'Nothing is waiting for review.', prev: after ? '/submissions/review' : null, next: nextCursor ? listHref({ after: nextCursor }, '/submissions/review') : null })}
++ ${listBlock(items, { empty: { title: 'Nothing to review', text: 'Nothing is waiting for review.' }, action: { href: '/submissions', label: 'See submissions', icon: 'grid' }, prev: after ? '/submissions/review' : null, next: nextCursor ? listHref({ after: nextCursor }, '/submissions/review') : null })}
</section>`;
-- return renderPage({ title: 'Review queue', description: 'Submissions waiting for review.', canonicalPath: '/submissions/review', robots: 'noindex,nofollow', active: 'submissions', footerVariant: 'compact', body });
++ return renderPage({ title: 'Review queue', description: 'Submissions waiting for review.', canonicalPath: '/submissions/review', robots: 'noindex,nofollow', active: 'submissions', footerVariant: 'compact', styles: ['kit.css'], body });
}
module.exports = { submissionsPage, submissionPage, reviewQueuePage, KIND_LABELS, STATUS_LABELS };
ddiff --git a/test/empty-states.test.js b/test/empty-states.test.js
nnew file mode 100644
iindex 0000000..46c519d
---- /dev/null
++++ b/test/empty-states.test.js
@@@ -0,0 +1,82 @@
++'use strict';
++/**
++ * The plain-page sweep (owner ask 81): the pages that used to print a bare "No X yet" draw the shared kit's
++ * empty state instead, and the one next action is a kit button. Each page that can show one loads kit.css.
++ */
++const assert = require('assert');
++const { pulsePage } = require('../server/render/pulse');
++const { boardIndexPage, forumSpacePage } = require('../server/render/board');
++const { searchPage } = require('../server/render/search');
++const { submissionsPage, reviewQueuePage } = require('../server/render/submissions');
++const { cardGrid } = require('../server/render/pages');
++
++const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to find: ${s}`);
++const emptyState = (html, title) => {
++ has(html, 'class="ovk-empty"');
++ has(html, title);
++ has(html, 'kit.css');
++};
++
++const space = { slug: 'empty', name: 'Empty', description: '', visibility: 'public', members_only: 0, style: 'forum', thread_kind: 'topic', statuses: [] };
++
++const checks = [
++ ['the board index with no forum', () => {
++ const html = boardIndexPage({ groups: [] });
++ emptyState(html, 'No forums yet');
++ has(html, 'See what is happening');
++ has(html, 'ovk-btn');
++ }],
++ ['a forum with no topic', () => {
++ const html = forumSpacePage({ space, threads: [], page: 1, pages: 1, total: 0, viewer: { can_start: true } });
++ emptyState(html, 'No topics yet');
++ has(html, 'Start the first topic');
++ }],
++ ['a filtered forum with no match', () => {
++ const html = forumSpacePage({ space, threads: [], page: 1, pages: 1, total: 0, category: 'other', viewer: { can_start: true } });
++ emptyState(html, 'Nothing here yet');
++ has(html, 'See every topic');
++ }],
++ ['Pulse with nothing', () => {
++ const html = pulsePage({ items: [] });
++ emptyState(html, 'Nothing here yet');
++ has(html, 'Visit the forum');
++ }],
++ ['Search before a query', () => {
++ const html = searchPage({ q: '', results: [] });
++ emptyState(html, 'Search Community');
++ has(html, 'Browse the forum');
++ }],
++ ['Search with no match', () => {
++ const html = searchPage({ q: 'zzz', results: [] });
++ emptyState(html, 'Nothing matches');
++ has(html, 'Search the whole network');
++ }],
++ ['Search not answering', () => {
++ const html = searchPage({ q: 'zzz', results: [], unavailable: true });
++ emptyState(html, 'Search is not answering');
++ has(html, 'Filter pastes');
++ }],
++ ['Submissions with nothing accepted', () => {
++ const html = submissionsPage({ items: [] });
++ emptyState(html, 'Nothing accepted yet');
++ has(html, 'Sign in to submit');
++ }],
++ ['the review queue with nothing waiting', () => {
++ const html = reviewQueuePage({ items: [] });
++ emptyState(html, 'Nothing to review');
++ has(html, 'See submissions');
++ }],
++ ['a paste grid with nothing', () => {
++ const html = cardGrid([], { title: 'No pastes yet', text: 'No paste has been shared here yet.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } });
++ has(html, 'class="ovk-empty"');
++ has(html, 'No pastes yet');
++ has(html, 'Make a paste');
++ }],
++];
++
++let failed = 0;
++for (const [name, run] of checks) {
++ try { run(); console.log(`ok - ${name}`); }
++ catch (e) { failed += 1; console.error(`not ok - ${name}: ${e.message}`); }
++}
++process.exitCode = failed ? 1 : 0;
ddiff --git a/test/pulse.test.js b/test/pulse.test.js
iindex 15f03f9..801580b 100644
---- a/test/pulse.test.js
++++ b/test/pulse.test.js
@@@ -182,7 +182,7 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(people.includes('<meta name="robots" content="index,follow">'));
const empty = pulsePage({ items: [], origin: 'system', origins: ['user'] });
assert.ok(empty.includes('aria-current="page">System</a>'), 'the filter asked for keeps its tab');
-- assert.ok(empty.includes('Nothing here yet.') && empty.includes('<meta name="robots" content="noindex,follow">'));
++ assert.ok(empty.includes('Nothing here yet') && empty.includes('class="ovk-empty"') && empty.includes('<meta name="robots" content="noindex,follow">'));
assert.ok(pulsePage({ items: [item] }).includes('href="/pulse?origin=system"'), 'without the origins list every filter shows');
const r = await t.get('/pulse');
assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');