openvibers/openvibe.reviews Commits

Reviews: the front page is a scoreboard (score, meter and what it rests on), with six features (#50)

OP OpenVibers committed 29 days ago · 12f6833433 · 3 files, +47 −7

**The Reviews front page is a scoreboard.**

**Before:** under "Entities with source data", each game was a link above a grey line such as "99.7% recommend (297 of 298)". The four feature cards also left one card alone on a second row.

**Now:**
- **"What is reviewed":** a lede says each score comes from named sources and what opening one shows. Each entity is a card with its name and kind, its headline number in large type, and a meter for it.
  - The headline number is the recommendation share; failing that, the rating on its scale ("4.2 out of 5"); failing that, the rating share.
  - Under it is what the number rests on ("39 of 40 reviews"), plus the rating when an entity has both.
  - An entity with no aggregate says "No score yet: no source has said enough."
- **Six features instead of four**, so the grid fills two rows. Both new ones are true today:
  - "Corrections from readers": any signed-in person can send one, and an accepted one is published with its note;
  - "Feeds and an API": Atom and JSON Feed, and `/api/v1`.
- The board uses the showcase's section heading, so it matches the sections above. "N entities in total" now reads "N reviewed in total".

**Checked** locally with seven Steam-like games at 1280 and 390 px: no horizontal overflow, tags never break, and the meters line up.

**Tests:** layout, nojs and signals pass. `perf-budget` is raised as a decision: HTML 28.5 / 7 KB (measured 27.0 / 6.6; the two new cards carry inline ring icons) and CSS 20.5 / 5 KB (19.4 / 4.6).

Changes

ddiff --git a/public/css/reviews.css b/public/css/reviews.css
iindex 619a2e0..93a374d 100644
---- a/public/css/reviews.css
++++ b/public/css/reviews.css
@@@ -73,6 +73,18 @@ table { border-collapse: collapse; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
/* Reviews: aggregate, signals, summary */
.rv-entities { list-style: none; padding: 0; }
++/* The front page's board: one card per entity, its score with a meter and what it rests on. */
++.rv-board { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); }
++.rv-card { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border: 1px solid var(--rv-line); border-radius: 12px; background: var(--rv-panel); color: var(--rv-text); text-decoration: none; }
++.rv-card:hover { border-color: var(--rv-accent); }
++.rv-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
++.rv-card-head b { font-size: 1.02rem; line-height: 1.3; }
++.rv-card-head .rv-tag { flex: none; white-space: nowrap; }
++.rv-score { display: flex; align-items: baseline; gap: 6px; margin-top: auto; }
++.rv-big { font-size: 1.7rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
++.rv-meter { display: block; height: 6px; border-radius: 3px; background: var(--rv-line); overflow: hidden; }
++.rv-meter > span { display: block; height: 100%; border-radius: 3px; background: var(--rv-accent); }
++.rv-basis { font-size: 13px; color: var(--rv-muted); }
.rv-entities li { padding: 8px 0; border-top: 1px solid var(--rv-line); }
.rv-agg-box { background: var(--rv-panel); border: 1px solid var(--rv-line); border-radius: 8px; padding: 10px 14px; }
.rv-agg { font-size: 1.15rem; margin: 4px 0; }
ddiff --git a/server/render/views.js b/server/render/views.js
iindex 0d4f5ba..1afe614 100644
---- a/server/render/views.js
++++ b/server/render/views.js
@@@ -86,16 +86,44 @@ function reviewsShowcase() {
{ icon: 'ov:check', title: 'An aggregate you can check', text: 'The inputs and the method are listed. An editor can leave a source out only with the reason shown.' },
{ icon: 'ov:history', title: 'Changes on the record', text: 'Replaced and withdrawn signals stay in the history, beside every revision of the aggregate.' },
{ icon: 'ov:docs', title: 'Summaries by people', text: 'Written or approved by an editor, citing the signals they rest on, and never carrying a rating.' },
++ { icon: 'ov:shield', title: 'Corrections from readers', text: 'Anyone signed in with OpenVibe can send the editors a correction; an accepted one is published with its note.' },
++ { icon: 'ov:feeds', title: 'Feeds and an API', text: 'New and changed scores arrive in Atom and JSON Feed, and the API serves the same data to OpenVibe apps.' },
],
});
}
++/**
++ * One entity on the front page's board: its headline number (the recommendation share, else the rating on its scale,
++ * else the rating share), a meter for it, and how many signals it rests on. No aggregate: the card says so.
++ */
++function boardCard(x) {
++ const c = x.aggregate && x.aggregate.result ? x.aggregate.result.components : null;
++ let big = null;
++ let label = '';
++ let share = null;
++ let basis = '';
++ if (c && c.recommendation) {
++ big = `${c.recommendation.percent}%`; label = 'recommend'; share = c.recommendation.percent;
++ basis = `${n(c.recommendation.positive)} of ${n(c.recommendation.total)} reviews`;
++ } else if (c && c.rating && c.rating.on_scale) {
++ big = `${c.rating.on_scale.value}`; label = `out of ${c.rating.on_scale.best}`; share = (100 * c.rating.on_scale.value) / c.rating.on_scale.best;
++ basis = `${n(c.rating.count)} ratings`;
++ } else if (c && c.rating) {
++ big = `${c.rating.percent}%`; label = 'rating'; share = c.rating.percent;
++ basis = `${n(c.rating.count)} ratings`;
++ }
++ const pct = share == null ? 0 : Math.max(0, Math.min(100, Number(share)));
++ return html`<li><a class="rv-card" href="${epath(x.entity)}"><span class="rv-card-head"><b>${x.entity.name}</b> <span class="rv-tag">${KIND_LABEL[x.entity.kind]}</span></span>${big ? html`<span class="rv-score"><span class="rv-big">${big}</span> <span class="rv-muted">${label}</span></span>
++<span class="rv-meter" aria-hidden="true"><span style="width:${pct.toFixed(1)}%"></span></span><span class="rv-basis">${basis}${c && c.recommendation && c.rating ? html` · ${c.rating.on_scale ? `${c.rating.on_scale.value} / ${c.rating.on_scale.best}` : `${c.rating.percent}%`} from ${n(c.rating.count)} ratings` : ''}</span>`
++ : html`<span class="rv-basis">No score yet: no source has said enough.</span>`}</a></li>`;
++}
++
function home({ entities, total, editor }) {
return html`${raw(reviewsShowcase())}
--<section id="entities"><h2>Entities with source data</h2>
--${entities.length ? html`<ul class="rv-entities">${entities.map((x) => html`<li><a href="${epath(x.entity)}"><strong>${x.entity.name}</strong></a> <span class="rv-tag">${KIND_LABEL[x.entity.kind]}</span><br><span class="rv-muted">${aggregateLine(x.aggregate)}</span></li>`)}</ul>`
++<section id="entities" class="sc-sec" aria-labelledby="rv-board-h"><h2 id="rv-board-h">What is reviewed</h2>
++${entities.length ? html`<p class="sc-lede">Each score comes from named sources. Open one to see every signal behind it, when it was read, and how the number was worked out.</p><ul class="rv-board">${entities.map(boardCard)}</ul>`
: html`<p class="rv-muted">Nothing yet. Entities appear here once a source has stated something about them (or an editor has published a summary).</p>`}
--<p class="rv-muted">${n(total)} entit${total === 1 ? 'y' : 'ies'} in total. <a href="/feed.atom">Atom</a> · <a href="/feed.json">JSON Feed</a>${editor ? html` · <a href="/editor">Editor desk</a>` : ''}</p></section>`;
++<p class="rv-muted">${n(total)} reviewed in total. <a href="/feed.atom">Atom</a> · <a href="/feed.json">JSON Feed</a>${editor ? html` · <a href="/editor">Editor desk</a>` : ''}</p></section>`;
}
function aboutPage() {
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 068c932..b72a9e3 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
-- htmlRawKB: 25, // measured 23.7 (fresh database; 17.8 before the showcase hero and features)
-- htmlBrotliKB: 6.5, // 5.9 (4.6 before the showcase)
++ htmlRawKB: 28.5, // measured 27.0 (fresh database; 23.7 before 2026-10-15's two more features with their ring icons, 17.8 before the showcase)
++ htmlBrotliKB: 7, // 6.6 (5.9, 4.6 before the showcase)
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime)
jsRawKB: 257, // 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 61.5, // 56.3 (49.9 before web-runtime.js)
cssFiles: 2, // 2 (reviews.css, openvibe-shared showcase.css)
-- cssRawKB: 19.5, // 18.2 (6.7 before showcase.css)
-- cssBrotliKB: 4.8, // 4.3 (1.7 before showcase.css)
++ cssRawKB: 20.5, // 19.4 (18.2 before 2026-10-15's scoreboard, 6.7 before showcase.css)
++ cssBrotliKB: 5, // 4.6 (4.3, 1.7 before showcase.css)
externalFiles: 1, // 0
};