openvibers/openvibe.services Commits

Home: a taste and a door, 16.1 phone screens to 5.8 (openvibe-shared v4.38.0) (#146)

OP OpenVibers committed 15 days ago · 86dc3077ad · 9 files, +172 −178

The front page stacked three directories of services as text cards (the
generated catalog of 26, the 13 building blocks, nine "what you can build"
cards), integration cards repeating the steps, a quick start repeating the
hero's code, two-column number tiles, open FAQ cards and a CTA repeating
the hero.

Now: the hero with two actions and the code that runs with no key; the
numbers as one kit.stats strip; the building blocks (15, with Tools and
Hosting) as a showcase.apps launcher with See all 37 service APIs
(/docs/api); the five examples as kit rows with the template and an app's
calls folded; the steps with Events' limits folded; the reference with the
resource index, export and the transparency page; the FAQ as
showcase.faq. kit.css is the one new file (no kit-web.js, no Kit 2):
budgets raised by what was measured, reasons in the notes.

Changes

ddiff --git a/README.md b/README.md
iindex 6592663..c8b1bb3 100644
---- a/README.md
++++ b/README.md
@@@ -287,5 +287,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.67.0
--- openvibe-shared: v4.37.0
++- openvibe-shared: v4.38.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 99d32c2..76125bd 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -9,11 +9,11 @@
"domain": "openvibe.services",
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.67.0",
-- "shared": "openvibe-shared v4.37.0",
++ "shared": "openvibe-shared v4.38.0",
"packages": [
"openvibe-contracts v0.222.0",
"openvibe-sdk v0.67.0",
-- "openvibe-shared v4.37.0"
++ "openvibe-shared v4.38.0"
],
"capabilities": [
"services.resource.read",
ddiff --git a/package-lock.json b/package-lock.json
iindex b1af0ee..422e40d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -18,7 +18,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.37.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.38.0",
"pg": "^8.23.0"
},
"devDependencies": {
@@@ -1209,9 +1209,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.37.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.37.0",
-- "integrity": "sha512-zT73fINooICUxuoxT8bwS1C5bh6ieNaDAX6aUaMdzMth1nTXbkwh54bgiPMytAJ2D0a8b3pqxhLYCvxFVzCVqA==",
++ "version": "4.38.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.38.0",
++ "integrity": "sha512-4sEQ78EQaw+cWWJs+AlPRhxObzHT1nwTMz6yHVBL20Mr+raAhhP10DtL1OTnbuMCDVsYhVwpd3QGgmyC6391EA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 180c7e3..ba52ca1 100644
---- a/package.json
++++ b/package.json
@@@ -29,7 +29,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.37.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.38.0",
"pg": "^8.23.0"
},
"devDependencies": {
ddiff --git a/public/css/services.css b/public/css/services.css
iindex 0fd6ead..be1c649 100644
---- a/public/css/services.css
++++ b/public/css/services.css
@@@ -77,19 +77,16 @@ meter.headroom { width: 8rem; max-width: 100%; vertical-align: middle; }
/* The home page's showcase sections (openvibe-shared/showcase.css) sit inside .page, which has its own gutter. */
.page .sc-hero, .page .sc-sec, .page .sc-cta { padding-left: 0; padding-right: 0; }
--.page .sc-ic .ov-icon { --ovi-size: 28px !important; }
.services-hero-code { margin: 0; padding: 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-primary, #0a0f1c); font-size: 13px; line-height: 1.6; overflow: auto; }
--/* The home page: the platform in numbers, quick-start samples one under another (their lines are long), and the FAQ. */
--.page .svc-num { margin-top: 28px; }
--.svc-num ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(min(170px, 100%), 1fr)); }
--.svc-num a { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-secondary, #101828); color: inherit; text-decoration: none; }
--.svc-num a:hover, .svc-num a:focus-visible { border-color: var(--accent, #3b82f6); }
--.svc-num b { font-size: 26px; font-weight: 800; letter-spacing: -0.5px; line-height: 1.15; }
--.svc-num span { font-size: 13px; line-height: 1.35; color: var(--text-secondary, #96a7c2); }
--#quick-start .sc-code { grid-template-columns: minmax(0, 1fr); }
--#build .sc-note code { overflow-wrap: anywhere; }
--.svc-faq { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); }
--.svc-faq > div { padding: 16px 18px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); background: var(--bg-secondary, #101828); }
--.svc-faq h3 { margin: 0 0 6px; font-size: 15.5px; line-height: 1.35; }
--.svc-faq p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--text-secondary, #96a7c2); }
++/* The home page: the numbers strip sits close under the hero; the quick start's examples list and its folds
++ (openvibe-shared kit.css) keep the kit's own spacing; the shipped list follows the last section at a section's gap. */
++.page .svc-num { margin-top: 24px; }
++.page .svc-num .ovk-stats { margin-top: 0; margin-bottom: 0; }
++#quick-start .ovk-list-head { margin-top: 14px; }
++#quick-start .ovk-fold pre { margin: 6px 0 12px; }
++#quick-start .ovk-fold .sc-note { margin: 0; }
++#how .ovk-fold { margin-top: 18px; }
++#reference .ovk-fold { margin-top: 12px; }
++.page > .ov-shipped-home { margin-top: 40px; }
++#how .ovk-fold .sc-lede { font-size: .95rem; }
ddiff --git a/server/http/home.js b/server/http/home.js
iindex 6f9b91b..526b244 100644
---- a/server/http/home.js
++++ b/server/http/home.js
@@@ -1,16 +1,18 @@
'use strict';
/**
-- * The home page's content beyond the kit's generic sections: the platform in numbers, what people build on it (each
-- * card is a working example in OpenVibe.Examples or a generated reference page), quick-start code checked against the
-- * pinned SDK, and the questions developers ask. Every number is counted from the pinned contracts or read from
-- * OpenVibe.Network's public status within the last minute; nothing here is typed in as a figure.
++ * The home page's content beyond the kit's generic sections, built for a developer on a phone (the owner 2026-10-28:
++ * "act more like an app, not a huge long web page"): the platform in numbers as one strip, the building blocks as a
++ * launcher, quick-start code folded under the working examples, and the questions developers ask as an accordion.
++ * Every number is counted from the pinned contracts or read from OpenVibe.Network's public status within the last
++ * minute; nothing here is typed in as a figure.
*/
--const contracts = require('openvibe-contracts');
const showcase = require('openvibe-shared/showcase');
++const kit = require('openvibe-shared/kit');
const esc = showcase.esc;
const EXAMPLES = 'https://github.com/OpenVibers/OpenVibe.Examples';
const example = (name) => `${EXAMPLES}/tree/main/examples/${name}`;
++const sdkInstall = (v) => `npm i https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v${v}`;
// ── Services up: Network's public status (GET /api/v1/status, the registry's health poll), read in the background at
// most once a minute and never awaited by a page. A count is shown only when at least half the live services were
@@@ -43,100 +45,102 @@ function createNetworkStatus({ url, enabled = true, fetchImpl = globalThis.fetch
};
}
--const publicCaps = (m) => (m.capabilities || []).filter((c) => { const cap = contracts.capabilities.get(c); return cap && cap.status === 'active' && cap.visibility === 'public'; });
--
--/** The platform in numbers: counted from the pinned contracts and SDK, plus services up when Network answered. */
++/** The platform in numbers: one strip (it scrolls sideways on a phone), counted from the pinned contracts and SDK, plus
++ * services up when Network answered. Each number opens the reference it counts. */
function numbers({ docs, programmable, status }) {
const items = [
-- [programmable.length, 'services you can call', `/docs/services`],
++ [programmable.length, 'services you can call', '/docs/services'],
[docs.capabilities.filter((c) => c.grantable && c.visibility === 'public').length, 'capabilities an app can request', '/docs/capabilities?grantable=1'],
[docs.events.length, 'event types to subscribe to', '/docs/events'],
[docs.sdk.length, 'SDK modules, typed', '/docs/sdk'],
status ? [status.up, `services up now, of ${status.checked} checked`, 'https://openvibe.network/status'] : null,
].filter((x) => x && Number(x[0]) > 0);
-- return `<section class="sc-sec svc-num" aria-label="The platform in numbers"><ul>${items.map(([n, label, href]) => `<li><a href="${esc(href)}"><b>${esc(Number(n).toLocaleString('en-US'))}</b><span>${esc(label)}</span></a></li>`).join('')}</ul></section>`;
++ return `<div class="sc-sec svc-num">${kit.stats(items, { label: 'The platform in numbers' })}</div>`;
}
--// What people build: one card per thing an app can do today, each pointing at code that does it. Text only, like
--// the services list: nine inline ring icons would add a quarter to the page's weight.
--const BUILD = [
-- { title: 'Sign people in with OpenVibe', text: 'OAuth 2.0 with PKCE: a public web app needs no secret, a server app verifies tokens offline. Example: oauth-app and browser-app.', href: example('oauth-app') },
-- { title: 'Store and serve files', text: 'Upload into your project\'s own Media space; sandbox files come back with signed, expiring links. Example: media-uploader.', href: example('media-uploader') },
-- { title: 'React to what happens', text: 'Publish to your project\'s topic, pull with a durable cursor, or take signed webhooks. Example: event-subscriber and webhook-consumer.', href: example('webhook-consumer') },
-- { title: 'Run tools as jobs', text: 'Submit an OpenVibe.Tools job once and follow its stream across restarts. Example: tool-job.', href: example('tool-job') },
-- { title: 'Build a chat bot', text: 'A bot that stays in its one room and always says it is a bot. Example: chat-bot.', href: example('chat-bot') },
-- { title: 'Search the network', text: 'Streams, recordings, pastes, posts, jobs and help answers from across the network in one query. Public results need no key.', href: '/docs/api/search' },
-- { title: 'Call many AI models', text: 'Chat, summaries, extraction and embeddings through one API; every run says which model answered and what it cost.', href: 'https://ai.openvibe.services/' },
-- { title: 'Keep objects in S3 buckets', text: 'Private storage zones that the AWS SDKs, rclone and backup tools reach over S3, with keys scoped to a zone.', href: 'https://openvibe.zone/' },
-- { title: 'Run jobs on a node', text: 'Send a job to one API; it runs on a node that offers its runtime, including your own computer, and streams its output.', href: 'https://openvibe.run/' },
--];
--
// The building blocks (plan track K, the owner 2026-10-24: consumer products for people, "while also having our services
--// website" for builders): every service an app can call, by the plain name the network's menus use since contracts
--// 0.213.0, with the domain that documents it. People who only want to use OpenVibe are sent to Vibe.
++// website" for builders): every service an app can call, by a plain name short enough for a phone's launcher tile, with
++// one line of what it is (short enough for a wide screen's tile) and the place that documents it. On the home they are
++// a launcher (openvibe-shared showcase.apps): a phone shows the icons three to a row under the name, wider screens add
++// the line. People who only want to use OpenVibe are sent to Vibe.
const BLOCKS = [
-- { title: 'AI API', text: 'Many models behind one OpenAI-compatible API: chat, summaries, extraction, embeddings, images. Every run says which model answered and what it cost.', href: 'https://ai.openvibe.services/' },
-- { title: 'Agents', text: 'Hand a task in plain words to the best agent for it (OpenVibe\'s own or another platform\'s), checked by a second model, with its cost and why. Runs now or on a schedule.', href: 'https://openvibe.actor/docs' },
-- { title: 'Compute', text: 'Send a job to one API; it runs on a node that offers its runtime, your own computer included, metered per second.', href: 'https://openvibe.run/' },
-- { title: 'Storage', text: 'Private S3-compatible zones per project and environment, for the AWS SDKs, rclone and backup tools.', href: 'https://openvibe.zone/' },
-- { title: 'Media API', text: 'Upload, transcode and serve video, clips and files from your project\'s own space, with signed links.', href: 'https://openvibe.media/' },
-- { title: 'Events', text: 'Publish to your project\'s topic and follow it with durable cursors or signed webhooks.', href: 'https://openvibe.events/' },
-- { title: 'Chat for your site', text: 'A support inbox or a chat room on any website with one tag: Vibe answers first, your team takes over. An open visitor API, the SDK, signed webhooks, and live feeds from Claude Code.', href: 'https://openvibe.chat/developers' },
-- { title: 'Search', text: 'Streams, recordings, posts, jobs, rentals and help answers across the network in one query.', href: '/docs/api/search' },
-- { title: 'Robots and devices', text: 'Pair a robot or a computer, drive it through one gate with roles, limits and an e-stop, and write drivers as plugins.', href: 'https://openvibe.bot/' },
-- { title: 'Sign-in and accounts', text: 'OAuth 2.0 with PKCE for your users, scoped capability grants for your apps, export and deletion handled for you.', href: '/docs' },
-- { title: 'Payments and items', text: 'Tips, memberships and checkout through one ledger; items people earn and wear across every site.', href: 'https://openvibe.vip/' },
-- { title: 'Game services', text: 'Achievements, leaderboards and cloud saves for any game.', href: 'https://play.openvibe.network/' },
-- { title: 'Code with any agent', text: 'The open coding-agent harness: Claude Code, Codex, OpenCode, DeepSeek and self-hosted models behind one router.', href: 'https://openvibe.codes/' },
++ { icon: 'ai', name: 'AI API', text: 'Many models, one API', href: 'https://ai.openvibe.services/' },
++ { icon: 'actor', name: 'Agents', text: 'Hand a task to an agent', href: 'https://openvibe.actor/docs' },
++ { icon: 'codes', name: 'Coding', text: 'Coding agents, one router', href: 'https://openvibe.codes/' },
++ { icon: 'run', name: 'Compute', text: 'Jobs on nodes, per second', href: 'https://openvibe.run/' },
++ { icon: 'zone', name: 'Storage', text: 'S3-compatible zones', href: 'https://openvibe.zone/' },
++ { icon: 'space', name: 'Hosting', text: 'Host sites and apps', href: 'https://openvibe.space/' },
++ { icon: 'media', name: 'Media API', text: 'Video, clips and files', href: 'https://openvibe.media/' },
++ { icon: 'events', name: 'Events', text: 'Topics, cursors, webhooks', href: 'https://openvibe.events/' },
++ { icon: 'search', name: 'Search', text: 'The network in one query', href: '/docs/api/search' },
++ { icon: 'account', name: 'Sign-in', text: 'OAuth 2.0 with PKCE', href: '/oauth' },
++ { icon: 'billing', name: 'Payments', text: 'Tips and memberships', href: 'https://openvibe.vip/' },
++ { icon: 'chat', name: 'Chat', text: 'Support chat, one tag', href: 'https://openvibe.chat/developers' },
++ { icon: 'tools', name: 'Tools', text: 'Hundreds of tools as APIs', href: 'https://openvibe.tools/developers' },
++ { icon: 'bot', name: 'Robots', text: 'Robots through one gate', href: 'https://openvibe.bot/' },
++ { icon: 'play', name: 'Games', text: 'Achievements and saves', href: 'https://play.openvibe.network/' },
];
--function blocks() {
-- return showcase.features({
++/** The building blocks as a launcher, then every service's API reference (See all) and the way to Vibe. */
++function blocks({ apis }) {
++ return showcase.apps({
id: 'blocks', title: 'The building blocks',
lede: 'Every OpenVibe product runs on these, and your app can call the same ones with a project\'s token.',
-- items: BLOCKS,
-- }).replace(/<\/section>$/, () => '<p class="sc-note">Just want to use OpenVibe? <a href="https://openvibe.chat/vibe">Chat with Vibe</a>, OpenVibe\'s assistant, or <a href="https://openvibe.network/">see everything people do here</a>.</p></section>');
++ items: BLOCKS.map((b) => ({ ...b, icon: `ov:${b.icon}` })),
++ more: apis ? { href: '/docs/api', label: `See all ${apis} service APIs` } : null,
++ }).replace(/<\/section>$/, () => '<p class="sc-note">Just here to use OpenVibe? <a href="https://openvibe.chat/vibe">Chat with Vibe</a> or <a href="https://openvibe.network/">see the network</a>.</p></section>');
}
--function build() {
-- return showcase.features({
-- id: 'build', title: 'What you can build today',
-- lede: 'Each card is a working example in OpenVibe.Examples (MIT, with a smoke test) or a service you can call now.',
-- items: BUILD,
-- }).replace(/<\/section>$/, () => `<p class="sc-note">Start from one: <code>npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 create-openvibe-app my-app --template web</code> (templates: web, server, bot, mod). <a href="${EXAMPLES}">All examples</a> · <a href="/mod">Every way to mod the network</a>.</p></section>`);
--}
++// Working examples in OpenVibe.Examples (MIT, each with a smoke test): the quickest start is one of these.
++const BUILD = [
++ { title: 'Sign people in with OpenVibe', text: 'oauth-app · PKCE, no secret in a web app', href: example('oauth-app') },
++ { title: 'Store and serve files', text: 'media-uploader · your project\'s Media space', href: example('media-uploader') },
++ { title: 'React to what happens', text: 'webhook-consumer · topics and signed webhooks', href: example('webhook-consumer') },
++ { title: 'Run tools as jobs', text: 'tool-job · follow its stream across restarts', href: example('tool-job') },
++ { title: 'Build a chat bot', text: 'chat-bot · stays in its room, says it is a bot', href: example('chat-bot') },
++];
--/** Quick start: code that runs against production with the pinned SDK (the first sample needs no key at all). */
--function quickStart({ sdkVersion }) {
-- const install = `npm i https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v${sdkVersion}`;
-- return showcase.code({
-- id: 'quick-start', title: 'Quick start',
-- lede: `openvibe-sdk v${sdkVersion}, for Node and the browser. Public reads need no key; an app's calls use the client id and secret of a sandbox app in your project.`,
-- samples: [
-- { label: 'No key: search the network and list its services', lang: 'js', code: `// ${install}
++/** The hero's code: it runs as written against production with the pinned SDK, with no key (test/home.test.js). */
++function heroCode({ sdkVersion }) {
++ return `// openvibe-sdk v${sdkVersion}
const sdk = require('openvibe-sdk');
--const ov = sdk.createClient(); // public reads need no token
++const ov = sdk.createClient(); // public reads need no key
const search = sdk.search.createSearchClient(ov);
--const { results } = await search.query('patch notes', { owner: 'blog' });
++const { results } = await search.query('go live');
const registry = sdk.registry.createRegistryClient(ov);
--const services = await registry.services(); // every service, with its health` },
-- { label: 'With an app: upload a file to your project', lang: 'js', code: `const sdk = require('openvibe-sdk');
++const services = await registry.services();`;
++}
++
++const pre = (code, lang) => `<pre tabindex="0"><code class="language-${lang}">${esc(code)}</code></pre>`;
++
++/**
++ * Quick start: the working examples first (one tap to the code), then what is read once, folded: a new app from a
++ * template, and an app's calls past the hero's code. Both samples run against production with the pinned SDK.
++ */
++function quickStart({ sdkVersion }) {
++ const app = `// ${sdkInstall(sdkVersion)}
++const sdk = require('openvibe-sdk');
const tokens = sdk.auth.createServiceTokenClient({
clientId: process.env.OV_CLIENT_ID, clientSecret: process.env.OV_CLIENT_SECRET,
});
const ov = sdk.createClient({ tokenProvider: tokens });
const { unverifiedClaims } = await tokens.getTokenInfo({ audience: 'openvibe.media' });
const media = sdk.media.createMediaClient(ov, { app: unverifiedClaims.project_id });
--const file = await media.upload(bytes, { filename: 'logo.png', contentType: 'image/png' });` },
-- { label: 'Any language: an app token over HTTP', lang: 'bash', code: `curl -s https://openvibe.network/oauth/token \\
++const file = await media.upload(bytes, { filename: 'logo.png', contentType: 'image/png' });`;
++ const http = `curl -s https://openvibe.network/oauth/token \\
-d grant_type=client_credentials \\
-d client_id="$OV_CLIENT_ID" -d client_secret="$OV_CLIENT_SECRET" \\
-d audience=openvibe.media
--# → {"access_token":"…","token_type":"Bearer","expires_in":300,…}` },
-- ],
-- });
++# → {"access_token":"…","token_type":"Bearer","expires_in":300,…}`;
++ const template = `npx --package=https://codeload.github.com/OpenVibers/OpenVibe.Examples/tar.gz/refs/tags/v0.3.0 \\
++ create-openvibe-app my-app --template web`;
++ return `<section class="sc-sec" id="quick-start" aria-labelledby="h-quick-start"><h2 id="h-quick-start">Quick start</h2>
++<p class="sc-lede">${esc(`openvibe-sdk v${sdkVersion}, for Node and the browser. Public reads need no key.`)}</p>
++${kit.list(BUILD.map((b) => kit.row({ href: b.href, title: b.title, sub: b.text })), { title: 'Start from a working example', more: { href: EXAMPLES, label: 'All examples' } })}
++${kit.fold({ id: 'qs-template', title: 'A new app from a template', meta: 'npx', body: `${pre(template, 'bash')}<p class="sc-note">Templates: web, server, bot and mod. <a href="/mod">Every way to mod the network</a>.</p>` })}
++${kit.fold({ id: 'qs-app', title: 'Calls with an app\'s key', meta: 'Node and curl', body: `<p class="sc-note">A sandbox app in your project has a client id and secret. Upload a file from Node:</p>${pre(app, 'js')}<p class="sc-note">Or get its token over HTTP, from any language:</p>${pre(http, 'bash')}` })}</section>`;
}
// The questions developers ask, answered from what the platform does today. The FAQPage JSON-LD repeats them.
@@@ -149,8 +153,9 @@ const FAQ = [
{ q: 'Is it open source?', a: 'Yes. Every OpenVibe service is open source on GitHub under OpenVibers, and the examples are MIT, so you can copy them into your own project.' },
];
++/** The questions as an accordion (openvibe-shared showcase.faq): each answer one tap away, the first one open. */
function faq() {
-- return `<section class="sc-sec" id="faq" aria-labelledby="h-faq"><h2 id="h-faq">Questions developers ask</h2><div class="svc-faq">${FAQ.map((p) => `<div><h3>${esc(p.q)}</h3><p>${esc(p.a)}</p></div>`).join('')}</div></section>`;
++ return showcase.faq({ id: 'faq', title: 'Questions developers ask', items: FAQ.map((p) => [p.q, p.a]) });
}
/** "120 per_minute" → "120 a minute", bytes → KB/MB, a bare count → the number. */
@@@ -170,4 +175,4 @@ function limitAmount(v, unit) {
}
}
--module.exports = { createNetworkStatus, numbers, blocks, build, quickStart, faq, FAQ, BLOCKS, BUILD, limitAmount, publicCaps };
++module.exports = { createNetworkStatus, numbers, blocks, heroCode, quickStart, faq, FAQ, BLOCKS, BUILD, limitAmount };
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 32244aa..3bb7433 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -15,13 +15,14 @@ const contracts = require('openvibe-contracts');
const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
++const kit = require('openvibe-shared/kit');
const seo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
const path = require('path');
const express = require('express');
const { asyncRouter } = require('./router');
const { createDiscoveryRoutes, homeJsonLd, COMMUNITY, CODES_ORIGIN } = require('./discovery');
--const { html, raw, table, code, badge, time, notice, csrfField, problemBox } = require('../render/html');
++const { html, raw, render, table, code, badge, time, notice, csrfField, problemBox } = require('../render/html');
const { send } = require('../render/layout');
const { markdown } = require('../render/markdown');
const { csrfToken, checkCsrf, sameOrigin } = require('../auth/forms');
@@@ -49,89 +50,63 @@ function createPageViews(ctx) {
const eventsSource = ctx.limits && ctx.limits.bySource ? ctx.limits.bySource('events') : null;
const eventsLimits = eventsSource ? await ctx.limits.read(eventsSource) : null;
const limitRows = eventsLimits && eventsLimits.body ? eventsLimits.body.limits.slice(0, 6).map((l) => ({ label: l.label, values: [home.limitAmount(l.sandbox, l.unit), home.limitAmount(l.production, l.unit)] })) : [];
-- // The hero's code runs as written against production with the pinned SDK: public reads need no key
-- // (test/home.test.js runs it against a mock).
-- const snippet = `// openvibe-sdk v${docs.sdkVersion}
--const sdk = require('openvibe-sdk');
--const ov = sdk.createClient(); // public reads need no key
--
--const search = sdk.search.createSearchClient(ov);
--const { results } = await search.query('go live');
--
--const registry = sdk.registry.createRegistryClient(ov);
--const services = await registry.services();`;
// The services a project can call: every live service with a generated API reference and at least one
-- // capability an app can be granted (Services itself aside), with its tagline from its site block or its
-- // product manifest. Read from the pinned contracts, never typed here.
-- const apiDocs = new Set(contracts.openapi.index().map((x) => x.service));
-- const productTagline = new Map(contracts.products.catalog().filter((p) => p.relationships && p.relationships.service && p.tagline).map((p) => [p.relationships.service, p.tagline]));
++ // capability an app can be granted (Services itself aside). Read from the pinned contracts, never typed here;
++ // the home counts them, and /docs/api lists every reference with its routes and capabilities.
++ const apiIndex = contracts.openapi.index();
++ const apiDocs = new Set(apiIndex.map((x) => x.service));
const grantable = (m) => (m.capabilities || []).some((c) => { const cap = contracts.capabilities.get(c); return cap && cap.status === 'active' && cap.visibility === 'public'; });
const programmable = contracts.services.manifests
-- .filter((m) => m && m.id !== 'services' && m.exposure && m.exposure.state === 'live' && apiDocs.has(m.id) && grantable(m))
-- .sort((a, b) => ((a.site && a.site.position) ?? 50) - ((b.site && b.site.position) ?? 50) || a.name.localeCompare(b.name));
++ .filter((m) => m && m.id !== 'services' && m.exposure && m.exposure.state === 'live' && apiDocs.has(m.id) && grantable(m));
++ // A front page is a taste and a door (the owner 2026-10-28: "act more like an app, not a huge long web page"):
++ // the hero with two actions and code that runs, the numbers as one strip, the building blocks as a launcher,
++ // the examples and the path, and what is read once (more code, the limits, releases) folded.
++ const limitsFold = limitRows.length ? kit.fold({
++ id: 'limits', title: 'What a project gets', meta: 'free today',
++ body: showcase.limits({
++ lede: 'No plan to buy and no card to add. What a project gets is its limits, and these are the numbers OpenVibe.Events enforces for every project.',
++ columns: ['Sandbox', 'Production'], rows: limitRows, source: eventsSource && eventsSource.public,
++ }).replace(/^<section class="sc-sec">/, '').replace(/<\/section>$/, ''),
++ }) : '';
++ const releasesFold = recent.length ? kit.fold({
++ id: 'releases', title: 'Recent releases', meta: `${recent.length} newest`,
++ body: render(table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((x) => [
++ html`<a href="/apps/${x.app_id}">${x.name}</a>`, x.kind, html`<a href="/releases/${x.id}">${x.version}</a>`, x.status, x.trust.tier, time(x.published_at),
++ ]))),
++ }) : '';
return {
index: true, cache: PUBLIC_CACHE,
jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.FAQ)],
-- styles: [showcase.STYLESHEET],
++ // kit.css draws the numbers strip, the folds and the examples list; nothing on the page needs kit-web.js.
++ styles: [showcase.STYLESHEET, 'kit.css'],
body: html`${raw(showcase.hero({
eyebrow: 'OpenVibe.Services · the developer platform · alpha',
title: 'Build with the whole', accent: 'network',
-- lede: 'One console for everything programmable on OpenVibe: projects, scoped credentials, capability grants, usage and releases, over every service. The reference is generated from the exact contract and SDK versions production runs.',
++ lede: 'One console for projects, apps and keys on every OpenVibe service.',
actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Read the docs', href: '/docs' }],
-- note: 'Open source. Network owns projects, apps, credentials, grants and quotas; this console shows its API with your own sign-in and never stores a secret.',
-- aside: { html: `<pre tabindex="0" class="services-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },
++ note: 'Alpha, open source and free. It never stores a secret.',
++ aside: { html: `<pre tabindex="0" class="services-hero-code"><code>${showcase.esc(home.heroCode({ sdkVersion: docs.sdkVersion }))}</code></pre>` },
}))}
--<div class="notice">Status: alpha. What works and what does not is listed on the <a href="/policy/transparency">transparency page</a>.</div>
${raw(home.numbers({ docs, programmable, status: (ctx.networkStatus || networkStatus).peek() }))}
--${raw(home.blocks())}
--${raw(home.build())}
++${raw(home.blocks({ apis: apiIndex.length }))}
${raw(home.quickStart({ sdkVersion: docs.sdkVersion }))}
--${raw(showcase.features({
-- title: 'Everything an integration needs', lede: 'Each piece is a public API with a reference generated from what production runs.',
-- items: [
-- { icon: 'ov:account', title: 'Scoped credentials', text: 'A confidential app gets a client secret, shown once; try calls in a playground with exactly the app\'s grants.', href: '/projects' },
-- { icon: 'ov:check', title: 'Capability grants', text: 'Ask for exactly what your app does; grants are approved inside the project\'s allowance.', href: '/docs/capabilities?grantable=1' },
-- { icon: 'ov:bell', title: 'Events and webhooks', text: 'Subscribe to the network and verify every delivery\'s signature.', href: '/tools/webhooks' },
-- { icon: 'ov:docs', title: 'Reference and SDK', text: `APIs, contracts, capabilities and events from openvibe-contracts v${docs.contractsVersion}; openvibe-sdk v${docs.sdkVersion} for browser and server.`, href: '/docs' },
-- { icon: 'ov:search', title: 'One resource index', text: 'Every resource of a project across every service, and on Your resources everything you own yourself (robots, rooms, streams, watches, Actor tasks, pastes), behind one API; each service keeps its own data.', href: '/docs/api/services' },
-- { icon: 'ov:download', title: 'Take it with you', text: 'Export a project\'s configuration, objects and events as one archive, any time.', href: '/docs/export' },
-- ],
-- }))}
--${programmable.length ? raw(showcase.features({
-- id: 'services', title: 'Services you can build on', lede: 'Each one runs in production today; its API reference is generated from the capabilities it declares, and the counts are the ones an app can be granted.',
-- // Text only: an inline icon per service would double the page's weight (the home page's budget).
-- items: programmable.map((m) => {
-- const caps = home.publicCaps(m).length;
-- const events = (m.eventsProduced || []).length;
-- const counts = [`${caps} ${caps === 1 ? 'capability' : 'capabilities'}`, events ? `${events} event ${events === 1 ? 'type' : 'types'}` : ''].filter(Boolean).join(' · ');
-- const tagline = (m.site && m.site.tagline) || productTagline.get(m.id) || '';
-- return { title: m.name, text: tagline ? `${/[.!?]$/.test(tagline) ? tagline : `${tagline}.`} ${counts}` : counts, href: `/docs/api/${m.id}` };
-- }),
-- })) : ''}
${raw(showcase.steps({
-- title: 'From account to integration',
++ id: 'how', title: 'From account to integration',
items: [
-- { title: 'Create a project', text: 'Signed in with your OpenVibe account. New projects are sandbox-only; staff enable production.', href: '/projects' },
-- { title: 'Add a sandbox app', text: 'A confidential app gets a client secret, shown once.' },
-- { title: 'Pick capabilities', text: 'Request them for the app; they are approved only inside the project\'s allowance.', href: '/docs/capabilities?grantable=1' },
-- { title: 'Get a token', text: 'createServiceTokenClient() for client credentials, or sign people in with authorization code + PKCE.', href: '/oauth' },
++ { title: 'Create a project', text: 'Sandbox-only at first; staff switch on production.', href: '/projects' },
++ { title: 'Add an app and its key', text: 'Its secret shows once; a playground has its grants.' },
++ { title: 'Ask for capabilities', text: 'Approved inside the project\'s allowance.', href: '/docs/capabilities?grantable=1' },
++ { title: 'Get a token', text: 'Client credentials, or sign people in with PKCE.', href: '/oauth' },
{ title: 'Receive events', text: 'Subscribe, then verify each delivery\'s signature.', href: '/tools/webhooks' },
-- { title: 'Publish a release', text: 'Validated release metadata; rotate or revoke credentials any time.', href: '/manifests/validate' },
++ { title: 'Publish a release', text: 'Validated; rotate or revoke keys any time.', href: '/manifests/validate' },
],
-- }))}
--${raw(showcase.limits({
-- title: 'What a project gets', lede: 'Projects cost nothing today: no plan to buy and no card to add. What a project gets is its limits, and these are the numbers OpenVibe.Events enforces for every project.',
-- columns: ['Sandbox', 'Production'], rows: limitRows, source: eventsSource && eventsSource.public,
-- }))}
--<section class="sc-sec" aria-labelledby="h-ref"><h2 id="h-ref">Reference</h2>
--<p class="sc-lede">Generated from <code>openvibe-contracts v${docs.contractsVersion}</code> and <code>openvibe-sdk v${docs.sdkVersion}</code>: <a href="/docs/api">API explorer</a> · <a href="/docs/contracts">contracts</a> · <a href="/docs/capabilities">capabilities</a> · <a href="/docs/events">events</a> · <a href="/docs/services">services</a> · <a href="/docs/tools">tools</a> · <a href="/docs/sdk">SDK</a> · <a href="/docs/limits">all limits</a>.</p></section>
--${recent.length ? html`<section class="sc-sec" aria-labelledby="h-rel"><h2 id="h-rel">Recent releases</h2>
--${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((x) => [
-- html`<a href="/apps/${x.app_id}">${x.name}</a>`, x.kind, html`<a href="/releases/${x.id}">${x.version}</a>`, x.status, x.trust.tier, time(x.published_at),
-- ]))}</section>` : ''}
++ }).replace(/<\/section>$/, () => `${limitsFold}</section>`))}
++<section class="sc-sec" id="reference" aria-labelledby="h-ref"><h2 id="h-ref">Reference</h2>
++<p class="sc-lede">Generated from <code>openvibe-contracts v${docs.contractsVersion}</code> and <code>openvibe-sdk v${docs.sdkVersion}</code>: <a href="/docs/api">API explorer</a> · <a href="/docs/contracts">contracts</a> · <a href="/docs/capabilities">capabilities</a> · <a href="/docs/events">events</a> · <a href="/docs/services">services</a> · <a href="/docs/tools">tools</a> · <a href="/docs/sdk">SDK</a> · <a href="/docs/limits">all limits</a>.</p>
++<p class="sc-note">One API lists <a href="/docs/api/services">every resource of a project</a> across every service; <a href="/docs/export">export a project</a> as one archive any time. Alpha: what works and what does not is on the <a href="/policy/transparency">transparency page</a>.</p>
++${raw(releasesFold)}</section>
${raw(home.faq())}
--${raw(showcase.network({ service: 'services' }))}
--${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibe account; your first project takes a minute. Coding with an agent? OpenVibe.Codes routes the task to Claude Code, Codex, OpenCode or your own model.', actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Code with any agent', href: 'https://openvibe.codes' }] }))}`,
++${raw(showcase.network({ service: 'services' }))}`,
};
}
ddiff --git a/test/home.test.js b/test/home.test.js
iindex ac8ffa2..045d9b3 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -1,9 +1,10 @@
'use strict';
/**
* The home page's own sections (server/http/home.js): numbers counted from the pinned contracts and SDK (services up
-- * only when Network's status was read), what you can build (each card points at a working example or a reference),
-- * quick-start code that runs against the pinned SDK as written, limits in plain units, and a FAQ that its FAQPage
-- * JSON-LD repeats word for word.
++ * only when Network's status was read) as one strip, the building blocks as a launcher with every service's API behind
++ * See all, the working examples, hero code that runs against the pinned SDK as written, limits in plain units, and a
++ * FAQ that its FAQPage JSON-LD repeats word for word. A front page is a taste and a door: two hero actions, and what is
++ * read once (more code, the limits) folded.
*/
const assert = require('assert');
const sdk = require('openvibe-sdk');
@@@ -21,11 +22,11 @@ const { boot, check, done } = require('./helpers/boot');
assert.strictEqual(home.limitAmount(null, 'count'), 'unlimited');
});
-- await check('the quick-start code runs as written against the pinned SDK (a stub platform answers)', async () => {
++ await check('the hero and quick-start code run as written against the pinned SDK (a stub platform answers)', async () => {
const html = home.quickStart({ sdkVersion: '0.60.0' });
const unesc = (s) => s.replace(/&#39;/g, "'").replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
-- const samples = [...html.matchAll(/<code class="language-js">([\s\S]*?)<\/code>/g)].map((m) => unesc(m[1]));
-- assert.ok(samples.length >= 2, 'two Node samples');
++ const samples = [home.heroCode({ sdkVersion: '0.60.0' }), ...[...html.matchAll(/<code class="language-js">([\s\S]*?)<\/code>/g)].map((m) => unesc(m[1]))];
++ assert.ok(samples.length >= 2, 'the hero\'s sample and the app\'s');
const seen = [];
const fetchStub = async (url) => {
const u = new URL(String(url));
@@@ -39,7 +40,7 @@ const { boot, check, done } = require('./helpers/boot');
const realFetch = globalThis.fetch;
globalThis.fetch = fetchStub;
try {
-- // The first sample is the one that needs no key: run it whole.
++ // The hero's sample is the one that needs no key: run it whole.
const run = new Function('require', `return (async () => { ${samples[0]}\n return { results, services }; })();`);
const out = await run((id) => require(id));
assert.deepStrictEqual(out.results.map((r) => r.title), ['Patch notes']);
@@@ -56,33 +57,49 @@ const { boot, check, done } = require('./helpers/boot');
const t = await boot();
try {
-- await check('the home page: hero code that runs, numbers, the building blocks, what you can build, quick start, FAQ and its JSON-LD', async () => {
++ await check('the home page: hero code that runs, the numbers strip, the building blocks launcher, examples, folded code, FAQ and its JSON-LD', async () => {
let r = await t.get('/');
assert.strictEqual(r.status, 200);
assert.ok(!/ov\.registry\./.test(r.text), 'the hero code no longer calls a client property the SDK does not have');
assert.match(r.text, /sdk\.registry\.createRegistryClient\(ov\)/);
-- assert.match(r.text, /<b>\d+<\/b><span>capabilities an app can request<\/span>/);
-- assert.match(r.text, /<b>\d+<\/b><span>event types to subscribe to<\/span>/);
++ assert.strictEqual((/<section class="sc-hero[\s\S]*?<\/section>/.exec(r.text)[0].match(/class="sc-btn/g) || []).length, 2, 'two hero actions');
++ // The numbers are one kit.stats strip, each opening the reference it counts.
++ assert.match(r.text, /<dl class="ovk-stats" aria-label="The platform in numbers">/);
++ assert.match(r.text, /<dt><a href="\/docs\/capabilities\?grantable=1">capabilities an app can request<\/a><\/dt><dd>\d+<\/dd>/);
++ assert.match(r.text, /<dt><a href="\/docs\/events">event types to subscribe to<\/a><\/dt><dd>\d+<\/dd>/);
assert.ok(!/services up now/.test(r.text), 'no services-up number while Network\'s status was not read');
for (const name of ['oauth-app', 'media-uploader', 'webhook-consumer', 'tool-job', 'chat-bot']) {
assert.ok(r.text.includes(`href="https://github.com/OpenVibers/OpenVibe.Examples/tree/main/examples/${name}"`), `links the ${name} example`);
}
assert.match(r.text, /id="quick-start"/);
-- // The building blocks by their plain names, each linked; people who only want to use OpenVibe go to Vibe.
-- assert.match(r.text, /id="blocks"/);
-- for (const b of home.BLOCKS) assert.ok(r.text.includes(b.title.replace(/'/g, '&#39;')) && r.text.includes(`href="${b.href}"`), `the ${b.title} block`);
-- assert.ok(r.text.includes('<a href="https://openvibe.chat/vibe">Chat with Vibe</a>'), 'people are sent to Vibe');
-- assert.match(r.text, /\d+ capabilit(y|ies)( · \d+ event types?)?<\/small>/, 'each service says what an app can be granted');
++ // Read once, so folded: a new app from a template, and an app's calls past the hero's code.
++ assert.match(r.text, /<details class="ovk-fold" id="qs-template">[\s\S]*?create-openvibe-app[\s\S]*?<\/details>/);
++ assert.match(r.text, /<details class="ovk-fold" id="qs-app">[\s\S]*?createServiceTokenClient[\s\S]*?oauth\/token[\s\S]*?<\/details>/);
++ // The building blocks as a launcher (showcase.apps), each linked; every service's API behind See all; people
++ // who only want to use OpenVibe go to Vibe.
++ const launcher = (/<section class="sc-sec sc-apps-sec" id="blocks"[\s\S]*?<\/section>/.exec(r.text) || [''])[0];
++ assert.ok(launcher.includes('<ul class="sc-apps">'), 'the building blocks are a launcher');
++ for (const b of home.BLOCKS) assert.ok(launcher.includes(`<a href="${b.href}">`) && launcher.includes(`<b>${b.name}</b>`), `the ${b.name} block`);
++ const apis = require('openvibe-contracts').openapi.index().length;
++ assert.ok(launcher.includes(`<p class="sc-apps-more"><a href="/docs/api">See all ${apis} service APIs →</a></p>`), 'See all opens every service\'s API reference');
++ assert.ok(launcher.includes('<a href="https://openvibe.chat/vibe">Chat with Vibe</a>'), 'people are sent to Vibe');
assert.ok(!/per_minute|\d bytes</.test(r.text), 'limits read in plain units');
++ assert.ok(/\/shared\/kit\.css\?v=/.test(r.text) && !/\/shared\/kit-web\.js/.test(r.text) && !/kit-data/.test(r.text), 'kit.css for the strip, folds and list; no kit-web.js or Kit 2');
++ assert.ok(!/class="svc-faq"|<h3 class="home-/.test(r.text), 'no hand-built FAQ or section markup is left');
const blocks = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
const faq = blocks.find((b) => b['@type'] === 'FAQPage');
assert.ok(faq && faq.mainEntity.length === home.FAQ.length, 'a FAQPage node with every question');
-- faq.mainEntity.forEach((q) => assert.ok(r.text.includes(`<h3>${q.name.replace(/'/g, '&#39;')}</h3>`), `the page shows "${q.name}"`));
++ faq.mainEntity.forEach((q) => assert.ok(r.text.includes(`<summary>${q.name.replace(/'/g, '&#39;')}</summary>`), `the page shows "${q.name}"`));
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
++ assert.strictEqual((r.text.match(/<section[\s>]/g) || []).length, (r.text.match(/<\/section>/g) || []).length, 'every section is closed once');
t.ctx.networkStatus = { peek: () => ({ up: 37, checked: 38 }) };
++ // What a project gets, from the service that enforces it, folded with its source.
++ t.ctx.limits = { bySource: (s) => (s === 'events' ? { service: 'events', public: 'https://openvibe.events/limits.json' } : null), read: async () => ({ body: { limits: [{ label: 'Events published per minute', sandbox: 30, production: 120, unit: 'per_minute' }] } }) };
r = await t.get('/');
-- assert.match(r.text, /<b>37<\/b><span>services up now, of 38 checked<\/span>/, 'services up once Network answered');
++ assert.match(r.text, /<dt><a href="https:\/\/openvibe\.network\/status">services up now, of 38 checked<\/a><\/dt><dd>37<\/dd>/, 'services up once Network answered');
++ assert.match(r.text, /<details class="ovk-fold" id="limits">[\s\S]*?<td>30 a minute<\/td><td>120 a minute<\/td>[\s\S]*?openvibe\.events\/limits\.json[\s\S]*?<\/details>/, 'the limits, folded, in plain units with their source');
++ assert.strictEqual((r.text.match(/<section[\s>]/g) || []).length, (r.text.match(/<\/section>/g) || []).length, 'the folded limits leave no section open');
});
} finally { await t.close(); }
done();
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 91bf66b..774213d 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
-- htmlRawKB: 55, // 50.7 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 46.0 with the twelve text-only building blocks (plan track K, 2026-10-24); 42.9 with the home's own sections (2026-10-22: the platform in numbers, nine text-only "what you can build" cards, three quick-start samples, six FAQs and their FAQPage JSON-LD); 33.7 with openvibe-contracts 0.209 (2026-10-20: Blog, Reviews, Wiki, Coupons, Deals, Trade, Space, Host and News opened capabilities to apps, so the text-only catalog of services you can build on lists them); 30.8 on 2026-10-08 (six ring icons plus that catalog)
++ htmlRawKB: 60, // 57.4 with the home as a taste and a door (2026-10-29: the building blocks as a launcher with 15 inline ring icons; the text-only service catalog, what-you-can-build and integration cards it replaced measured 54.5 on Shared 4.38.0); 50.7 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 46.0 with the twelve text-only building blocks (plan track K, 2026-10-24); 42.9 with the home's own sections (2026-10-22: the platform in numbers, nine text-only "what you can build" cards, three quick-start samples, six FAQs and their FAQPage JSON-LD); 33.7 with openvibe-contracts 0.209 (2026-10-20: Blog, Reviews, Wiki, Coupons, Deals, Trade, Space, Host and News opened capabilities to apps, so the text-only catalog of services you can build on lists them); 30.8 on 2026-10-08 (six ring icons plus that catalog)
htmlBrotliKB: 13, // 11.5 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 10.0 with those sections (2026-10-22); 7.4
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime, 2026-10-04)
jsRawKB: 305, // 300.5 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 263.4 with shared 4.11.0's seasonal navbar; 254.1 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 76, // 74.2 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 61.7 with shared 4.11.0's seasonal navbar; 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)
-- cssFiles: 2, // 1
-- cssRawKB: 36, // 31.8 openvibe-shared 4.26.1 (hero visual); 26.3 with Shared 4.13.0 (2026-10-26: account links); 20.3 with the numbers and FAQ styles (2026-10-22); 17.3 (services.css + the cached /shared/showcase.css)
-- cssBrotliKB: 7, // 5.8 with Shared 4.13.0 (2026-10-26: account links); 4.1
++ cssFiles: 3, // 3 with kit.css (2026-10-29: the home's numbers strip, folds and examples list); 2; 1
++ cssRawKB: 61, // 58.2 with kit.css (25.6 KB raw; 34.0 before on Shared 4.38.0): the numbers as one kit.stats strip, what is read once in kit.fold and the examples as kit rows, which took the phone home from 16.1 screens to 5.8; 31.8 openvibe-shared 4.26.1 (hero visual); 26.3 with Shared 4.13.0 (2026-10-26: account links); 20.3 with the numbers and FAQ styles (2026-10-22); 17.3 (services.css + the cached /shared/showcase.css)
++ cssBrotliKB: 13, // 12.1 with kit.css (2026-10-29; 7.3 before); 5.8 with Shared 4.13.0 (2026-10-26: account links); 4.1
externalFiles: 2, // 0
};