openvibers/openvibe.deals Commits

Deals: a product home built with the showcase kit above the hot list. (#10)

OP OpenVibers committed 7 Oct 2026 · 2eb6b15d47 · 8 files, +44 −21

The front page now opens with what OpenVibe.Deals is for, built with the shared showcase kit (openvibe-shared v2.10.1), above the hot list it already had.

- server/render/views.js: dealsShowcase() renders a hero ("Deals where every price says when": people post and vote; every price is an observation with its source and time; old ones say they may be gone; an unstated price stays "not stated"; actions Browse hot deals / Submit a deal) and a "How it works" grid (prices as of a time with their history, server-counted votes with the hotness formula on the deal page, comparison across stores on product pages, watches and saved searches). The copy restates the README and the pages; it makes no promise about notification delivery. offerList takes an optional lead: under it the list heading is an h2 with id deals above the sort tabs (one h1 per page).
- server/http/pages.js: page 1 of / passes the showcase lead and asks for showcase.css; /new and later pages are unchanged.
- server/render/layout.js: renderPage forwards styles to openvibe-publishing renderDocument.
- test/perf-budget.test.js: budgets raised for the showcase (html 17.2 -> 23.5 KB raw; css 5.6 -> 17.1 KB raw for showcase.css) and for openvibe-shared 2.10 shell web-runtime.js (js 212.2 -> 239.1 KB raw).
- package.json / lockfile, README, STATUS.json: openvibe-shared v2.6.0 -> v2.10.1.

Tests: ov test test/layout.test.js test/pages.test.js test/perf-budget.test.js: 3 passed.

Changes

ddiff --git a/README.md b/README.md
iindex aa18a31..5e9634a 100644
---- a/README.md
++++ b/README.md
@@@ -256,7 +256,7 @@ render time, so the cache stays short); signed-in views, forms, API responses, e
- **Packages** (pinned release tarballs): `openvibe-publishing` v1.3.0 (seo gate, JSON-LD, feeds,
sitemaps, index-hooks, discussion, ssr, and the shared ingest/publication chassis — the Sources
client, change cursor and pull loop, signed event consumer, common normalisers, and the publication
-- glue Deals' indexing uses), `openvibe-contracts` v0.97.0, `openvibe-shared` v2.6.0
++ glue Deals' indexing uses), `openvibe-contracts` v0.97.0, `openvibe-shared` v2.10.1
(Frame, app icon, footer, legal, release, metrics, ready), `openvibe-sdk` v0.26.0 (sso, outbox,
inbox, webhook signatures v2, service tokens, per-actor limits).
- **OpenVibe.Network:** SSO (OAuth client `deals`, registered in production), JWKS,
ddiff --git a/STATUS.json b/STATUS.json
iindex b5b5f15..713b1e8 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -11,7 +11,7 @@
"packages": [
"openvibe-publishing v1.3.0",
"openvibe-contracts v0.97.0",
-- "openvibe-shared v2.6.0",
++ "openvibe-shared v2.10.1",
"openvibe-sdk v0.26.0"
],
"capabilities": [
@@@ -36,7 +36,7 @@
"deployed": true,
"deployment": "Deployed with `sudo ovhost deploy deals`: unit openvibe-deals, 127.0.0.1:4840 loopback only; Network OAuth client deals and its grants exist; on 2026-09-23 /api/ready answered 200 and the production database held 0 offers. Deployed internally, not launched: openvibe.deals still serves the OpenVibe.Sites placeholder until a person verifies a deals source's terms and enables it in Sources",
"placeholderInSites": true,
-- "updated": "2026-10-04",
++ "updated": "2026-10-07",
"features": [
"offers with timestamped price observations shown \"as of\" and stale labels",
"unknown prices stay unknown in pages, JSON, JSON-LD, feeds and Search",
ddiff --git a/package-lock.json b/package-lock.json
iindex a2db3fc..0fe9845 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -18,7 +18,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0"
},
"devDependencies": {
@@@ -936,9 +936,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.6.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
-- "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
++ "version": "2.10.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
++ "integrity": "sha512-6fUOw22bSvxg0K33BefAyAkNRkETe9euLNT0CHs2mLIvzYR1q38KiHRUQIWj8FESbVANF0pXMojhBz5TeqpMtg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 1c7f709..692f765 100644
---- a/package.json
++++ b/package.json
@@@ -29,7 +29,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.97.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0"
},
"devDependencies": {
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex dd385cd..59b033f 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -1,4 +1,5 @@
'use strict';
++const showcase = require('openvibe-shared/showcase');
/**
* Public, server-rendered routes — useful without JavaScript (every action is a plain form):
@@@ -119,7 +120,7 @@ function createPages(ctx) {
title: kind === 'hot' ? 'Hot deals' : 'New deals',
description: 'Deals submitted and voted on by the community, each with its source and the time its price was observed.',
// The home page carries the site summary an AI can read (ai-summary meta + WebPage JSON-LD).
-- ...(kind === 'hot' && pager.page === 1 ? { summary: SITE_SUMMARY } : {}),
++ ...(kind === 'hot' && pager.page === 1 ? { summary: SITE_SUMMARY, styles: [showcase.STYLESHEET] } : {}),
decision: pageDecision(canonical, { empty: total === 0, query: ['page'] }),
canonical,
feeds: [{ type: 'rss', href: '/feed.xml', title: 'New deals (RSS)' }, { type: 'atom', href: '/atom.xml', title: 'New deals (Atom)' }, { type: 'json', href: '/feed.json', title: 'New deals (JSON Feed)' }],
@@@ -130,6 +131,7 @@ function createPages(ctx) {
heading: kind === 'hot' ? 'Hot deals' : 'New deals',
intro: kind === 'hot' ? 'Ranked by community votes over time (hotness formula hot@1). Every price shows when it was observed.' : 'The newest deals first. Every price shows when it was observed.',
views: list, pager, urls, tabs: views.tabs(kind),
++ lead: kind === 'hot' && pager.page === 1 ? views.dealsShowcase() : '',
empty: 'No deals have been posted yet. Submit the first one.',
}),
}, { cacheable: true });
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex f92a900..2a28f2b 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -93,6 +93,7 @@ function renderPage(o) {
navLinks: NAV_LINKS.map(({ label, href }) => ({ label, href })),
home: '/',
css: asset('css/deals.css'),
++ styles: o.styles, // openvibe-shared stylesheet names (the home page's showcase.css)
release: RELEASE,
account,
body: o.body,
ddiff --git a/server/render/views.js b/server/render/views.js
iindex 61651c3..0a1c8ed 100644
---- a/server/render/views.js
++++ b/server/render/views.js
@@@ -9,6 +9,7 @@
*/
const { html, raw, renderMarkdown, paginationHtml, breadcrumbsHtml } = require('openvibe-publishing/ssr');
const { AVAILABILITY_LABEL, CONDITIONS, AVAILABILITY } = require('../domain/values');
++const showcase = require('openvibe-shared/showcase');
const pad = (n) => String(n).padStart(2, '0');
function when(ms) {
@@@ -74,9 +75,28 @@ ${latestLine(v)}
</div></li>`;
}
--function offerList({ heading, intro, views, pager, urls, tabs, empty }) {
-- return html`${tabs ? raw(tabs) : ''}
--<h1>${heading}</h1>
++/** The front page opens with what OpenVibe.Deals is for (openvibe-shared/showcase); the hot list follows it. */
++function dealsShowcase() {
++ return showcase.hero({
++ eyebrow: 'OpenVibe.Deals',
++ title: 'Deals where every price', accent: 'says when',
++ lede: 'People post deals and vote them up or down. Every price is an observation with its source and the time it was seen: an old one says it may no longer be available, and a price nobody stated stays "not stated".',
++ actions: [{ label: 'Browse hot deals', href: '#deals', primary: true }, { label: 'Submit a deal', href: '/submit' }],
++ }) + showcase.features({
++ title: 'How it works',
++ items: [
++ { icon: 'ov:history', title: 'Prices as of a time', text: 'Each deal keeps every price it was seen at, newest first, each with its source. Nothing is called current without a fresh one.' },
++ { icon: 'ov:check', title: 'Votes decide what is hot', text: 'One vote per person, counted on the server. A deal page shows its hotness with the formula and the inputs.' },
++ { icon: 'ov:search', title: 'Compare across stores', text: 'A product page lists every offer for it, each with its own latest price and the time it was seen.' },
++ { icon: 'ov:bell', title: 'Watches and saved searches', text: 'Watch words, a product or a price limit for new prices that match, or save a search to run again.' },
++ ],
++ });
++}
++
++function offerList({ heading, intro, views, pager, urls, tabs, empty, lead }) {
++ // Under the showcase the list's heading is an h2 above the sort tabs, so #deals lands on both.
++ return html`${lead ? html`${raw(lead)}<h2 id="deals">${heading}</h2>` : ''}${tabs ? raw(tabs) : ''}
++${lead ? '' : html`<h1>${heading}</h1>`}
${intro ? html`<p class="lede">${intro}</p>` : ''}
${views.length ? html`<ol class="deal-list">${views.map((v) => offerCard(v, urls))}</ol>` : html`<p class="empty">${empty || 'Nothing here yet.'}</p>`}
${raw(pager ? paginationHtml(pager) : '')}`;
@@@ -282,4 +302,4 @@ function message({ heading, text, action }) {
return html`<h1>${heading}</h1><p>${text}</p>${action ? html`<p><a href="${action.href}">${action.label}</a></p>` : ''}`;
}
--module.exports = { offerList, tabs, offerPage, productPage, submitForm, watchesPage, searchPage, modPage, storePage, message, priceText, when };
++module.exports = { dealsShowcase, offerList, tabs, offerPage, productPage, submitForm, watchesPage, searchPage, modPage, storePage, message, priceText, when };
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 3a71032..bd9297b 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: 22, // measured 17.2 (fresh database, 2026-09-29)
-- htmlBrotliKB: 5.5, // 4.5
-- jsFiles: 5, // 4
-- jsRawKB: 240, // 212.2
-- jsBrotliKB: 58, // 49.9
-- cssFiles: 2, // 1
-- cssRawKB: 7, // 5.6
-- cssBrotliKB: 2, // 1.5
++ htmlRawKB: 25, // measured 23.5 (fresh database, 2026-10-07; 17.2 before the showcase hero and features)
++ htmlBrotliKB: 6.5, // 6.0 (4.5 before the showcase)
++ jsFiles: 5, // 5 (openvibe-shared 2.10's shell adds web-runtime.js)
++ jsRawKB: 250, // 239.1 (212.2 before web-runtime.js)
++ jsBrotliKB: 60, // 56.3 (49.9 before web-runtime.js)
++ cssFiles: 2, // 2 (deals.css, openvibe-shared showcase.css)
++ cssRawKB: 18.5, // 17.1 (5.6 before showcase.css)
++ cssBrotliKB: 4.6, // 4.1 (1.5 before showcase.css)
externalFiles: 1, // 0
};