openvibers/openvibe.watch Commits

openvibe.watch's hero is one watch's own history, not the generic aura (#187)

OP OpenVibers committed 11 days ago · a169c38dad · 3 files, +46 −0

A price and its rule (under $1,250), the checks every two hours with what each read, the check that crossed the line
($1,199, down $150) and the alert it sent. Every part is something a watch keeps. Theme tokens, the kit's line icons.

Changes

ddiff --git a/public/css/home.css b/public/css/home.css
iindex 2c97d09..5ab3b06 100644
---- a/public/css/home.css
++++ b/public/css/home.css
@@@ -101,3 +101,25 @@
:where(.sc-sec, .cm-sec, .hm-sec, .section) > .hm-fold-block { margin: 0; }
/* The fold holds the questions, so its list gets the room the card gives it. */
.hm-fold-block .hm-faq { margin: 0; padding: 0 16px 16px; }
++
++/* ── The home's picture: one watch's history (MOTION-1, 2026-11-02) ──────────────────────────────────────────────── */
++.wt-scene { position: relative; width: min(100%, 360px); margin: 0 auto; padding-bottom: 6px; }
++.wt-scene svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
++.wt-sc-card { border-radius: 16px; border: 1px solid var(--border, rgba(255, 255, 255, .1)); background: var(--bg-card, var(--bg-secondary, #141c2a)); padding: 14px 16px; display: grid; gap: 10px; box-shadow: 0 12px 30px rgba(0, 0, 0, .22); }
++.wt-sc-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
++.wt-sc-head b { font-size: 14.5px; color: var(--text-primary, #e8ecf3); }
++.wt-sc-head span { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--text-muted, #93a0b4); white-space: nowrap; }
++.wt-sc-rule { font-size: 13px; color: var(--text-secondary, #b6c0d2); padding: 8px 10px; border-radius: 10px; background: color-mix(in srgb, var(--accent, #3b82f6) 10%, transparent); }
++.wt-sc-rule b { color: var(--text-primary, #e8ecf3); }
++.wt-sc-checks { list-style: none; margin: 0; padding: 0 0 0 6px; display: grid; gap: 2px; border-left: 2px solid var(--border, rgba(255, 255, 255, .12)); }
++.wt-sc-check { position: relative; display: flex; align-items: baseline; gap: 12px; padding: 7px 0 7px 14px; font-size: 13.5px; color: var(--text-secondary, #b6c0d2); }
++.wt-sc-dot { position: absolute; left: -7px; top: 50%; width: 10px; height: 10px; margin-top: -5px; border-radius: 50%; background: var(--bg-secondary, #0f1522); border: 2px solid var(--border, rgba(255, 255, 255, .25)); }
++.wt-sc-check time { font-variant-numeric: tabular-nums; color: var(--text-muted, #93a0b4); width: 3.2em; }
++.wt-sc-check b { font-variant-numeric: tabular-nums; color: var(--text-primary, #e8ecf3); font-weight: 600; }
++.wt-sc-check em { margin-left: auto; font-style: normal; font-weight: 700; font-size: 12.5px; color: var(--success-text, var(--success, #22c55e)); }
++.wt-sc-check.is-hit .wt-sc-dot { background: var(--accent, #3b82f6); border-color: var(--accent, #3b82f6); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 25%, transparent); }
++.wt-sc-alert { position: relative; margin: -10px 0 0 auto; width: 86%; display: flex; gap: 10px; align-items: center; padding: 12px 14px; border-radius: 14px; border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 45%, var(--border, rgba(255, 255, 255, .1))); background: var(--bg-card, var(--bg-secondary, #141c2a)); box-shadow: 0 10px 26px rgba(0, 0, 0, .25); }
++.wt-sc-alert > svg { color: var(--accent, #3b82f6); width: 20px; height: 20px; }
++.wt-sc-alert span { display: grid; gap: 2px; min-width: 0; }
++.wt-sc-alert b { font-size: 13.5px; color: var(--text-primary, #e8ecf3); }
++.wt-sc-alert small { font-size: 12px; color: var(--text-muted, #93a0b4); }
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex a84b323..be44e81 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -125,6 +125,22 @@ const QUIET_ACTION = [{ kind: 'event', event_type: 'watch.watch.triggered' }];
const INT = (v, d) => { const n = parseInt(v, 10); return Number.isFinite(n) ? n : d; };
/** Same-site writes only: another site must not make a signed-in visitor act here. */
++/**
++ * The home's picture (MOTION-1, 2026-11-02: it drew the generic aura): one watch's own history. A price, its rule, the
++ * checks every two hours with what each one read, the check that crossed the line, and the alert it sent. Every part is
++ * something a watch keeps.
++ */
++function watchScene() {
++ const ic = (n) => require('openvibe-shared/kit').icon(n);
++ const row = (time, value, extra = '') => `<li class="wt-sc-check${extra ? ' is-hit' : ''}"><span class="wt-sc-dot"></span><time>${time}</time><b>${value}</b>${extra}</li>`;
++ return `<div class="wt-scene" aria-hidden="true">
++<div class="wt-sc-card"><div class="wt-sc-head"><b>Price: ThinkPad X1</b><span>${ic('clock')} every 2 h</span></div>
++<div class="wt-sc-rule">Tell me when it is <b>under $1,250</b></div>
++<ol class="wt-sc-checks">${row('09:00', '$1,349')}${row('11:00', '$1,349')}${row('13:00', '$1,199', '<em>↓ $150</em>')}</ol></div>
++<div class="wt-sc-alert">${ic('bell')}<span><b>Price dropped to $1,199</b><small>Told you at 13:00 · the check is kept</small></span></div>
++</div>`;
++}
++
function sameOrigin(req, baseUrl) {
const site = String(req.get('sec-fetch-site') || '');
if (site) return site === 'same-origin';
@@@ -565,6 +581,7 @@ ${field('The coding task', html`<textarea id="${idOf('codes_brief')}" name="code
? [{ label: 'New watch', href: '/watches/new?template=page', primary: true }, { label: 'Your watches', href: '/watches' }]
: [{ label: 'Sign in to start', href: '/auth/login?next=%2Fwatches%2Fnew%3Ftemplate%3Dpage', primary: true }, { label: 'How it checks', href: '#h-check' }],
note: yours || 'Free while it is in alpha. Reading this page needs no account.',
++ aside: { html: watchScene() },
});
const ICONS = { page: 'ov:docs', price: 'ov:deals', feed: 'ov:news', json: 'ov:json', event: 'ov:live', webhook: 'ov:code', lan: 'ov:plug' };
const [channelRows, plans, postRows] = await Promise.all([reads.channels(), reads.plans(), reads.posts()]);
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 5320b2b..40cdc5c 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -103,6 +103,13 @@ t('the API sample\'s body is one the API accepts', async () => {
assert.strictEqual(r.status, 201, r.text);
});
++t('the hero is one watch\'s own history, not the generic aura (MOTION-1)', async () => {
++ const page = await plain.get('/', { headers: { accept: 'text/html' } });
++ assert.match(page.text, /<div class="wt-scene" aria-hidden="true">/);
++ assert.match(page.text, /under \$1,250[\s\S]*?\$1,199[\s\S]*?Price dropped to \$1,199/);
++ assert.ok(!/sc-aura-glow/.test(page.text), 'no generic aura');
++});
++
t('done', async () => { await search.close(); await svc.stop(); await plain.stop(); });
t.run();