openvibers/openvibe.bench Commits

Bench: the owner page for sharing defaults, apply to all and private reads (#206)

OP OpenVibers committed 2 days ago · faa32736c8 · 5 files, +190 −25

Changes

ddiff --git a/README.md b/README.md
iindex ff5011d..516ec3c 100644
---- a/README.md
++++ b/README.md
@@@ -7,7 +7,7 @@ The shared AI benchmark and comparison engine for every OpenVibe room (plan 2026
Bench is the comparative-evaluation authority. AI stays the run authority, Report the analytics authority, Route the learner, and each room keeps its own pages. Bench owns kinds, tasks, pinned rounds, entries, judgements, ratings, boards, the open dataset and private evals.
--**The public pages moved to OpenVibe.Report's AI section (2026-11-08):** `https://openvibe.report/ai`, `/ai/<benchmark>` and `/ai/models/<model>`. This origin's old HTML pages answer `301` there; its API, MCP endpoint, render origin, badges and embeds stay here.
++**The public pages moved to OpenVibe.Report's AI section (2026-11-08):** `https://openvibe.report/ai`, `/ai/<benchmark>` and `/ai/models/<model>`. This origin's old HTML pages answer `301` there; its API, MCP endpoint, render origin, badges and embeds stay here. The owner's pages stay here too: `/benchmarks/mine` (the default audience, "apply to all", and each benchmark's Share button), `/benchmarks/new`, an unlisted share link (`/b/<token>`), and the noindex task and run pages. A benchmark's own page redirects only when it is public and the viewer is not its owner: a private benchmark, a named reader and an unlisted link have no Report page.
- Service id: `bench`
- Port: `5230`
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 8d71976..9916d8c 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -100,9 +100,11 @@ function createPageRoutes(ctx) {
const id = out.benchmark.id;
const owner = subjectOf(req) != null && String(out.benchmark.owner) === String(subjectOf(req));
const since = parseSince(req.query.since);
-- const [rankings, runs] = await Promise.all([
++ const [rankings, runs, audienceRow] = await Promise.all([
registry.rankings({ benchmark: id, metric, since: since ? since.text : null }),
registry.listRuns({ benchmark: id, limit: 8 }),
++ // The column maps Specific people to private; the share row names the real audience for the page's cue.
++ ctx.sharing ? ctx.sharing.shareOf(id).catch(() => null) : Promise.resolve(null),
]);
const allTime = since ? await registry.rankings({ benchmark: id, metric }) : null;
const isPublic = out.benchmark.visibility === 'public';
@@@ -126,6 +128,7 @@ function createPageRoutes(ctx) {
benchmark: out.benchmark, definition: out.definition, version: out.version && out.version.version,
rankings, metric, runs, codesRunUrl: config.codesRunUrl || null, viewer: req.viewer, baseUrl: base(),
ownBoards, share, shared, owner, since: rankings && rankings.since, allTimeEntries: allTime && allTime.entries,
++ audience: audienceRow ? audienceRow.audience : null,
}))),
}, status);
};
@@@ -214,6 +217,49 @@ function createPageRoutes(ctx) {
return renderBenchmark(req, res, out, { shared: true });
});
++ // ---- a person's own benchmarks: the default audience, "apply to all", and each Share button -----------------
++ const myBenchmarksPage = async (req, res, extra = {}) => {
++ const subject = subjectOf(req);
++ if (!subject) return res.redirect(302, `/auth/login?next=${encodeURIComponent('/benchmarks/mine')}`);
++ const mine = await registry.listBenchmarks({ owner: subject, limit: 200, viewer: subject });
++ const counts = await registry.runCounts();
++ for (const b of mine) {
++ b.runs = counts.get(b.id) || 0;
++ // The column says private for both Only me and Specific people; the share row names the real audience.
++ const row = ctx.sharing ? await ctx.sharing.shareOf(b.id).catch(() => null) : null;
++ if (row) b.audience = row.audience;
++ }
++ const defaultAudience = ctx.sharing ? await ctx.sharing.defaultAudience(subject) : 'private';
++ return page(req, res, {
++ index: false, cache: null,
++ title: 'Your benchmarks',
++ description: 'Your benchmarks: the default audience for new ones, and who can open each one.',
++ body: raw(String(results.myBenchmarks({ benchmarks: mine, defaultAudience, baseUrl: base(), ...extra }))),
++ }, extra.status || 200);
++ };
++ r.get('/benchmarks/mine', async (req, res) => {
++ const saved = req.query.saved === '1';
++ const changed = Number.parseInt(req.query.changed, 10) || 0;
++ const notice = saved
++ ? `Sharing preference saved.${changed ? ` Updated ${changed} existing ${changed === 1 ? 'benchmark' : 'benchmarks'}.` : ''}`
++ : null;
++ return myBenchmarksPage(req, res, notice ? { notice } : {});
++ });
++ r.post('/benchmarks/mine/default', express.urlencoded({ extended: false, limit: '16kb' }), ctx.principal.middleware, async (req, res) => {
++ if (!sameOrigin(req, config)) return res.status(403).type('text').send('This form only works from Bench itself.');
++ const subject = subjectOf(req);
++ if (!subject) return res.redirect(303, `/auth/login?next=${encodeURIComponent('/benchmarks/mine')}`);
++ const body = req.body && typeof req.body === 'object' ? req.body : {};
++ const audience = String(body.audience || '');
++ if (!ctx.sharing || !ctx.sharing.AUDIENCES.includes(audience)) {
++ return myBenchmarksPage(req, res, { status: 422, error: 'Choose Everyone, Anyone with the link, Specific people, or Only me.' });
++ }
++ await ctx.sharing.setDefaultAudience(subject, audience);
++ const changed = body.apply === '1' ? await ctx.sharing.applyDefault(subject, audience) : 0;
++ const query = new URLSearchParams({ saved: '1', changed: String(changed) });
++ return res.redirect(303, `/benchmarks/mine?${query}`);
++ });
++
// The owner shares, unshares or publishes: a plain same-origin form post.
r.post('/benchmarks/:ref/share', express.urlencoded({ extended: false, limit: '16kb' }), ctx.principal.middleware, async (req, res) => {
if (!sameOrigin(req, config)) return res.status(403).type('text').send('This form only works from Bench itself.');
@@@ -275,6 +321,14 @@ function createPageRoutes(ctx) {
r.get('/benchmarks/:ref', async (req, res) => {
const out = await readableBenchmark(req, req.params.ref);
if (!out) return notFound(req, res, 'benchmark');
++ // The public page moved to Report (2026-11-08), but Report reads only the public API: its owner, a named
++ // reader and an unlisted link keep the page here.
++ const subject = subjectOf(req);
++ const owner = subject != null && String(out.benchmark.owner) === String(subject);
++ if (!owner && out.benchmark.visibility === 'public') {
++ const report = String(config.reportUrl || 'https://openvibe.report').replace(/\/+$/, '');
++ return res.redirect(301, `${report}/ai/${encodeURIComponent(out.benchmark.id)}`);
++ }
return renderBenchmark(req, res, out);
});
ddiff --git a/server/http/redirect.js b/server/http/redirect.js
iindex 447d825..a59c872 100644
---- a/server/http/redirect.js
++++ b/server/http/redirect.js
@@@ -7,11 +7,16 @@
*
* / → /ai
* /benchmarks → /ai
-- * /benchmarks/<id>[/…] → /ai/<id>
++ * /benchmarks/<id> → /ai/<id> when it is public and the viewer is not its owner; otherwise the page
++ * stays here, because Report reads only Bench's public API (a private benchmark,
++ * a named reader and a share link have no Report page).
* /models/<model> → /ai/models/<model>
* /compare → /ai/compare
* /docs → /ai/method
* /showcase, /gallery, … → /ai
++ *
++ * The owner's pages stay here: /benchmarks/mine (the default audience and the person's own list), /benchmarks/new,
++ * an unlisted share link (/b/<token>), and the noindex task and run pages.
*/
const DEFAULT_REPORT = 'https://openvibe.report';
@@@ -21,7 +26,11 @@ const decode = (s) => { try { return decodeURIComponent(s); } catch { return s;
const KEEP = [/^\/render(?:\/|$)/, /^\/vendor(?:\/|$)/, /^\/badge(?:\/|$)/, /^\/embed(?:\/|$)/,
/^\/api(?:\/|$)/, /^\/mcp(?:\/|$)/, /^\/auth(?:\/|$)/, /^\/shared(?:\/|$)/, /^\/\.well-known(?:\/|$)/,
/^\/metrics(?:\/|$)/, /^\/release(?:\.json|-metrics)?(?:\/|$)/, /^\/robots\.txt$/, /^\/sitemap\.xml$/,
-- /^\/favicon\.ico$/, /^\/assets(?:\/|$)/];
++ /^\/favicon\.ico$/, /^\/assets(?:\/|$)/,
++ // The owner's pages and the pages Report cannot render: the person's list and default, the create form, an
++ // unlisted share link, and the noindex task/run pages. /benchmarks/<id> itself is decided by its page route.
++ /^\/benchmarks\/mine(?:\/|$)/, /^\/benchmarks\/new(?:\/|$)/,
++ /^\/benchmarks\/[^/]+\/(?:run|tasks\/[^/]+)(?:\/|$)/, /^\/b\/[^/]+$/];
function targetFor(pathname, reportUrl) {
const base = String(reportUrl || DEFAULT_REPORT).replace(/\/+$/, '');
@@@ -30,15 +39,19 @@ function targetFor(pathname, reportUrl) {
if (p === '/docs' || p === '/docs/') return `${base}/ai/method`;
if (p === '/compare' || p === '/compare/') return `${base}/ai/compare`;
if (p === '/benchmarks' || p === '/benchmarks/') return `${base}/ai`;
-- if (p === '/benchmarks/new' || p === '/benchmarks/new/') return `${base}/ai`;
-- const bench = /^\/benchmarks\/([^/]+)(?:\/.*)?$/.exec(p);
++ // A benchmark's own page decides: public pages redirect from the route (which knows the viewer and the
++ // benchmark's owner), while a private benchmark, a named reader and an unlisted link stay on Bench.
++ const bench = /^\/benchmarks\/([^/]+)\/.+$/.exec(p);
if (bench) return `${base}/ai/${encodeURIComponent(decode(bench[1]))}`;
const model = /^\/models\/(.+)$/.exec(p);
if (model) return `${base}/ai/models/${encodeURIComponent(decode(model[1]))}`;
if (/^\/(?:showcase|gallery|pick|battle|ratings)(?:\/|$)/.test(p)) return `${base}/ai`;
-- if (/^\/b\/[^/]+$/.test(p)) return `${base}/ai`;
if (/^\/runs\/[^/]+$/.test(p)) return `${base}/ai`;
if (p === '/llms.txt' || p === '/llms-full.txt') return `${base}${p}`;
++ // A Markdown twin of a public benchmark page follows the page to Report. The detail page itself is decided by
++ // its route (an owner or a named reader keeps it); its twin is a signed-out render, so it stays public-only.
++ const benchTwin = /^\/benchmarks\/([^/]+)\.md$/.exec(p);
++ if (benchTwin) return `${base}/ai/${encodeURIComponent(decode(benchTwin[1]))}.md`;
// A Markdown twin of a moved page: its Report twin is the moved page's own twin. An unknown twin is still a 404.
if (p.endsWith('.md')) {
const page = p === '/index.md' ? '/' : p.slice(0, -3);
ddiff --git a/server/views/results.js b/server/views/results.js
iindex 4a99a40..291cbfe 100644
---- a/server/views/results.js
++++ b/server/views/results.js
@@@ -13,6 +13,9 @@ const kit = require('openvibe-shared/kit');
const MEDIA_ORIGIN = 'https://openvibe.media';
++/** The four audiences in the words the Share sheet uses; Bench's visibility column maps unlisted to link. */
++const AUDIENCE_LABEL = { public: 'Everyone', link: 'Anyone with the link', people: 'Specific people', private: 'Only me', unlisted: 'Anyone with the link' };
++
// ---- numbers -------------------------------------------------------------------------------------------------
const fmtScore = (v) => (v == null ? '—' : (Math.round(Number(v) * 10) / 10).toFixed(1).replace(/\.0$/, ''));
@@@ -329,16 +332,54 @@ function benchmarkCard(b) {
return html`<li class="br-card"><a class="br-card-link" href="${benchmarkHref(b.id)}"><span class="br-card-top">${kindChip(b.kind)}<span class="br-card-v">v${b.latest_version || 0}</span>${vis}</span><span class="br-card-title">${b.title}</span><span class="br-card-desc">${b.description}</span><span class="br-card-foot">${plural(tasks, 'task')}${b.runs != null ? html` · ${plural(b.runs, 'run')}` : ''}${b.parent ? ' · a fork' : ''}</span></a></li>`;
}
++/**
++ * A person's own page: the default audience for new benchmarks, "apply to all", and a Share button on each one.
++ * The page stays on Bench because Report reads only the public API: a private or unlisted benchmark, and the
++ * person's own default, have no Report page.
++ */
++function myBenchmarks({ benchmarks = [], defaultAudience = 'private', baseUrl = '', notice = null, error = null } = {}) {
++ const audiences = [
++ ['public', 'Everyone'],
++ ['link', 'Anyone with the link'],
++ ['people', 'Specific people'],
++ ['private', 'Only me'],
++ ];
++ const label = Object.fromEntries(audiences);
++ const visibleLabel = { public: 'Everyone', unlisted: 'Anyone with the link', private: 'Only me' };
++ const options = audiences.map(([value, text]) => html`<option value="${value}"${value === defaultAudience ? raw(' selected') : ''}>${text}</option>`);
++ return html`<div class="bm-wrap br" id="br-mine">
++<header class="bm-page-h"><div class="bm-head-row"><div><p class="bm-kicker">Your benchmarks</p><h1>Your benchmarks</h1>
++<p class="bm-lede">Private ones are yours alone. An unlisted one opens with its link; a public one is also on OpenVibe.Report's AI page.</p></div><a class="bm-btn" href="/benchmarks/new">Make your own</a></div></header>
++${notice ? html`<p class="bm-note" role="status">${notice}</p>` : ''}
++${error ? html`<div class="bm-note is-bad" role="alert">${error}</div>` : ''}
++<section class="bm-section" aria-labelledby="br-default-h"><div class="bm-head-row"><h2 id="br-default-h">New benchmarks</h2><span class="br-chip br-vis">${label[defaultAudience] || 'Only me'}</span></div>
++<p class="br-fine">Who can open a benchmark you make from now on. Each one's Share button can still change just that one.</p>
++<form class="br-form" method="post" action="/benchmarks/mine/default">
++<label class="br-field"><span>New benchmarks</span><select name="audience">${options}</select></label>
++<label class="br-check"><input type="checkbox" name="apply" value="1"> Apply to all my existing benchmarks</label>
++<button class="bm-btn" type="submit">Save sharing preference</button>
++</form></section>
++<section class="bm-section" aria-labelledby="br-mine-h"><div class="bm-head-row"><h2 id="br-mine-h">Your benchmarks</h2><span class="bm-count">${benchmarks.length}</span></div>
++${benchmarks.length ? html`<ul class="br-cards">${benchmarks.map((b) => html`<li class="br-card"><a class="br-card-link" href="${benchmarkHref(b.id)}"><span class="br-card-top">${kindChip(b.kind)}<span class="br-card-v">v${b.latest_version || 0}</span><span class="br-chip br-vis">${label[b.audience] || visibleLabel[b.visibility] || 'Only me'}</span></span><span class="br-card-title">${b.title}</span><span class="br-card-desc">${b.description || ''}</span><span class="br-card-foot">${plural((((b.draft || {}).tasks) || []).length, 'task')}${b.runs != null ? html` · ${plural(b.runs, 'run')}` : ''}</span></a>
++<p class="br-share-sheet">${raw(kit.share({ title: b.title, url: `${String(baseUrl || '').replace(/\/+$/, '')}/benchmarks/${encodeURIComponent(b.id)}`, text: b.description || '', kind: 'benchmark', id: b.id }))}</p></li>`)}</ul>`
++ : empty('No benchmarks yet', 'Paste 5 to 50 prompts you really use as a private benchmark.', html`<a class="bm-btn bm-btn-small" href="/benchmarks/new">Make your own</a>`)}</section>
++</div>`;
++}
++
/** The owner's sharing controls: private, an unlisted link, or public. Plain form posts, no JavaScript. */
--function shareBox({ benchmark, shareUrl = null, error = null, baseUrl = '' }) {
++function shareBox({ benchmark, shareUrl = null, error = null, baseUrl = '', audience = null }) {
const visibility = benchmark.visibility || 'private';
-- const note = visibility === 'private'
-- ? 'Private: only you can see this benchmark and its prompts. No link works.'
-- : visibility === 'unlisted'
-- ? 'Unlisted: anyone with the link can see it. It is not in any list, search, sitemap or the open data.'
-- : 'Public: it is in the benchmark list, the sitemap and the open data, with every run published.';
++ const seenBy = (audience === 'unlisted' ? 'link' : audience) || visibility;
++ const chip = AUDIENCE_LABEL[seenBy] || 'Only me';
++ const note = seenBy === 'private'
++ ? 'Only me: only you can see this benchmark and its prompts. No link works.'
++ : seenBy === 'people'
++ ? 'Specific people: only you and the people named in the Share sheet can open it. It is not in any list, search, sitemap or the open data.'
++ : seenBy === 'link'
++ ? 'Anyone with the link: anyone who has it can see this benchmark. It is not in any list, search, sitemap or the open data.'
++ : 'Everyone: it is in the benchmark list, the sitemap and the open data, with every run published.';
return html`<section class="bm-section br-share" id="br-share" aria-labelledby="br-share-h">
--<div class="br-fold br-share-details"><input class="br-fold-toggle" type="checkbox" id="br-share-open"><label class="br-fold-h" for="br-share-open"><h2 id="br-share-h">Sharing</h2><span class="br-chip br-vis">${visibility}</span><span class="br-fold-mark" aria-hidden="true"></span></label>
++<div class="br-fold br-share-details"><input class="br-fold-toggle" type="checkbox" id="br-share-open"><label class="br-fold-h" for="br-share-open"><h2 id="br-share-h">Sharing</h2><span class="br-chip br-vis">${chip}</span><span class="br-fold-mark" aria-hidden="true"></span></label>
<div class="br-fold-body">
<p class="br-fine">${note}</p>
<p class="br-share-sheet">${raw(kit.share({ title: benchmark.title, url: `${String(baseUrl || '').replace(/\/+$/, '')}/benchmarks/${encodeURIComponent(benchmark.id)}`, text: benchmark.description || '', kind: 'benchmark', id: benchmark.id }))}</p>
@@@ -356,7 +397,7 @@ ${shareUrl ? html`<div class="bm-note br-share-now" role="status"><p>Your unlist
/** The new-benchmark form: 5–50 pasted prompts, one per line or CSV with an optional expected answer. */
function newBenchmarkPage({ kinds = [], values = {}, error = null, signedIn = false, loginUrl = '/auth/login' }) {
-- const head = html`<nav class="br-crumbs" aria-label="Breadcrumb"><a href="/benchmarks">Benchmarks</a><span aria-hidden="true">/</span><span>Make your own</span></nav>
++ const head = html`<nav class="br-crumbs" aria-label="Breadcrumb"><a href="${signedIn ? '/benchmarks/mine' : '/benchmarks'}">${signedIn ? 'Your benchmarks' : 'Benchmarks'}</a><span aria-hidden="true">/</span><span>Make your own</span></nav>
<header class="bm-page-h"><p class="bm-kicker">Your prompts</p><h1>Make a benchmark from your own prompts</h1>
<p class="bm-lede">Paste 5 to 50 prompts you really use. They become the tasks of a private benchmark: no model is called and nothing is charged until you run it on openvibe.codes.</p></header>`;
if (!signedIn) {
@@@ -386,7 +427,7 @@ ${error ? html`<div class="bm-note is-bad" role="alert">${error}</div>` : ''}
</div>`;
}
--function benchmarkPage({ benchmark, definition, version, rankings, metric = 'score', runs = [], codesRunUrl = null, viewer = null, baseUrl = '', ownBoards = null, share = null, shared = false, owner = false, since = null, allTimeEntries = null }) {
++function benchmarkPage({ benchmark, definition, version, rankings, metric = 'score', runs = [], codesRunUrl = null, viewer = null, baseUrl = '', ownBoards = null, share = null, shared = false, owner = false, since = null, allTimeEntries = null, audience = null }) {
const d = definition || {};
const tasks = Array.isArray(d.tasks) ? d.tasks : [];
const freshSets = [...new Set(tasks.map((t) => (t && t.added_on ? String(t.added_on).slice(0, 7) : null)).filter(Boolean))].sort().reverse();
@@@ -403,19 +444,26 @@ function benchmarkPage({ benchmark, definition, version, rankings, metric = 'sco
? html`<a class="bm-btn" href="${benchmarkHref(benchmark.id)}/run">Run it yourself</a>`
: html`<a class="bm-btn bm-btn-quiet" href="/docs#br-publish">Publish a run</a>`;
const byline = owner ? 'by you' : shared ? 'by its owner' : `by ${benchmark.owner}`;
-- const privateNote = benchmark.visibility === 'private'
-- ? html`<div class="bm-note br-private" role="status"><b>Private.</b> Only you can see this benchmark and its prompts. It is not in any list, search or the open data.</div>`
-- : shared
-- ? html`<div class="bm-note br-shared" role="status"><b>Shared by its owner.</b> This page is not in any list or search engine. The link can stop working if the owner makes it private.</div>`
-- : '';
++ const seenBy = (audience === 'unlisted' ? 'link' : audience) || benchmark.visibility || 'private';
++ const audienceLabel = AUDIENCE_LABEL[seenBy] || 'Only me';
++ const readerNote = html`<div class="bm-note br-shared" role="status"><b>Shared by its owner.</b> This page is not in any list or search engine. The owner can remove your access at any time.</div>`;
++ const privateNote = !owner && seenBy !== 'public'
++ ? readerNote
++ : seenBy === 'private'
++ ? html`<div class="bm-note br-private" role="status"><b>Only me.</b> Only you can see this benchmark and its prompts. It is not in any list, search or the open data.</div>`
++ : seenBy === 'people'
++ ? html`<div class="bm-note br-private" role="status"><b>Specific people.</b> Only you and the people named in Share can open this benchmark. It is not in any list, search or the open data.</div>`
++ : seenBy === 'link'
++ ? html`<div class="bm-note br-private" role="status"><b>Anyone with the link.</b> Anyone who has the link can open this benchmark. It is not in any list, search or the open data.</div>`
++ : '';
return html`<div class="bm-wrap br" id="br-benchmark">
--<nav class="br-crumbs" aria-label="Breadcrumb"><a href="/benchmarks">Benchmarks</a><span aria-hidden="true">/</span><span>${benchmark.title}</span></nav>
--<header class="br-head"><div class="br-head-main"><p class="br-head-meta">${kindChip(benchmark.kind)}<span>v${version || benchmark.latest_version || 0}</span><span>${byline}</span>${benchmark.visibility !== 'public' ? html`<span class="br-chip br-vis">${benchmark.visibility}</span>` : ''}${benchmark.parent ? html`<a href="${benchmarkHref(benchmark.parent)}">forked</a>` : ''}</p>
++<nav class="br-crumbs" aria-label="Breadcrumb"><a href="${owner ? '/benchmarks/mine' : '/benchmarks'}">${owner ? 'Your benchmarks' : 'Benchmarks'}</a><span aria-hidden="true">/</span><span>${benchmark.title}</span></nav>
++<header class="br-head"><div class="br-head-main"><p class="br-head-meta">${kindChip(benchmark.kind)}<span>v${version || benchmark.latest_version || 0}</span><span>${byline}</span>${seenBy !== 'public' ? html`<span class="br-chip br-vis">${audienceLabel}</span>` : ''}${benchmark.parent ? html`<a href="${benchmarkHref(benchmark.parent)}">forked</a>` : ''}</p>
<h1>${benchmark.title}</h1>${benchmark.description ? html`<p class="bm-lede">${benchmark.description}</p>` : ''}</div>
<div class="br-head-act">${runYourself}<p class="br-fine">${canRun ? 'On openvibe.codes, with your own keys or the free pool through OpenVibe.AI. The result is published here.' : codesRunUrl ? 'Only the owner starts runs on this benchmark. Its results are readable here.' : 'Running it on openvibe.codes with your own keys is coming. Today a run is published through the API.'}</p></div></header>
${privateNote}
<dl class="br-facts"><div><dt>Tasks</dt><dd>${tasks.length}${hidden ? html` <span class="br-fine">(${hidden} hidden)</span>` : ''}</dd></div><div><dt>Judged by</dt><dd>${judges.length ? judges.map((j) => String(j.type).replace(/_/g, ' ')).join(', ') : '—'}</dd></div><div><dt>Limits</dt><dd>${[limits.ms ? fmtMs(limits.ms) : null, limits.tokens ? `${fmtTokens(limits.tokens)} tokens` : null, limits.usd ? fmtUsd(limits.usd) : null].filter(Boolean).join(' · ') || '—'}</dd></div><div><dt>Verified runs</dt><dd>${entries.reduce((n, e) => n + e.runs, 0)}</dd></div></dl>
--${owner ? shareBox({ benchmark, shareUrl: share && share.shareUrl, error: share && share.error, baseUrl }) : ''}
++${owner ? shareBox({ benchmark, shareUrl: share && share.shareUrl, error: share && share.error, baseUrl, audience: seenBy }) : ''}
<div class="br-sections">
<nav class="br-sec-pick" aria-label="Benchmark sections"><a href="#br-rank">Rankings</a><a href="#br-tasks">Tasks</a><a href="#br-runs">Runs</a></nav>
${ownBoards
@@@ -574,7 +622,7 @@ ${modelUseThis({ baseUrl, model, rows, viewer })}
}
module.exports = {
-- benchmarksIndex, benchmarkPage, newBenchmarkPage, promptLeaderboard, promptBoards, shareBox,
++ benchmarksIndex, benchmarkPage, newBenchmarkPage, myBenchmarks, promptLeaderboard, promptBoards, shareBox,
runForm, runPage, comparePage, modelPage, taskPage, valueChart, METRICS,
fmtMinutes, horizonText, horizonLine,
fmtScore, fmtUsd, fmtMs, fmtTokens, ordinal, benchmarkCard, render,
ddiff --git a/test/bench-share-defaults.test.js b/test/bench-share-defaults.test.js
iindex 272c640..6b85684 100644
---- a/test/bench-share-defaults.test.js
++++ b/test/bench-share-defaults.test.js
@@@ -59,6 +59,56 @@ const { boot, check, done } = require('./helpers/boot');
});
assert.strictEqual(bad.status, 403);
});
++
++ await check('the owner page carries the default, "apply to all" and each Share button', async () => {
++ const id = await make(alice, 'Owner page benchmark');
++ const mine = await t.get('/benchmarks/mine', { as: alice });
++ assert.strictEqual(mine.status, 200, mine.text.slice(0, 200));
++ assert.ok(mine.text.includes('New benchmarks') && mine.text.includes('name="audience"'));
++ assert.ok(mine.text.includes('Apply to all my existing benchmarks'));
++ assert.ok(mine.text.includes('Owner page benchmark'));
++ assert.ok(mine.text.includes(`data-kind="benchmark" data-id="${id}"`), 'the Share sheet button names the benchmark');
++ assert.strictEqual((await t.get('/benchmarks/mine')).status, 302, 'signed out, the owner page signs in first');
++
++ const saved = await t.get('/benchmarks/mine/default', {
++ as: alice, method: 'POST',
++ body: new URLSearchParams({ audience: 'link', apply: '1' }).toString(),
++ headers: { 'content-type': 'application/x-www-form-urlencoded' },
++ });
++ assert.strictEqual(saved.status, 303, saved.text);
++ assert.ok(String(saved.headers.get('location')).startsWith('/benchmarks/mine?'));
++ assert.strictEqual((await row(id)).audience, 'link');
++ const after = await t.get(saved.headers.get('location'), { as: alice });
++ assert.ok(after.text.includes('Sharing preference saved'), 'the saved notice is shown');
++ assert.strictEqual((await t.get('/benchmarks/new', { as: alice })).status, 200, 'the create form stays on Bench');
++ });
++
++ await check('a public benchmark still moves to Report; a private one and a share link stay here', async () => {
++ const id = await make(alice, 'Moved public benchmark');
++ const madePublic = await t.get(`/benchmarks/${id}/share`, {
++ as: alice, method: 'POST', body: new URLSearchParams({ visibility: 'public' }).toString(),
++ headers: { 'content-type': 'application/x-www-form-urlencoded' },
++ });
++ assert.strictEqual(madePublic.status, 200, madePublic.text.slice(0, 200));
++ const anon = await t.get(`/benchmarks/${id}`);
++ assert.strictEqual(anon.status, 301);
++ assert.strictEqual(anon.headers.get('location'), `https://openvibe.report/ai/${id}`);
++ const owner = await t.get(`/benchmarks/${id}`, { as: alice });
++ assert.strictEqual(owner.status, 200, 'the owner keeps the page with the Share controls');
++ assert.ok(owner.text.includes('Sharing'));
++
++ await put(alice, 'private');
++ const hidden = await make(alice, 'Private benchmark');
++ assert.strictEqual((await t.get(`/benchmarks/${hidden}`)).status, 404);
++ assert.strictEqual((await t.get(`/benchmarks/${hidden}`, { as: bob })).status, 404);
++ assert.strictEqual((await t.get(`/benchmarks/${hidden}`, { as: alice })).status, 200);
++
++ const unlisted = await t.ctx.registry.setVisibility(hidden, { visibility: 'unlisted', subject: alice.subject });
++ assert.strictEqual(unlisted.ok, true);
++ const link = await t.get(unlisted.share_path);
++ assert.strictEqual(link.status, 200, 'an unlisted share link opens on Bench, not Report');
++ assert.ok(link.text.includes('Private benchmark'));
++ });
} finally { await t.close(); }
done();
})().catch((error) => { console.error(error); process.exit(1); });