openvibers/openvibe.design Commits

Components: Metrics, and rule numbers-drawn-once (#313)

OP OpenVibers committed 6 h ago · 089e61f9cb · 5 files, +86 −4

The page draws every part of openvibe-shared/ov-metrics.js (4.232.0) with the real module this site pins: tiles with
deltas that know whether up is good, the trend line, percentiles, ranked bars, the split and the sortable table, on
example numbers the page labels as examples. The rule: Report, Exchange, Compare and the admin panel draw their AI and
usage numbers with it, in its words, and a number nobody measured is a dash.

Changes

ddiff --git a/public/css/specimens.css b/public/css/specimens.css
iindex 96b685f..c33c457 100644
---- a/public/css/specimens.css
++++ b/public/css/specimens.css
@@@ -178,3 +178,12 @@
.crl-note-link b { font-size: 1rem; }
.crl-note-link span { color: var(--ovk-muted, var(--text-muted, #9aa6b8)); font-size: .87rem; line-height: 1.35; }
.crl-note-link:hover b, .crl-note-link:focus-visible b { text-decoration: underline; }
++
++/* /components/metrics: ov-metrics.js draws every part; this only lays the parts out (one column on a phone). */
++.spec--metrics .spec-ovm-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 16px; margin-top: 16px; }
++.spec--metrics figure { margin: 0; padding: 14px; border: 1px solid var(--border, #2c2c38); border-radius: 12px; min-width: 0; }
++.spec--metrics figcaption { margin: 0 0 10px; color: var(--text-muted, #9aa6b8); font-size: 12.5px; }
++.spec--metrics figcaption code { color: var(--text-primary, #e6edf7); }
++.spec--metrics .spec-ovm-wide { grid-column: 1 / -1; overflow-x: auto; }
++.spec--metrics [data-ovm] { min-height: 40px; min-width: 0; }
++.spec--metrics .spec-ovm-label { margin: 0 0 12px; font-size: 12.5px; color: var(--text-muted, #9aa6b8); }
ddiff --git a/public/js/component-metrics.js b/public/js/component-metrics.js
nnew file mode 100644
iindex 0000000..414a1b7
---- /dev/null
++++ b/public/js/component-metrics.js
@@@ -0,0 +1,41 @@
++'use strict';
++// /components/metrics: each part of openvibe-shared/ov-metrics.js, drawn by the real module this site pins, with the
++// example numbers the page labels as examples. Nothing here draws a number itself.
++(function () {
++ const M = window.OVMetrics;
++ const slot = (name) => document.querySelector(`[data-ovm="${name}"]`);
++ if (!M) return;
++ const f = M.format;
++ const put = (name, node) => { const s = slot(name); if (s && node) s.replaceChildren(node); };
++
++ put('tiles', M.tiles([
++ { label: 'Calls', value: f.count(18420), delta: 0.12, trend: [3, 5, 4, 8, 9, 11, 12] },
++ { label: 'Time to first token', value: f.ms(540), sub: 'p95 1.3 s', delta: 0.08, better: 'down' },
++ { label: 'Speed', value: f.tps(86), sub: 'output tokens a second, p50' },
++ { label: 'Succeeded', value: f.rate(0.9941), delta: -0.002 },
++ { label: 'Charged', value: f.usd(61.2), sub: 'last 30 days' },
++ { label: 'Not measured', value: f.ms(null), sub: 'unknown is a dash, never 0' },
++ ]));
++ put('spark', M.spark([12, 14, 11, 18, 22, 19, 26, 31, 28, 35], { label: 'Calls a day', width: 280, height: 56 }));
++ put('bars', M.bars([
++ { label: 'Actor', value: 24.1 }, { label: 'Chat', value: 18.6 }, { label: 'Live', value: 9.3 },
++ { label: 'Codes', value: 6.8 }, { label: 'a-product-with-a-very-long-name-that-must-not-push-the-page', value: 2.4 },
++ ], { format: f.usd }));
++ put('percentiles', M.percentiles({ p50: 540, p90: 980, p95: 1310, n: 13080 }, { label: 'Time to first token' }));
++ put('split', M.split([
++ { label: 'Charged', value: 61.2 }, { label: 'Saved by cache', value: 19.8 },
++ ], { format: f.usd }));
++ put('table', M.table([
++ { model: 'deepseek-flash', ttft: 540, tps: 86, ok: 0.994, usd: 0.0042 },
++ { model: 'gpt-5-mini', ttft: 820, tps: 61, ok: 0.998, usd: 0.0061 },
++ { model: 'gemini-4-hydrogen', ttft: null, tps: null, ok: null, usd: null },
++ { model: 'glm-5.3', ttft: 1150, tps: 133, ok: 0.989, usd: 0.0118 },
++ ], [
++ { key: 'model', label: 'Model' },
++ { key: 'ttft', label: 'First token p50', numeric: true, format: f.ms },
++ { key: 'tps', label: 'Speed p50', numeric: true, bar: true, format: f.tps },
++ { key: 'ok', label: 'Succeeded', numeric: true, format: f.rate },
++ { key: 'usd', label: 'Per call', numeric: true, format: f.usd },
++ ], { caption: 'Example models: a click on a heading sorts, a second click turns it' }));
++ for (const s of document.querySelectorAll('[data-ovm-wait]')) s.hidden = true;
++}());
ddiff --git a/server/rules/register.js b/server/rules/register.js
iindex 9f24d52..4ced094 100644
---- a/server/rules/register.js
++++ b/server/rules/register.js
@@@ -589,6 +589,10 @@ const RULES = Object.freeze([
"What a person makes is one small document of ten parts (face, brain, knows, hands, places, memory, rules, when, makes, team), each part a slot fed by the product that knows it best: Design for the face, Exchange for the models and prices, Tools and every product for the hands, Link for the machines, Events and Sources for what starts it. Vibe is one too. A blueprint asks and never grants: installing shows every ask in words, a new version that asks for more waits for a yes, it carries no key or token, and money, passwords, deletions, new recipients and posting in a person's name always ask. Made on Design (Create, Agent), shared and remixed in Design's gallery, run from any chat, composer, schedule, event or machine.",
'2026-11-12', 'The owner: one agent system, "multi purpose reusable in a modular and API/sdk driven way", the same one other people use, with Exchange, Link, Design, Chat, Codes, Tools and shared hardware all part of it.',
[], ['openvibe-contracts test: agents.blueprint fixtures', 'openvibe-sdk test: agents-blueprint']),
++ R('numbers-drawn-once', 'honest', 'Every number is drawn by ov-metrics, and a number nobody measured is a dash.',
++ 'Report, Exchange, Compare and the admin panel draw AI and usage numbers with openvibe-shared/ov-metrics.js (tiles, a trend line, ranked bars, percentiles, a split, a sortable table) and say them in its words: 540 ms, $0.0042, 86.0 tok/s, 99.4%. A percentile is the median and the 95th on one scale, with how many calls it rests on. Unknown is a dash, never 0, and a delta says in words whether it got better.',
++ '2026-11-12', 'The owner asked for tokens, caching, model, time and cost per task, tokens per second and time to first token on Report, Compare and Exchange, with the designs reused through openvibe.design. Three sites drawing their own charts would show one number three ways.',
++ [{ label: 'Metrics', href: '/components/metrics' }], ['openvibe-shared test: ov-metrics-chrome', 'OpenVibe.Design test: changes']),
]);
// The register's order is the rule's number: a person can cite "rule 7" and a link can carry it.
ddiff --git a/server/views/component-notes.js b/server/views/component-notes.js
iindex b5fc431..5856c68 100644
---- a/server/views/component-notes.js
++++ b/server/views/component-notes.js
@@@ -1,7 +1,7 @@
'use strict';
/**
* The component pages for what has shipped since the first registry: the bar's brand, the search pill, the quest
-- * card, the avatar, the player, soft pages, icon ink and the footer. Each page has one specimen, built from the
++ * card, the avatar, the player, soft pages, icon ink, the footer and metrics. Each page has one specimen, built from the
* openvibe-shared this site pins wherever that package already holds the part, and the release, the changelog words
* and the numbered rules behind it. A part that is still in review says so; nothing is claimed as shipped before it is.
*
@@@ -151,6 +151,20 @@ const SPECIMENS = {
</div>
<p class="spec-note">The footer's online strip says the short count, the faces and one tooltip. Hovering or focusing the count shows "In the last 30 minutes" and the members and guests beneath it; a press opens the same words above the list of names. The tooltip file is fetched by the first pointer over the bar, not with the page.</p>
</div>`,
++
++ metrics: () => `<div class="spec spec--metrics">
++ <p class="spec-ovm-label">Example numbers, so every part shows. On Report, Exchange and Compare the same parts draw real ones.</p>
++ <div data-ovm="tiles"></div>
++ <div class="spec-ovm-grid">
++ <figure><figcaption><code>OVMetrics.spark(values)</code></figcaption><div data-ovm="spark"></div></figure>
++ <figure><figcaption><code>OVMetrics.percentiles({ p50, p90, p95, n })</code></figcaption><div data-ovm="percentiles"></div></figure>
++ <figure><figcaption><code>OVMetrics.bars(rows, { format })</code></figcaption><div data-ovm="bars"></div></figure>
++ <figure><figcaption><code>OVMetrics.split(parts, { format })</code></figcaption><div data-ovm="split"></div></figure>
++ <figure class="spec-ovm-wide"><figcaption><code>OVMetrics.table(rows, columns)</code></figcaption><div data-ovm="table"></div></figure>
++ </div>
++ <p class="spec-note" data-ovm-wait>The parts draw in the browser from <code>/shared/ov-metrics.js</code>; with script off, a page shows its own table of the same numbers.</p>
++ <p class="spec-note">Every part takes its colours from the theme (tokens only), fits 390 px, reads in both themes and holds still with motion off. A delta knows whether up is good: more calls is green, a slower first token is red, and both say it in words. A number nobody measured is a dash, never 0.</p>
++</div>`,
};
const NOTES = Object.freeze({
@@@ -253,6 +267,15 @@ const NOTES = Object.freeze({
rules: ['two-icon-sets', 'only-the-pair', 'targets-44'],
build: `// openvibe-shared/app-icon.js: the favicon's heavy form on a rounded tile\nfavicon({ site }) // mark(site, 0.58, 'heavy')\n\n// the test measures the painted bounds in headless Chrome, not the viewBox:\ntest/icon-ink.test.js // ink within 4% of the target, centre within 3%`,
},
++ metrics: {
++ title: 'Metrics',
++ summary: 'The network’s one way to show numbers: tiles, a trend line, ranked bars, percentiles, a split and a sortable table.',
++ release: { version: '4.232.0', state: 'shipped', href: '/changes#v4.232.0' },
++ shipped: '4.232.0 shipped ov-metrics.js. Report, Exchange, Compare and the admin panel draw their AI and usage numbers with it: time to first token, tokens a second, success, cache share, cost per call and per task. The words come from OVMetrics.format, which servers also use, so 540 ms, $0.0042 and 86.0 tok/s read the same on every site.',
++ quote: '“ov-metrics.js: the network’s one way to show numbers … nobody draws a chart of their own.” — the 4.232.0 change, for every AI call measured everywhere',
++ rules: ['numbers-drawn-once', 'state-in-words', 'no-fake-content'],
++ build: `<script src="/shared/ov-metrics.js" defer></script>\n\n// tiles with a delta that knows whether up is good\nslot.append(OVMetrics.tiles([\n { label: 'Calls', value: OVMetrics.format.count(18420), delta: 0.12, trend: daily },\n { label: 'Time to first token', value: OVMetrics.format.ms(row.ttft_ms.p50), sub: 'p95 ' + OVMetrics.format.ms(row.ttft_ms.p95), better: 'down' },\n]));\n// p50 and p95 on one scale, from report.ai-performance@1\nslot.append(OVMetrics.percentiles(row.ttft_ms, { label: 'Time to first token' }));\n// a sortable table; a numeric column can carry a bar\nslot.append(OVMetrics.table(rows, [{ key: 'model', label: 'Model' }, { key: 'tps', label: 'Speed', numeric: true, bar: true, format: OVMetrics.format.tps }]));\n\n// on a server: the same words\nconst { format } = require('openvibe-shared/ov-metrics');\nformat.ms(1310); // '1.3 s'`,
++ },
footer: {
title: 'The footer',
summary: 'The strip that says how many are online, the faces, and one tooltip that says what the count means.',
@@@ -293,7 +316,8 @@ function notePage(name) {
active: '/components',
styles: ['css/component-registry.css', 'css/specimens.css'],
sharedStyles: name === 'player' ? ['player.css'] : [],
-- shared: name === 'player' ? ['player.js'] : [],
++ shared: name === 'player' ? ['player.js'] : name === 'metrics' ? ['ov-metrics.js'] : [],
++ scripts: name === 'metrics' ? ['js/component-metrics.js'] : [],
body,
});
}
ddiff --git a/test/changes.test.js b/test/changes.test.js
iindex b581107..9901e4f 100644
---- a/test/changes.test.js
++++ b/test/changes.test.js
@@@ -142,14 +142,18 @@ const { PUBLIC_DIR } = require('../server/assets');
await check('the home and every component page link the changes archive', async () => {
assert.match((await t.get('/')).text, /href="\/changes"/, 'the home links it');
const pages = ['/components/registry', '/components/brand', '/components/menu', '/components/navbar', '/components/search-pill',
-- '/components/quest-card', '/components/avatar', '/components/player', '/components/soft-pages', '/components/icon-ink', '/components/footer'];
++ '/components/quest-card', '/components/avatar', '/components/player', '/components/soft-pages', '/components/icon-ink', '/components/footer', '/components/metrics'];
for (const href of pages) {
const r = await t.get(href);
assert.strictEqual(r.status, 200, href);
assert.match(r.text, /href="\/changes"/, `${href} links the archive`);
}
++ const metrics = await t.get('/components/metrics');
++ assert.match(metrics.text, /<script src="[^"]*ov-metrics[^"]*\.js[^"]*" defer>/, 'the metrics page loads the real module');
++ assert.match(metrics.text, /component-metrics[^"]*\.js/, 'and the script that draws its parts');
++ assert.match(metrics.text, /href="\/rules#numbers-drawn-once"/, 'the rule it carries out');
const registry = await t.get('/components/registry');
-- for (const name of ['brand', 'search-pill', 'quest-card', 'avatar', 'player', 'soft-pages', 'icon-ink', 'footer']) {
++ for (const name of ['brand', 'search-pill', 'quest-card', 'avatar', 'player', 'soft-pages', 'icon-ink', 'footer', 'metrics']) {
assert.match(registry.text, new RegExp(`href="/components/${name}"`), name);
}
});