openvibers/openvibe.design Commits
Components: Metrics, and rule numbers-drawn-once (#313)
Changes
@@@ -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); }nnew file mode 100644@@@ -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;++}());@@@ -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.@@@ -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,});}@@@ -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);}});
@@ -178,3 +178,12 @@
@@ -178,3 +178,12 @@
.crl-note-link b { font-size: 1rem; }
.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 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; }
.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); }
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -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;
+}());
@@ -589,6 +589,10 @@ const RULES = Object.freeze([
@@ -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.",
"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.',
'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']),
[], ['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.
// The register's order is the rule's number: a person can cite "rule 7" and a link can carry it.
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* The component pages for what has shipped since the first registry: the bar's brand, the search pill, the quest
* 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
* 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.
* 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 = {
@@ -151,6 +151,20 @@ const SPECIMENS = {
</div>
</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>
<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>`,
</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({
const NOTES = Object.freeze({
@@ -253,6 +267,15 @@ const NOTES = Object.freeze({
@@ -253,6 +267,15 @@ const NOTES = Object.freeze({
rules: ['two-icon-sets', 'only-the-pair', 'targets-44'],
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%`,
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: {
footer: {
title: 'The footer',
title: 'The footer',
summary: 'The strip that says how many are online, the faces, and one tooltip that says what the count means.',
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) {
@@ -293,7 +316,8 @@ function notePage(name) {
active: '/components',
active: '/components',
styles: ['css/component-registry.css', 'css/specimens.css'],
styles: ['css/component-registry.css', 'css/specimens.css'],
sharedStyles: name === 'player' ? ['player.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,
body,
});
});
}
}
@@ -142,14 +142,18 @@ const { PUBLIC_DIR } = require('../server/assets');
@@ -142,14 +142,18 @@ const { PUBLIC_DIR } = require('../server/assets');
await check('the home and every component page link the changes archive', async () => {
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');
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',
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) {
for (const href of pages) {
const r = await t.get(href);
const r = await t.get(href);
assert.strictEqual(r.status, 200, href);
assert.strictEqual(r.status, 200, href);
assert.match(r.text, /href="\/changes"/, `${href} links the archive`);
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');
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);
assert.match(registry.text, new RegExp(`href="/components/${name}"`), name);
}
}
});
});