openvibers/openvibe.shoes Commits
Open.Shoes by name; Home and Drops draw their icons (#47)
Changes
@@@ -3,7 +3,7 @@.skip { position: absolute; left: -9999px; }.skip:focus { left: 12px; top: 12px; z-index: 99; padding: 8px 12px; border-radius: 10px; background: var(--bg-card, #0f1729); color: inherit; }--/* OpenVibe.Shoes — the small set of page pieces the kit does not draw (tables, forms, the price chart).++/* Open.Shoes — the small set of page pieces the kit does not draw (tables, forms, the price chart).The room's look and every card, rail and scene come from kit.css, room.css and showcase.css. */.sh-wrap { width: min(1180px, 100% - 32px); margin: 0 auto; }.sh-page { padding: 28px 0 64px; }@@@ -1,7 +1,7 @@'use strict';/**-- * OpenVibe.Shoes — Express app factory; server/index.js listens, tests build their own instance with a temp++ * Open.Shoes — Express app factory; server/index.js listens, tests build their own instance with a temp* database and a mock Network.** /, /shoes, /shoe/:slug, /drops, /track, /watches, /developers the pages (http/pages.js)@@@ -112,7 +112,7 @@ async function createApp(opts = {}) {app.use(cookieParser());// ── Machine endpoints ───────────────────────────────────-- app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-shoes', name: 'OpenVibe.Shoes', version: VERSION }));++ app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-shoes', name: 'Open.Shoes', version: VERSION }));release.mount(app, { registry: metrics.registry });const readiness = createServiceReadiness({ s, config, release: release.release, valkey });app.get('/api/ready', readiness.handler);@@@ -123,7 +123,7 @@ async function createApp(opts = {}) {// ── Sign-in (OAuth2 + PKCE client of OpenVibe.Network) ──app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, standardHeaders: true, legacyHeaders: false }));app.use('/auth', sso.routes());-- { const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'shoes', service: 'shoes', host: 'open.shoes', name: 'OpenVibe.Shoes', profile: 'ugc' })); }++ { const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'shoes', service: 'shoes', host: 'open.shoes', name: 'Open.Shoes', profile: 'ugc' })); }// ── OpenVibe.Events → this service (loopback only) ──────// Account export/deletion; the consumer reads the raw body itself, so it is mounted before any body parser.@@@ -159,7 +159,7 @@ async function createApp(opts = {}) {// eslint-disable-next-line no-unused-varsapp.use((err, req, res, _next) => {-- log.error('[OpenVibe.Shoes]', err && err.message ? err.message.slice(0, 300) : err);++ log.error('[Open.Shoes]', err && err.message ? err.message.slice(0, 300) : err);if (res.headersSent) return;res.set('Cache-Control', cache.htmlHeaders({ private: true }));if (err && err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'request.malformed_json', { ctx: req.ov });@@@ -34,7 +34,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })const ks = await client.keys();const first = ks && ks[0];mirror = first ? { pem: first.key.export({ type: 'spki', format: 'pem' }), count: ks.length } : null;-- } catch (err) { log.warn(`[OpenVibe.Shoes] JWKS mirror refresh failed: ${err && err.message || err}`); mirror = null; }++ } catch (err) { log.warn(`[Open.Shoes] JWKS mirror refresh failed: ${err && err.message || err}`); mirror = null; }}async function ensure() { await refreshMirror(); }function get() { return mirror ? mirror.pem : null; }@@@ -50,7 +50,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })} catch (err) {await refreshMirror();const code = err && err.code;-- log.warn(`[OpenVibe.Shoes] user token rejected (${code || 'error'}):`, (err && err.message) || err);++ log.warn(`[Open.Shoes] user token rejected (${code || 'error'}):`, (err && err.message) || err);return { ok: false, reason: publicReason(err), code, expired: code === 'token.expired' };}}@@@ -63,7 +63,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })} catch (err) {await refreshMirror();const code = err && err.code;-- log.warn(`[OpenVibe.Shoes] app token rejected (${code || 'error'}):`, (err && err.message) || err);++ log.warn(`[Open.Shoes] app token rejected (${code || 'error'}):`, (err && err.message) || err);return { ok: false, reason: publicReason(err), code };}}@@@ -62,7 +62,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat// The one module. The site's own values, its own old cookies, and the audience it always verified with.const sso = createSsoClient({site: 'shoes',-- siteName: 'OpenVibe.Shoes',++ siteName: 'Open.Shoes',baseUrl: config.baseUrl,clientId: config.oauth.clientId,clientSecret: config.oauth.clientSecret,@@@ -144,7 +144,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Datreq.viewer = viewerFrom(fresh.claims, fresh.token);}} catch (err) {-- log.warn('[OpenVibe.Shoes] session refresh failed:', (err && err.message) || err);++ log.warn('[Open.Shoes] session refresh failed:', (err && err.message) || err);}return next();});@@@ -197,7 +197,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Datconst q = req.query.next ? `?next=${encodeURIComponent(nextOf(req.query.next))}` : '';res.type('html').send('<!doctype html><meta charset="utf-8"><title>Sign out</title><meta name="viewport" content="width=device-width,initial-scale=1">'+ `<form method="post" action="/auth/logout${q}" style="font:16px system-ui;margin:3em auto;max-width:24em;text-align:center">`-- + '<p>Sign out of OpenVibe.Shoes?</p><button type="submit">Sign out</button> <a href="/">Cancel</a></form>');++ + '<p>Sign out of Open.Shoes?</p><button type="submit">Sign out</button> <a href="/">Cancel</a></form>');});r.post('/logout', (req, res, next) => (sameOriginRequest(req) ? next() : res.status(403).type('text/plain').send('Sign-out must come from this site.')));r.all('/logout', async (req, res) => {@@@ -1,7 +1,7 @@'use strict';/**-- * OpenVibe.Shoes configuration (open.shoes). Every value comes from the environment (production:++ * Open.Shoes configuration (open.shoes). Every value comes from the environment (production:* /etc/openvibe/shoes.env, see .env.example). Only environment variable NAMES appear in code and docs; secrets are* never logged.*@@@ -1,7 +1,7 @@'use strict';/**-- * OpenVibe.Shoes's own PostgreSQL database (ADR-035): the schema is migrations/NNNN_*.sql, applied at boot.++ * Open.Shoes's own PostgreSQL database (ADR-035): the schema is migrations/NNNN_*.sql, applied at boot.* Nothing written here is ever a secret. An offer stores the store's own URL, the price and the time it was* checked; a watch stores a person's subject id, a threshold and the public URL they pasted.*/@@@ -17,7 +17,7 @@ async function openDb(config, { log = console, registry } = {}) {if (!config.db.url) {if (config.isProduction) throw new Error('DATABASE_URL is not set: production serves from PostgreSQL (OpenVibe.Host roles/data add-service.sh today)');const dir = config.db.pgliteDir || DEV_PGLITE;-- log.warn(`[OpenVibe.Shoes] DATABASE_URL unset: embedded PGlite database in ${dir} (development only, one process)`);++ log.warn(`[Open.Shoes] DATABASE_URL unset: embedded PGlite database in ${dir} (development only, one process)`);fs.mkdirSync(dir, { recursive: true });const db = createDb({ pglite: dir, service: 'shoes', registry, log });await db.migrate({ dir: MIGRATIONS, log });@@@ -1,7 +1,7 @@'use strict';/**-- * /api/v1 — OpenVibe.Shoes' JSON API.++ * /api/v1 — Open.Shoes' JSON API.** GET /shoes?q&brand&size&condition&store&sort&page&limit public search, with facets* GET /shoes/:slug offers, own history, size curve, cited facts@@@ -11,9 +11,9 @@ const seo = require('openvibe-shared/seo');const cache = require('openvibe-shared/cache-policy');const { asyncRouter } = require('./router');--const SITE_NAME = 'OpenVibe.Shoes';++const SITE_NAME = 'Open.Shoes';const HOST = 'open.shoes';--const DESCRIPTION = 'OpenVibe.Shoes is every shoe, every market, the right price: one page per model with every store\'s all-in price, a price history from Shoes\' own daily snapshots, the size-price curve and a watch for your size.';++const DESCRIPTION = 'Open.Shoes is every shoe, every market, the right price: one page per model with every store\'s all-in price, a price history from Shoes\' own daily snapshots, the size-price curve and a watch for your size.';const DISALLOW = ['/auth/', '/api/', '/internal', '/metrics', '/watches', '/track'];function siteUpdated() {@@@ -54,7 +54,7 @@ function productJsonLd(config, model, offers, low) {function publicPages(models = []) {const pages = [-- { path: '/', changefreq: 'hourly', priority: 1.0, title: 'OpenVibe.Shoes home', text: 'Every shoe, every market, the right price: all-in prices, the price history and the size-price curve.' },++ { path: '/', changefreq: 'hourly', priority: 1.0, title: 'Open.Shoes home', text: 'Every shoe, every market, the right price: all-in prices, the price history and the size-price curve.' },{ path: '/shoes', changefreq: 'hourly', priority: 0.9, title: 'Every shoe', text: 'Search every model by brand, size and condition; every offer is all-in with its store and the time it was checked.' },{ path: '/drops', changefreq: 'daily', priority: 0.8, title: 'The drops calendar', text: 'Official shoe release dates, each cited to the brand\'s or store\'s own release page, with a reminder on OpenVibe.Today.' },{ path: '/developers', changefreq: 'weekly', priority: 0.6, title: 'The Shoes API', text: 'The open JSON API for shoes, offers, history, curves, drops and watches, and the OpenAPI document.' },@@@ -25,7 +25,7 @@ function openapi(config = {}) {return {openapi: '3.1.0',info: {-- title: 'OpenVibe.Shoes API',++ title: 'Open.Shoes API',version: '0.1.0',summary: 'Every shoe, every market, the right price.',description: 'Shoes aggregates official store offers into one all-in price per variant, keeps its own daily price snapshots, and exposes the drops calendar and a person\'s watches. Public reads take no key. There are no affiliate links and no Marketplace scraping.',@@@ -59,7 +59,7 @@ function createPageRoutes(ctx) {const body = views.home({ facets, dropping, dropsToday, popular, curveModel, curve, sceneDrop, jobs, now });return page(req, res, {title: null,-- description: 'OpenVibe.Shoes: every shoe, every market, the right price. A page per model with every store\'s all-in price, Shoes\' own price history, the size-price curve and a watch for your size.',++ description: 'Open.Shoes: every shoe, every market, the right price. A page per model with every store\'s all-in price, Shoes\' own price history, the size-price curve and a watch for your size.',body,active: '/',index: true,@@@ -83,7 +83,7 @@ function createPageRoutes(ctx) {]);return page(req, res, {title: q ? `Shoes matching “${q}”` : 'Every shoe',-- description: 'Search every shoe in OpenVibe.Shoes by brand, model, size and condition; every price is all-in with its store and the time it was checked.',++ description: 'Search every shoe in Open.Shoes by brand, model, size and condition; every price is all-in with its store and the time it was checked.',body: views.shoesIndex({ models, facets, query: { ...filters, sort }, page: pageNo, total }),active: '/shoes',robots: q || brand || size || condition || store ? 'noindex, follow' : 'index, follow',@@@ -84,7 +84,7 @@ function createPrincipal({ config, keys }) {}function require_(cap) {-- if (!CAPABILITIES.includes(cap)) throw new Error(`requireCapability(${cap}): not one of OpenVibe.Shoes's capabilities`);++ if (!CAPABILITIES.includes(cap)) throw new Error(`requireCapability(${cap}): not one of Open.Shoes's capabilities`);return function guard(req, res, next) {if (req.principal.kind === 'anonymous') return http.sendProblem(res, 401, 'token.required', { detail: 'Sign in at open.shoes, or send a person\'s Network token as a Bearer.', ctx: req.ov });if (req.principal.kind === 'user') {@@@ -1,7 +1,7 @@'use strict';/**-- * OpenVibe.Shoes — process entry. `node server/index.js`++ * Open.Shoes — process entry. `node server/index.js`* Listens on PORT (5180) behind nginx (deploy/).*/const { createApp } = require('./app');@@@ -15,7 +15,7 @@ const { startSubscriptions } = require('openvibe-sdk/account-data');*/function createLifecycle({ server, ctx, exit, signals, timers = [], extra = [] }) {return gracefulStop({-- name: 'OpenVibe.Shoes', server, deadlineExitCode: 0, exit, signals, deadlineMs: 10_000,++ name: 'Open.Shoes', server, deadlineExitCode: 0, exit, signals, deadlineMs: 10_000,close: [() => { for (const t of timers) clearInterval(t); },() => ctx.keys.client.stop(),@@@ -33,7 +33,7 @@ async function start() {const { config } = ctx;const server = app.listen(config.port, config.host, () => {-- console.log(`[OpenVibe.Shoes] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.s.db.store})`);++ console.log(`[Open.Shoes] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.s.db.store})`);// Warm the Network signing key immediately: a service nobody has signed in to yet must not read degraded// only because the first JWKS fetch had not happened.ctx.keys.ensure().catch(() => {});@@@ -66,7 +66,7 @@ async function start() {}if (require.main === module) {-- start().catch((err) => { console.error('[OpenVibe.Shoes] failed to start:', err); process.exit(1); });++ start().catch((err) => { console.error('[Open.Shoes] failed to start:', err); process.exit(1); });}module.exports = { start, createLifecycle };@@@ -26,7 +26,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {try {releases = await upstreams.releaseCalendar(source);} catch (err) {-- log.warn(`[OpenVibe.Shoes] drops source ${source.url} failed: ${err.message}`);++ log.warn(`[Open.Shoes] drops source ${source.url} failed: ${err.message}`);continue;}counts.sources++;@@@ -38,7 +38,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {}}await catalogue.finishJobRun(runId, { ok: true, counts });-- log.log(`[OpenVibe.Shoes] drops: ${counts.drops} releases from ${counts.sources} source(s)`);++ log.log(`[Open.Shoes] drops: ${counts.drops} releases from ${counts.sources} source(s)`);return counts;} catch (err) {await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });@@@ -51,7 +51,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {const tick = () => {if (running) return;running = true;-- runOnce().catch((err) => log.warn('[OpenVibe.Shoes] drops job failed:', err.message)).finally(() => { running = false; });++ runOnce().catch((err) => log.warn('[Open.Shoes] drops job failed:', err.message)).finally(() => { running = false; });};timer = setInterval(tick, config.jobs.dropsIntervalMs);timer.unref();@@@ -38,7 +38,7 @@ function createEbayJob({ config, catalogue, upstreams, log = console }) {else counts.skipped++;}await catalogue.finishJobRun(runId, { ok: true, counts });-- log.log(`[OpenVibe.Shoes] ebay: ${counts.offers} offers from ${counts.items} items`);++ log.log(`[Open.Shoes] ebay: ${counts.offers} offers from ${counts.items} items`);return { configured: true, ...counts };} catch (err) {await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });@@@ -51,7 +51,7 @@ function createEbayJob({ config, catalogue, upstreams, log = console }) {const tick = () => {if (running) return;running = true;-- runOnce().catch((err) => log.warn('[OpenVibe.Shoes] ebay job failed:', err.message)).finally(() => { running = false; });++ runOnce().catch((err) => log.warn('[Open.Shoes] ebay job failed:', err.message)).finally(() => { running = false; });};timer = setInterval(tick, config.jobs.ebayIntervalMs);timer.unref();@@@ -82,7 +82,7 @@ function createOffersJob({ config, catalogue, upstreams, log = console }) {if (page < total && config.sources.requestGapMs) await upstreams.sleep(config.sources.requestGapMs);}await catalogue.finishJobRun(runId, { ok: true, counts });-- log.log(`[OpenVibe.Shoes] offers: ${counts.offers} offers from ${counts.candidates} shoe candidates over ${counts.pages} pages`);++ log.log(`[Open.Shoes] offers: ${counts.offers} offers from ${counts.candidates} shoe candidates over ${counts.pages} pages`);return counts;} catch (err) {await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });@@@ -95,7 +95,7 @@ function createOffersJob({ config, catalogue, upstreams, log = console }) {const tick = () => {if (running) return;running = true;-- runOnce().catch((err) => log.warn('[OpenVibe.Shoes] offers job failed:', err.message)).finally(() => { running = false; });++ runOnce().catch((err) => log.warn('[Open.Shoes] offers job failed:', err.message)).finally(() => { running = false; });};timer = setInterval(tick, config.jobs.offersIntervalMs);timer.unref();@@@ -16,14 +16,14 @@ const cache = require('openvibe-shared/cache-policy');const { esc } = require('./html');const NETWORK_URL = 'https://openvibe.network';--const SITE_NAME = 'OpenVibe.Shoes';++const SITE_NAME = 'Open.Shoes';const MENU_NAME = 'Shoes';const TAGLINE = 'Every shoe, every market, the right price.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');const NAV = [-- { label: 'Home', href: '/', icon: 'fa-shoe-prints' },++ { label: 'Home', href: '/', icon: 'fa-house' },{ label: 'Shoes', href: '/shoes', icon: 'fa-magnifying-glass' },-- { label: 'Drops', href: '/drops', icon: 'fa-calendar-days' },++ { label: 'Drops', href: '/drops', icon: 'fa-clock' },{ label: 'Watches', href: '/watches', icon: 'fa-bell' },{ label: 'Developers', href: '/developers', icon: 'fa-code', more: true },];@@@ -59,7 +59,7 @@ function layout(o) {links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && pathNow.startsWith(`${l.href}/`)) })),appBar: [{ label: 'Shoes', href: '/shoes', icon: 'fa-magnifying-glass' },-- { label: 'Drops', href: '/drops', icon: 'fa-calendar-days' },++ { label: 'Drops', href: '/drops', icon: 'fa-clock' },{ label: 'Watches', href: '/watches', icon: 'fa-bell' },],silentLogin: `${String(config.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,@@@ -36,7 +36,7 @@ function createUpstreams({ config, fetchImpl = globalThis.fetch, log = console,try {const res = await fetchImpl(url, {method,-- headers: { 'user-agent': 'OpenVibe.Shoes/0.1 (+https://open.shoes)', accept: 'application/json, text/html;q=0.9', ...headers },++ headers: { 'user-agent': 'Open.Shoes/0.1 (+https://open.shoes)', accept: 'application/json, text/html;q=0.9', ...headers },body,signal: ctrl.signal,});@@@ -79,7 +79,7 @@ function createUpstreams({ config, fetchImpl = globalThis.fetch, log = console,const body = await request(`${dealsBase()}/api/v1/products/${encodeURIComponent(slug)}`, { json: true });return body && body.product ? body.product : null;} catch (err) {-- log.warn('[OpenVibe.Shoes] deals product failed:', err.message);++ log.warn('[Open.Shoes] deals product failed:', err.message);return null;}}@@@ -21,7 +21,7 @@ const rooms = require('openvibe-shared/rooms');const { esc, icon, card, rail } = kit;const ROOM = rooms.get('shoes');--const SITE = 'OpenVibe.Shoes';++const SITE = 'Open.Shoes';const money = (n, cur = 'USD') => {if (n == null || n === '') return '—';const v = Number(n);@@@ -146,7 +146,7 @@ const { assert, equal } = require('./helpers/check');const res = await t.get('/api/health');equal(res.status, 200);equal(res.json().service, 'openvibe-shoes');-- equal(res.json().name, 'OpenVibe.Shoes');++ equal(res.json().name, 'Open.Shoes');});await check('GET /api/v1/status: the jobs, counts and what is configured', async () => {@@@ -5,7 +5,7 @@ process.env.SHOES_API_RATE_LIMIT_PER_MIN = process.env.SHOES_API_RATE_LIMIT_PER_process.env.SHOES_LIMITS_MINUTE = process.env.SHOES_LIMITS_MINUTE || '100000';process.env.SHOES_LIMITS_HOUR = process.env.SHOES_LIMITS_HOUR || '100000';/**-- * Boots OpenVibe.Shoes on a temp database with a mock Network (test/helpers/mocks.js) and in-process public++ * Boots Open.Shoes on a temp database with a mock Network (test/helpers/mocks.js) and in-process public* sources and sibling services (test/helpers/upstreams.js), captures log lines, and returns a small HTTP client.* Every test file gets its own.*nnew file mode 100644@@@ -0,0 +1,22 @@++'use strict';++// Every icon in the bar's links is one the shared navbar draws (11-02: Home asked for fa-shoe-prints, which the++// OpenVibe glyph set does not have and Font Awesome is not loaded, so Home showed no icon at all), and the site is++// named for its domain, Open.Shoes.++// node test/nav-icons.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const layout = require('../server/render/layout');++++const navbar = fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'openvibe-shared', 'navbar.js'), 'utf8');++// The icons the navbar ships (NAV_ICONS) and the ones it loads beside itself (NAV_ICONS_LAZY).++const known = new Set([...navbar.matchAll(/'(fa-[a-z0-9-]+)'\s*:\s*\[/g)].map((m) => m[1]));++for (const m of navbar.matchAll(/NAV_ICONS_LAZY = \{([\s\S]*?)\};/g)) for (const n of m[1].match(/fa-[a-z0-9-]+/g) || []) known.add(n);++assert.ok(known.size > 20, 'the navbar\'s icon list was read');++const icons = [].concat(...[layout.NAV].flat(3).map((l) => (l && l.icon ? [l.icon] : [])));++assert.ok(icons.length >= 3, 'the bar has its links');++const missing = icons.filter((i) => !known.has(i));++assert.deepStrictEqual(missing, [], `icons the navbar cannot draw: ${missing.join(', ')}`);++assert.strictEqual(layout.NAV.find((l) => l.label === 'Home').icon, 'fa-house');++assert.strictEqual(layout.SITE_NAME, 'Open.Shoes', 'named for its domain');++console.log(`nav icons: ${icons.length} links, all drawn`);@@@ -35,7 +35,7 @@ const { assert, equal } = require('./helpers/check');assert(h.includes('Questions') || h.includes('sc-faq'), 'the FAQ');assert(h.includes('Part of OpenVibe'), 'the network section');assert(!h.includes('undefined'), 'no undefined in the page');-- assert(h.includes('OpenVibe.Shoes') && h.includes('open.shoes'), 'the name and the domain');++ assert(h.includes('Open.Shoes') && h.includes('open.shoes'), 'the name and the domain');});await check('GET /shoes: search, facets and a real card', async () => {@@@ -140,7 +140,7 @@ const { assert, equal } = require('./helpers/check');assert(sitemap.text.includes('/shoe/nike-air-max-90'), 'the model page');const llms = await t.get('/llms.txt');equal(llms.status, 200);-- assert(llms.text.includes('OpenVibe.Shoes'), 'the llms index');++ assert(llms.text.includes('Open.Shoes'), 'the llms index');const full = await t.get('/llms-full.txt');equal(full.status, 200);assert(full.text.includes('Nike Air Max 90'), 'the full pages');
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
.skip { position: absolute; left: -9999px; }
.skip { position: absolute; left: -9999px; }
.skip:focus { left: 12px; top: 12px; z-index: 99; padding: 8px 12px; border-radius: 10px; background: var(--bg-card, #0f1729); color: inherit; }
.skip:focus { left: 12px; top: 12px; z-index: 99; padding: 8px 12px; border-radius: 10px; background: var(--bg-card, #0f1729); color: inherit; }
-/* OpenVibe.Shoes — the small set of page pieces the kit does not draw (tables, forms, the price chart).
+/* Open.Shoes — the small set of page pieces the kit does not draw (tables, forms, the price chart).
The room's look and every card, rail and scene come from kit.css, room.css and showcase.css. */
The room's look and every card, rail and scene come from kit.css, room.css and showcase.css. */
.sh-wrap { width: min(1180px, 100% - 32px); margin: 0 auto; }
.sh-wrap { width: min(1180px, 100% - 32px); margin: 0 auto; }
.sh-page { padding: 28px 0 64px; }
.sh-page { padding: 28px 0 64px; }
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * OpenVibe.Shoes — Express app factory; server/index.js listens, tests build their own instance with a temp
+ * Open.Shoes — Express app factory; server/index.js listens, tests build their own instance with a temp
* database and a mock Network.
* database and a mock Network.
*
*
* /, /shoes, /shoe/:slug, /drops, /track, /watches, /developers the pages (http/pages.js)
* /, /shoes, /shoe/:slug, /drops, /track, /watches, /developers the pages (http/pages.js)
@@ -112,7 +112,7 @@ async function createApp(opts = {}) {
@@ -112,7 +112,7 @@ async function createApp(opts = {}) {
app.use(cookieParser());
app.use(cookieParser());
// ── Machine endpoints ───────────────────────────────────
// ── Machine endpoints ───────────────────────────────────
- app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-shoes', name: 'OpenVibe.Shoes', version: VERSION }));
+ app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-shoes', name: 'Open.Shoes', version: VERSION }));
release.mount(app, { registry: metrics.registry });
release.mount(app, { registry: metrics.registry });
const readiness = createServiceReadiness({ s, config, release: release.release, valkey });
const readiness = createServiceReadiness({ s, config, release: release.release, valkey });
app.get('/api/ready', readiness.handler);
app.get('/api/ready', readiness.handler);
@@ -123,7 +123,7 @@ async function createApp(opts = {}) {
@@ -123,7 +123,7 @@ async function createApp(opts = {}) {
// ── Sign-in (OAuth2 + PKCE client of OpenVibe.Network) ──
// ── Sign-in (OAuth2 + PKCE client of OpenVibe.Network) ──
app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, standardHeaders: true, legacyHeaders: false }));
app.use('/auth/', rateLimit({ windowMs: 15 * 60_000, limit: 60, standardHeaders: true, legacyHeaders: false }));
app.use('/auth', sso.routes());
app.use('/auth', sso.routes());
- { const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'shoes', service: 'shoes', host: 'open.shoes', name: 'OpenVibe.Shoes', profile: 'ugc' })); }
+ { const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'shoes', service: 'shoes', host: 'open.shoes', name: 'Open.Shoes', profile: 'ugc' })); }
// ── OpenVibe.Events → this service (loopback only) ──────
// ── OpenVibe.Events → this service (loopback only) ──────
// Account export/deletion; the consumer reads the raw body itself, so it is mounted before any body parser.
// Account export/deletion; the consumer reads the raw body itself, so it is mounted before any body parser.
@@ -159,7 +159,7 @@ async function createApp(opts = {}) {
@@ -159,7 +159,7 @@ async function createApp(opts = {}) {
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, _next) => {
app.use((err, req, res, _next) => {
- log.error('[OpenVibe.Shoes]', err && err.message ? err.message.slice(0, 300) : err);
+ log.error('[Open.Shoes]', err && err.message ? err.message.slice(0, 300) : err);
if (res.headersSent) return;
if (res.headersSent) return;
res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.set('Cache-Control', cache.htmlHeaders({ private: true }));
if (err && err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'request.malformed_json', { ctx: req.ov });
if (err && err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'request.malformed_json', { ctx: req.ov });
@@ -34,7 +34,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })
@@ -34,7 +34,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })
const ks = await client.keys();
const ks = await client.keys();
const first = ks && ks[0];
const first = ks && ks[0];
mirror = first ? { pem: first.key.export({ type: 'spki', format: 'pem' }), count: ks.length } : null;
mirror = first ? { pem: first.key.export({ type: 'spki', format: 'pem' }), count: ks.length } : null;
- } catch (err) { log.warn(`[OpenVibe.Shoes] JWKS mirror refresh failed: ${err && err.message || err}`); mirror = null; }
+ } catch (err) { log.warn(`[Open.Shoes] JWKS mirror refresh failed: ${err && err.message || err}`); mirror = null; }
}
}
async function ensure() { await refreshMirror(); }
async function ensure() { await refreshMirror(); }
function get() { return mirror ? mirror.pem : null; }
function get() { return mirror ? mirror.pem : null; }
@@ -50,7 +50,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })
@@ -50,7 +50,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })
} catch (err) {
} catch (err) {
await refreshMirror();
await refreshMirror();
const code = err && err.code;
const code = err && err.code;
- log.warn(`[OpenVibe.Shoes] user token rejected (${code || 'error'}):`, (err && err.message) || err);
+ log.warn(`[Open.Shoes] user token rejected (${code || 'error'}):`, (err && err.message) || err);
return { ok: false, reason: publicReason(err), code, expired: code === 'token.expired' };
return { ok: false, reason: publicReason(err), code, expired: code === 'token.expired' };
}
}
}
}
@@ -63,7 +63,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })
@@ -63,7 +63,7 @@ function createKeyStore({ config, fetchImpl = globalThis.fetch, log = console })
} catch (err) {
} catch (err) {
await refreshMirror();
await refreshMirror();
const code = err && err.code;
const code = err && err.code;
- log.warn(`[OpenVibe.Shoes] app token rejected (${code || 'error'}):`, (err && err.message) || err);
+ log.warn(`[Open.Shoes] app token rejected (${code || 'error'}):`, (err && err.message) || err);
return { ok: false, reason: publicReason(err), code };
return { ok: false, reason: publicReason(err), code };
}
}
}
}
@@ -62,7 +62,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -62,7 +62,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
// The one module. The site's own values, its own old cookies, and the audience it always verified with.
// The one module. The site's own values, its own old cookies, and the audience it always verified with.
const sso = createSsoClient({
const sso = createSsoClient({
site: 'shoes',
site: 'shoes',
- siteName: 'OpenVibe.Shoes',
+ siteName: 'Open.Shoes',
baseUrl: config.baseUrl,
baseUrl: config.baseUrl,
clientId: config.oauth.clientId,
clientId: config.oauth.clientId,
clientSecret: config.oauth.clientSecret,
clientSecret: config.oauth.clientSecret,
@@ -144,7 +144,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -144,7 +144,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
req.viewer = viewerFrom(fresh.claims, fresh.token);
req.viewer = viewerFrom(fresh.claims, fresh.token);
}
}
} catch (err) {
} catch (err) {
- log.warn('[OpenVibe.Shoes] session refresh failed:', (err && err.message) || err);
+ log.warn('[Open.Shoes] session refresh failed:', (err && err.message) || err);
}
}
return next();
return next();
});
});
@@ -197,7 +197,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -197,7 +197,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
const q = req.query.next ? `?next=${encodeURIComponent(nextOf(req.query.next))}` : '';
const q = req.query.next ? `?next=${encodeURIComponent(nextOf(req.query.next))}` : '';
res.type('html').send('<!doctype html><meta charset="utf-8"><title>Sign out</title><meta name="viewport" content="width=device-width,initial-scale=1">'
res.type('html').send('<!doctype html><meta charset="utf-8"><title>Sign out</title><meta name="viewport" content="width=device-width,initial-scale=1">'
+ `<form method="post" action="/auth/logout${q}" style="font:16px system-ui;margin:3em auto;max-width:24em;text-align:center">`
+ `<form method="post" action="/auth/logout${q}" style="font:16px system-ui;margin:3em auto;max-width:24em;text-align:center">`
- + '<p>Sign out of OpenVibe.Shoes?</p><button type="submit">Sign out</button> <a href="/">Cancel</a></form>');
+ + '<p>Sign out of Open.Shoes?</p><button type="submit">Sign out</button> <a href="/">Cancel</a></form>');
});
});
r.post('/logout', (req, res, next) => (sameOriginRequest(req) ? next() : res.status(403).type('text/plain').send('Sign-out must come from this site.')));
r.post('/logout', (req, res, next) => (sameOriginRequest(req) ? next() : res.status(403).type('text/plain').send('Sign-out must come from this site.')));
r.all('/logout', async (req, res) => {
r.all('/logout', async (req, res) => {
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * OpenVibe.Shoes configuration (open.shoes). Every value comes from the environment (production:
+ * Open.Shoes configuration (open.shoes). Every value comes from the environment (production:
* /etc/openvibe/shoes.env, see .env.example). Only environment variable NAMES appear in code and docs; secrets are
* /etc/openvibe/shoes.env, see .env.example). Only environment variable NAMES appear in code and docs; secrets are
* never logged.
* never logged.
*
*
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * OpenVibe.Shoes's own PostgreSQL database (ADR-035): the schema is migrations/NNNN_*.sql, applied at boot.
+ * Open.Shoes's own PostgreSQL database (ADR-035): the schema is migrations/NNNN_*.sql, applied at boot.
* Nothing written here is ever a secret. An offer stores the store's own URL, the price and the time it was
* Nothing written here is ever a secret. An offer stores the store's own URL, the price and the time it was
* checked; a watch stores a person's subject id, a threshold and the public URL they pasted.
* checked; a watch stores a person's subject id, a threshold and the public URL they pasted.
*/
*/
@@ -17,7 +17,7 @@ async function openDb(config, { log = console, registry } = {}) {
@@ -17,7 +17,7 @@ async function openDb(config, { log = console, registry } = {}) {
if (!config.db.url) {
if (!config.db.url) {
if (config.isProduction) throw new Error('DATABASE_URL is not set: production serves from PostgreSQL (OpenVibe.Host roles/data add-service.sh today)');
if (config.isProduction) throw new Error('DATABASE_URL is not set: production serves from PostgreSQL (OpenVibe.Host roles/data add-service.sh today)');
const dir = config.db.pgliteDir || DEV_PGLITE;
const dir = config.db.pgliteDir || DEV_PGLITE;
- log.warn(`[OpenVibe.Shoes] DATABASE_URL unset: embedded PGlite database in ${dir} (development only, one process)`);
+ log.warn(`[Open.Shoes] DATABASE_URL unset: embedded PGlite database in ${dir} (development only, one process)`);
fs.mkdirSync(dir, { recursive: true });
fs.mkdirSync(dir, { recursive: true });
const db = createDb({ pglite: dir, service: 'shoes', registry, log });
const db = createDb({ pglite: dir, service: 'shoes', registry, log });
await db.migrate({ dir: MIGRATIONS, log });
await db.migrate({ dir: MIGRATIONS, log });
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * /api/v1 — OpenVibe.Shoes' JSON API.
+ * /api/v1 — Open.Shoes' JSON API.
*
*
* GET /shoes?q&brand&size&condition&store&sort&page&limit public search, with facets
* GET /shoes?q&brand&size&condition&store&sort&page&limit public search, with facets
* GET /shoes/:slug offers, own history, size curve, cited facts
* GET /shoes/:slug offers, own history, size curve, cited facts
@@ -11,9 +11,9 @@ const seo = require('openvibe-shared/seo');
@@ -11,9 +11,9 @@ const seo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
-const SITE_NAME = 'OpenVibe.Shoes';
+const SITE_NAME = 'Open.Shoes';
const HOST = 'open.shoes';
const HOST = 'open.shoes';
-const DESCRIPTION = 'OpenVibe.Shoes is every shoe, every market, the right price: one page per model with every store\'s all-in price, a price history from Shoes\' own daily snapshots, the size-price curve and a watch for your size.';
+const DESCRIPTION = 'Open.Shoes is every shoe, every market, the right price: one page per model with every store\'s all-in price, a price history from Shoes\' own daily snapshots, the size-price curve and a watch for your size.';
const DISALLOW = ['/auth/', '/api/', '/internal', '/metrics', '/watches', '/track'];
const DISALLOW = ['/auth/', '/api/', '/internal', '/metrics', '/watches', '/track'];
function siteUpdated() {
function siteUpdated() {
@@ -54,7 +54,7 @@ function productJsonLd(config, model, offers, low) {
@@ -54,7 +54,7 @@ function productJsonLd(config, model, offers, low) {
function publicPages(models = []) {
function publicPages(models = []) {
const pages = [
const pages = [
- { path: '/', changefreq: 'hourly', priority: 1.0, title: 'OpenVibe.Shoes home', text: 'Every shoe, every market, the right price: all-in prices, the price history and the size-price curve.' },
+ { path: '/', changefreq: 'hourly', priority: 1.0, title: 'Open.Shoes home', text: 'Every shoe, every market, the right price: all-in prices, the price history and the size-price curve.' },
{ path: '/shoes', changefreq: 'hourly', priority: 0.9, title: 'Every shoe', text: 'Search every model by brand, size and condition; every offer is all-in with its store and the time it was checked.' },
{ path: '/shoes', changefreq: 'hourly', priority: 0.9, title: 'Every shoe', text: 'Search every model by brand, size and condition; every offer is all-in with its store and the time it was checked.' },
{ path: '/drops', changefreq: 'daily', priority: 0.8, title: 'The drops calendar', text: 'Official shoe release dates, each cited to the brand\'s or store\'s own release page, with a reminder on OpenVibe.Today.' },
{ path: '/drops', changefreq: 'daily', priority: 0.8, title: 'The drops calendar', text: 'Official shoe release dates, each cited to the brand\'s or store\'s own release page, with a reminder on OpenVibe.Today.' },
{ path: '/developers', changefreq: 'weekly', priority: 0.6, title: 'The Shoes API', text: 'The open JSON API for shoes, offers, history, curves, drops and watches, and the OpenAPI document.' },
{ path: '/developers', changefreq: 'weekly', priority: 0.6, title: 'The Shoes API', text: 'The open JSON API for shoes, offers, history, curves, drops and watches, and the OpenAPI document.' },
@@ -25,7 +25,7 @@ function openapi(config = {}) {
@@ -25,7 +25,7 @@ function openapi(config = {}) {
return {
return {
openapi: '3.1.0',
openapi: '3.1.0',
info: {
info: {
- title: 'OpenVibe.Shoes API',
+ title: 'Open.Shoes API',
version: '0.1.0',
version: '0.1.0',
summary: 'Every shoe, every market, the right price.',
summary: 'Every shoe, every market, the right price.',
description: 'Shoes aggregates official store offers into one all-in price per variant, keeps its own daily price snapshots, and exposes the drops calendar and a person\'s watches. Public reads take no key. There are no affiliate links and no Marketplace scraping.',
description: 'Shoes aggregates official store offers into one all-in price per variant, keeps its own daily price snapshots, and exposes the drops calendar and a person\'s watches. Public reads take no key. There are no affiliate links and no Marketplace scraping.',
@@ -59,7 +59,7 @@ function createPageRoutes(ctx) {
@@ -59,7 +59,7 @@ function createPageRoutes(ctx) {
const body = views.home({ facets, dropping, dropsToday, popular, curveModel, curve, sceneDrop, jobs, now });
const body = views.home({ facets, dropping, dropsToday, popular, curveModel, curve, sceneDrop, jobs, now });
return page(req, res, {
return page(req, res, {
title: null,
title: null,
- description: 'OpenVibe.Shoes: every shoe, every market, the right price. A page per model with every store\'s all-in price, Shoes\' own price history, the size-price curve and a watch for your size.',
+ description: 'Open.Shoes: every shoe, every market, the right price. A page per model with every store\'s all-in price, Shoes\' own price history, the size-price curve and a watch for your size.',
body,
body,
active: '/',
active: '/',
index: true,
index: true,
@@ -83,7 +83,7 @@ function createPageRoutes(ctx) {
@@ -83,7 +83,7 @@ function createPageRoutes(ctx) {
]);
]);
return page(req, res, {
return page(req, res, {
title: q ? `Shoes matching “${q}”` : 'Every shoe',
title: q ? `Shoes matching “${q}”` : 'Every shoe',
- description: 'Search every shoe in OpenVibe.Shoes by brand, model, size and condition; every price is all-in with its store and the time it was checked.',
+ description: 'Search every shoe in Open.Shoes by brand, model, size and condition; every price is all-in with its store and the time it was checked.',
body: views.shoesIndex({ models, facets, query: { ...filters, sort }, page: pageNo, total }),
body: views.shoesIndex({ models, facets, query: { ...filters, sort }, page: pageNo, total }),
active: '/shoes',
active: '/shoes',
robots: q || brand || size || condition || store ? 'noindex, follow' : 'index, follow',
robots: q || brand || size || condition || store ? 'noindex, follow' : 'index, follow',
@@ -84,7 +84,7 @@ function createPrincipal({ config, keys }) {
@@ -84,7 +84,7 @@ function createPrincipal({ config, keys }) {
}
}
function require_(cap) {
function require_(cap) {
- if (!CAPABILITIES.includes(cap)) throw new Error(`requireCapability(${cap}): not one of OpenVibe.Shoes's capabilities`);
+ if (!CAPABILITIES.includes(cap)) throw new Error(`requireCapability(${cap}): not one of Open.Shoes's capabilities`);
return function guard(req, res, next) {
return function guard(req, res, next) {
if (req.principal.kind === 'anonymous') return http.sendProblem(res, 401, 'token.required', { detail: 'Sign in at open.shoes, or send a person\'s Network token as a Bearer.', ctx: req.ov });
if (req.principal.kind === 'anonymous') return http.sendProblem(res, 401, 'token.required', { detail: 'Sign in at open.shoes, or send a person\'s Network token as a Bearer.', ctx: req.ov });
if (req.principal.kind === 'user') {
if (req.principal.kind === 'user') {
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
- * OpenVibe.Shoes — process entry. `node server/index.js`
+ * Open.Shoes — process entry. `node server/index.js`
* Listens on PORT (5180) behind nginx (deploy/).
* Listens on PORT (5180) behind nginx (deploy/).
*/
*/
const { createApp } = require('./app');
const { createApp } = require('./app');
@@ -15,7 +15,7 @@ const { startSubscriptions } = require('openvibe-sdk/account-data');
@@ -15,7 +15,7 @@ const { startSubscriptions } = require('openvibe-sdk/account-data');
*/
*/
function createLifecycle({ server, ctx, exit, signals, timers = [], extra = [] }) {
function createLifecycle({ server, ctx, exit, signals, timers = [], extra = [] }) {
return gracefulStop({
return gracefulStop({
- name: 'OpenVibe.Shoes', server, deadlineExitCode: 0, exit, signals, deadlineMs: 10_000,
+ name: 'Open.Shoes', server, deadlineExitCode: 0, exit, signals, deadlineMs: 10_000,
close: [
close: [
() => { for (const t of timers) clearInterval(t); },
() => { for (const t of timers) clearInterval(t); },
() => ctx.keys.client.stop(),
() => ctx.keys.client.stop(),
@@ -33,7 +33,7 @@ async function start() {
@@ -33,7 +33,7 @@ async function start() {
const { config } = ctx;
const { config } = ctx;
const server = app.listen(config.port, config.host, () => {
const server = app.listen(config.port, config.host, () => {
- console.log(`[OpenVibe.Shoes] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.s.db.store})`);
+ console.log(`[Open.Shoes] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.s.db.store})`);
// Warm the Network signing key immediately: a service nobody has signed in to yet must not read degraded
// Warm the Network signing key immediately: a service nobody has signed in to yet must not read degraded
// only because the first JWKS fetch had not happened.
// only because the first JWKS fetch had not happened.
ctx.keys.ensure().catch(() => {});
ctx.keys.ensure().catch(() => {});
@@ -66,7 +66,7 @@ async function start() {
@@ -66,7 +66,7 @@ async function start() {
}
}
if (require.main === module) {
if (require.main === module) {
- start().catch((err) => { console.error('[OpenVibe.Shoes] failed to start:', err); process.exit(1); });
+ start().catch((err) => { console.error('[Open.Shoes] failed to start:', err); process.exit(1); });
}
}
module.exports = { start, createLifecycle };
module.exports = { start, createLifecycle };
@@ -26,7 +26,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {
@@ -26,7 +26,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {
try {
try {
releases = await upstreams.releaseCalendar(source);
releases = await upstreams.releaseCalendar(source);
} catch (err) {
} catch (err) {
- log.warn(`[OpenVibe.Shoes] drops source ${source.url} failed: ${err.message}`);
+ log.warn(`[Open.Shoes] drops source ${source.url} failed: ${err.message}`);
continue;
continue;
}
}
counts.sources++;
counts.sources++;
@@ -38,7 +38,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {
@@ -38,7 +38,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {
}
}
}
}
await catalogue.finishJobRun(runId, { ok: true, counts });
await catalogue.finishJobRun(runId, { ok: true, counts });
- log.log(`[OpenVibe.Shoes] drops: ${counts.drops} releases from ${counts.sources} source(s)`);
+ log.log(`[Open.Shoes] drops: ${counts.drops} releases from ${counts.sources} source(s)`);
return counts;
return counts;
} catch (err) {
} catch (err) {
await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });
await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });
@@ -51,7 +51,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {
@@ -51,7 +51,7 @@ function createDropsJob({ config, catalogue, upstreams, log = console }) {
const tick = () => {
const tick = () => {
if (running) return;
if (running) return;
running = true;
running = true;
- runOnce().catch((err) => log.warn('[OpenVibe.Shoes] drops job failed:', err.message)).finally(() => { running = false; });
+ runOnce().catch((err) => log.warn('[Open.Shoes] drops job failed:', err.message)).finally(() => { running = false; });
};
};
timer = setInterval(tick, config.jobs.dropsIntervalMs);
timer = setInterval(tick, config.jobs.dropsIntervalMs);
timer.unref();
timer.unref();
@@ -38,7 +38,7 @@ function createEbayJob({ config, catalogue, upstreams, log = console }) {
@@ -38,7 +38,7 @@ function createEbayJob({ config, catalogue, upstreams, log = console }) {
else counts.skipped++;
else counts.skipped++;
}
}
await catalogue.finishJobRun(runId, { ok: true, counts });
await catalogue.finishJobRun(runId, { ok: true, counts });
- log.log(`[OpenVibe.Shoes] ebay: ${counts.offers} offers from ${counts.items} items`);
+ log.log(`[Open.Shoes] ebay: ${counts.offers} offers from ${counts.items} items`);
return { configured: true, ...counts };
return { configured: true, ...counts };
} catch (err) {
} catch (err) {
await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });
await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });
@@ -51,7 +51,7 @@ function createEbayJob({ config, catalogue, upstreams, log = console }) {
@@ -51,7 +51,7 @@ function createEbayJob({ config, catalogue, upstreams, log = console }) {
const tick = () => {
const tick = () => {
if (running) return;
if (running) return;
running = true;
running = true;
- runOnce().catch((err) => log.warn('[OpenVibe.Shoes] ebay job failed:', err.message)).finally(() => { running = false; });
+ runOnce().catch((err) => log.warn('[Open.Shoes] ebay job failed:', err.message)).finally(() => { running = false; });
};
};
timer = setInterval(tick, config.jobs.ebayIntervalMs);
timer = setInterval(tick, config.jobs.ebayIntervalMs);
timer.unref();
timer.unref();
@@ -82,7 +82,7 @@ function createOffersJob({ config, catalogue, upstreams, log = console }) {
@@ -82,7 +82,7 @@ function createOffersJob({ config, catalogue, upstreams, log = console }) {
if (page < total && config.sources.requestGapMs) await upstreams.sleep(config.sources.requestGapMs);
if (page < total && config.sources.requestGapMs) await upstreams.sleep(config.sources.requestGapMs);
}
}
await catalogue.finishJobRun(runId, { ok: true, counts });
await catalogue.finishJobRun(runId, { ok: true, counts });
- log.log(`[OpenVibe.Shoes] offers: ${counts.offers} offers from ${counts.candidates} shoe candidates over ${counts.pages} pages`);
+ log.log(`[Open.Shoes] offers: ${counts.offers} offers from ${counts.candidates} shoe candidates over ${counts.pages} pages`);
return counts;
return counts;
} catch (err) {
} catch (err) {
await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });
await catalogue.finishJobRun(runId, { ok: false, counts, error: err.message });
@@ -95,7 +95,7 @@ function createOffersJob({ config, catalogue, upstreams, log = console }) {
@@ -95,7 +95,7 @@ function createOffersJob({ config, catalogue, upstreams, log = console }) {
const tick = () => {
const tick = () => {
if (running) return;
if (running) return;
running = true;
running = true;
- runOnce().catch((err) => log.warn('[OpenVibe.Shoes] offers job failed:', err.message)).finally(() => { running = false; });
+ runOnce().catch((err) => log.warn('[Open.Shoes] offers job failed:', err.message)).finally(() => { running = false; });
};
};
timer = setInterval(tick, config.jobs.offersIntervalMs);
timer = setInterval(tick, config.jobs.offersIntervalMs);
timer.unref();
timer.unref();
@@ -16,14 +16,14 @@ const cache = require('openvibe-shared/cache-policy');
@@ -16,14 +16,14 @@ const cache = require('openvibe-shared/cache-policy');
const { esc } = require('./html');
const { esc } = require('./html');
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
-const SITE_NAME = 'OpenVibe.Shoes';
+const SITE_NAME = 'Open.Shoes';
const MENU_NAME = 'Shoes';
const MENU_NAME = 'Shoes';
const TAGLINE = 'Every shoe, every market, the right price.';
const TAGLINE = 'Every shoe, every market, the right price.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const NAV = [
const NAV = [
- { label: 'Home', href: '/', icon: 'fa-shoe-prints' },
+ { label: 'Home', href: '/', icon: 'fa-house' },
{ label: 'Shoes', href: '/shoes', icon: 'fa-magnifying-glass' },
{ label: 'Shoes', href: '/shoes', icon: 'fa-magnifying-glass' },
- { label: 'Drops', href: '/drops', icon: 'fa-calendar-days' },
+ { label: 'Drops', href: '/drops', icon: 'fa-clock' },
{ label: 'Watches', href: '/watches', icon: 'fa-bell' },
{ label: 'Watches', href: '/watches', icon: 'fa-bell' },
{ label: 'Developers', href: '/developers', icon: 'fa-code', more: true },
{ label: 'Developers', href: '/developers', icon: 'fa-code', more: true },
];
];
@@ -59,7 +59,7 @@ function layout(o) {
@@ -59,7 +59,7 @@ function layout(o) {
links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && pathNow.startsWith(`${l.href}/`)) })),
links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && pathNow.startsWith(`${l.href}/`)) })),
appBar: [
appBar: [
{ label: 'Shoes', href: '/shoes', icon: 'fa-magnifying-glass' },
{ label: 'Shoes', href: '/shoes', icon: 'fa-magnifying-glass' },
- { label: 'Drops', href: '/drops', icon: 'fa-calendar-days' },
+ { label: 'Drops', href: '/drops', icon: 'fa-clock' },
{ label: 'Watches', href: '/watches', icon: 'fa-bell' },
{ label: 'Watches', href: '/watches', icon: 'fa-bell' },
],
],
silentLogin: `${String(config.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,
silentLogin: `${String(config.baseUrl).replace(/\/+$/, '')}/auth/login?silent=1&next={url}`,
@@ -36,7 +36,7 @@ function createUpstreams({ config, fetchImpl = globalThis.fetch, log = console,
@@ -36,7 +36,7 @@ function createUpstreams({ config, fetchImpl = globalThis.fetch, log = console,
try {
try {
const res = await fetchImpl(url, {
const res = await fetchImpl(url, {
method,
method,
- headers: { 'user-agent': 'OpenVibe.Shoes/0.1 (+https://open.shoes)', accept: 'application/json, text/html;q=0.9', ...headers },
+ headers: { 'user-agent': 'Open.Shoes/0.1 (+https://open.shoes)', accept: 'application/json, text/html;q=0.9', ...headers },
body,
body,
signal: ctrl.signal,
signal: ctrl.signal,
});
});
@@ -79,7 +79,7 @@ function createUpstreams({ config, fetchImpl = globalThis.fetch, log = console,
@@ -79,7 +79,7 @@ function createUpstreams({ config, fetchImpl = globalThis.fetch, log = console,
const body = await request(`${dealsBase()}/api/v1/products/${encodeURIComponent(slug)}`, { json: true });
const body = await request(`${dealsBase()}/api/v1/products/${encodeURIComponent(slug)}`, { json: true });
return body && body.product ? body.product : null;
return body && body.product ? body.product : null;
} catch (err) {
} catch (err) {
- log.warn('[OpenVibe.Shoes] deals product failed:', err.message);
+ log.warn('[Open.Shoes] deals product failed:', err.message);
return null;
return null;
}
}
}
}
@@ -21,7 +21,7 @@ const rooms = require('openvibe-shared/rooms');
@@ -21,7 +21,7 @@ const rooms = require('openvibe-shared/rooms');
const { esc, icon, card, rail } = kit;
const { esc, icon, card, rail } = kit;
const ROOM = rooms.get('shoes');
const ROOM = rooms.get('shoes');
-const SITE = 'OpenVibe.Shoes';
+const SITE = 'Open.Shoes';
const money = (n, cur = 'USD') => {
const money = (n, cur = 'USD') => {
if (n == null || n === '') return '—';
if (n == null || n === '') return '—';
const v = Number(n);
const v = Number(n);
@@ -146,7 +146,7 @@ const { assert, equal } = require('./helpers/check');
@@ -146,7 +146,7 @@ const { assert, equal } = require('./helpers/check');
const res = await t.get('/api/health');
const res = await t.get('/api/health');
equal(res.status, 200);
equal(res.status, 200);
equal(res.json().service, 'openvibe-shoes');
equal(res.json().service, 'openvibe-shoes');
- equal(res.json().name, 'OpenVibe.Shoes');
+ equal(res.json().name, 'Open.Shoes');
});
});
await check('GET /api/v1/status: the jobs, counts and what is configured', async () => {
await check('GET /api/v1/status: the jobs, counts and what is configured', async () => {
@@ -5,7 +5,7 @@ process.env.SHOES_API_RATE_LIMIT_PER_MIN = process.env.SHOES_API_RATE_LIMIT_PER_
@@ -5,7 +5,7 @@ process.env.SHOES_API_RATE_LIMIT_PER_MIN = process.env.SHOES_API_RATE_LIMIT_PER_
process.env.SHOES_LIMITS_MINUTE = process.env.SHOES_LIMITS_MINUTE || '100000';
process.env.SHOES_LIMITS_MINUTE = process.env.SHOES_LIMITS_MINUTE || '100000';
process.env.SHOES_LIMITS_HOUR = process.env.SHOES_LIMITS_HOUR || '100000';
process.env.SHOES_LIMITS_HOUR = process.env.SHOES_LIMITS_HOUR || '100000';
/**
/**
- * Boots OpenVibe.Shoes on a temp database with a mock Network (test/helpers/mocks.js) and in-process public
+ * Boots Open.Shoes on a temp database with a mock Network (test/helpers/mocks.js) and in-process public
* sources and sibling services (test/helpers/upstreams.js), captures log lines, and returns a small HTTP client.
* sources and sibling services (test/helpers/upstreams.js), captures log lines, and returns a small HTTP client.
* Every test file gets its own.
* Every test file gets its own.
*
*
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+'use strict';
+// Every icon in the bar's links is one the shared navbar draws (11-02: Home asked for fa-shoe-prints, which the
+// OpenVibe glyph set does not have and Font Awesome is not loaded, so Home showed no icon at all), and the site is
+// named for its domain, Open.Shoes.
+// node test/nav-icons.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const layout = require('../server/render/layout');
+
+const navbar = fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'openvibe-shared', 'navbar.js'), 'utf8');
+// The icons the navbar ships (NAV_ICONS) and the ones it loads beside itself (NAV_ICONS_LAZY).
+const known = new Set([...navbar.matchAll(/'(fa-[a-z0-9-]+)'\s*:\s*\[/g)].map((m) => m[1]));
+for (const m of navbar.matchAll(/NAV_ICONS_LAZY = \{([\s\S]*?)\};/g)) for (const n of m[1].match(/fa-[a-z0-9-]+/g) || []) known.add(n);
+assert.ok(known.size > 20, 'the navbar\'s icon list was read');
+const icons = [].concat(...[layout.NAV].flat(3).map((l) => (l && l.icon ? [l.icon] : [])));
+assert.ok(icons.length >= 3, 'the bar has its links');
+const missing = icons.filter((i) => !known.has(i));
+assert.deepStrictEqual(missing, [], `icons the navbar cannot draw: ${missing.join(', ')}`);
+assert.strictEqual(layout.NAV.find((l) => l.label === 'Home').icon, 'fa-house');
+assert.strictEqual(layout.SITE_NAME, 'Open.Shoes', 'named for its domain');
+console.log(`nav icons: ${icons.length} links, all drawn`);
@@ -35,7 +35,7 @@ const { assert, equal } = require('./helpers/check');
@@ -35,7 +35,7 @@ const { assert, equal } = require('./helpers/check');
assert(h.includes('Questions') || h.includes('sc-faq'), 'the FAQ');
assert(h.includes('Questions') || h.includes('sc-faq'), 'the FAQ');
assert(h.includes('Part of OpenVibe'), 'the network section');
assert(h.includes('Part of OpenVibe'), 'the network section');
assert(!h.includes('undefined'), 'no undefined in the page');
assert(!h.includes('undefined'), 'no undefined in the page');
- assert(h.includes('OpenVibe.Shoes') && h.includes('open.shoes'), 'the name and the domain');
+ assert(h.includes('Open.Shoes') && h.includes('open.shoes'), 'the name and the domain');
});
});
await check('GET /shoes: search, facets and a real card', async () => {
await check('GET /shoes: search, facets and a real card', async () => {
@@ -140,7 +140,7 @@ const { assert, equal } = require('./helpers/check');
@@ -140,7 +140,7 @@ const { assert, equal } = require('./helpers/check');
assert(sitemap.text.includes('/shoe/nike-air-max-90'), 'the model page');
assert(sitemap.text.includes('/shoe/nike-air-max-90'), 'the model page');
const llms = await t.get('/llms.txt');
const llms = await t.get('/llms.txt');
equal(llms.status, 200);
equal(llms.status, 200);
- assert(llms.text.includes('OpenVibe.Shoes'), 'the llms index');
+ assert(llms.text.includes('Open.Shoes'), 'the llms index');
const full = await t.get('/llms-full.txt');
const full = await t.get('/llms-full.txt');
equal(full.status, 200);
equal(full.status, 200);
assert(full.text.includes('Nike Air Max 90'), 'the full pages');
assert(full.text.includes('Nike Air Max 90'), 'the full pages');