openvibers/openvibe.services Commits
Home: a taste and a door, 16.1 phone screens to 5.8 (openvibe-shared v4.38.0) (#146)
Changes
@@@ -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 -->@@@ -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",@@@ -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",@@@ -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": {@@@ -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; }@@@ -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 keyconst 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 };@@@ -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' }))}`,};}@@@ -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(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/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, ''')) && 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, ''')}</h3>`), `the page shows "${q.name}"`));++ faq.mainEntity.forEach((q) => assert.ok(r.text.includes(`<summary>${q.name.replace(/'/g, ''')}</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();@@@ -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.4jsFiles: 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.1externalFiles: 2, // 0};
@@ -287,5 +287,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -287,5 +287,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.67.0
- openvibe-sdk: v0.67.0
-- openvibe-shared: v4.37.0
+- openvibe-shared: v4.38.0
<!-- versions:end -->
<!-- versions:end -->
@@ -9,11 +9,11 @@
@@ -9,11 +9,11 @@
"domain": "openvibe.services",
"domain": "openvibe.services",
"contracts": "openvibe-contracts v0.222.0",
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.67.0",
"sdk": "openvibe-sdk v0.67.0",
- "shared": "openvibe-shared v4.37.0",
+ "shared": "openvibe-shared v4.38.0",
"packages": [
"packages": [
"openvibe-contracts v0.222.0",
"openvibe-contracts v0.222.0",
"openvibe-sdk v0.67.0",
"openvibe-sdk v0.67.0",
- "openvibe-shared v4.37.0"
+ "openvibe-shared v4.38.0"
],
],
"capabilities": [
"capabilities": [
"services.resource.read",
"services.resource.read",
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.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-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"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1209,9 +1209,9 @@
@@ -1209,9 +1209,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.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-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"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -77,19 +77,16 @@ meter.headroom { width: 8rem; max-width: 100%; vertical-align: middle; }
@@ -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. */
/* 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-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; }
.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; }
@@ -1,16 +1,18 @@
@@ -1,16 +1,18 @@
'use strict';
'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 showcase = require('openvibe-shared/showcase');
+const kit = require('openvibe-shared/kit');
const esc = showcase.esc;
const esc = showcase.esc;
const EXAMPLES = 'https://github.com/OpenVibers/OpenVibe.Examples';
const EXAMPLES = 'https://github.com/OpenVibers/OpenVibe.Examples';
const example = (name) => `${EXAMPLES}/tree/main/examples/${name}`;
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
// ── 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
// 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
@@ -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 }) {
function numbers({ docs, programmable, status }) {
const items = [
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.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.events.length, 'event types to subscribe to', '/docs/events'],
[docs.sdk.length, 'SDK modules, typed', '/docs/sdk'],
[docs.sdk.length, 'SDK modules, typed', '/docs/sdk'],
status ? [status.up, `services up now, of ${status.checked} checked`, 'https://openvibe.network/status'] : null,
status ? [status.up, `services up now, of ${status.checked} checked`, 'https://openvibe.network/status'] : null,
].filter((x) => x && Number(x[0]) > 0);
].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
// 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 = [
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',
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.',
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 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 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 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({
const tokens = sdk.auth.createServiceTokenClient({
clientId: process.env.OV_CLIENT_ID, clientSecret: process.env.OV_CLIENT_SECRET,
clientId: process.env.OV_CLIENT_ID, clientSecret: process.env.OV_CLIENT_SECRET,
});
});
const ov = sdk.createClient({ tokenProvider: tokens });
const ov = sdk.createClient({ tokenProvider: tokens });
const { unverifiedClaims } = await tokens.getTokenInfo({ audience: 'openvibe.media' });
const { unverifiedClaims } = await tokens.getTokenInfo({ audience: 'openvibe.media' });
const media = sdk.media.createMediaClient(ov, { app: unverifiedClaims.project_id });
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 grant_type=client_credentials \\
-d client_id="$OV_CLIENT_ID" -d client_secret="$OV_CLIENT_SECRET" \\
-d client_id="$OV_CLIENT_ID" -d client_secret="$OV_CLIENT_SECRET" \\
-d audience=openvibe.media
-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.
// The questions developers ask, answered from what the platform does today. The FAQPage JSON-LD repeats them.
@@ -149,8 +153,9 @@ const FAQ = [
@@ -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.' },
{ 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() {
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. */
/** "120 per_minute" → "120 a minute", bytes → KB/MB, a bare count → the number. */
@@ -170,4 +175,4 @@ function limitAmount(v, unit) {
@@ -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 };
@@ -15,13 +15,14 @@ const contracts = require('openvibe-contracts');
@@ -15,13 +15,14 @@ const contracts = require('openvibe-contracts');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
+const kit = require('openvibe-shared/kit');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const path = require('path');
const path = require('path');
const express = require('express');
const express = require('express');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
const { createDiscoveryRoutes, homeJsonLd, COMMUNITY, CODES_ORIGIN } = require('./discovery');
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 { send } = require('../render/layout');
const { markdown } = require('../render/markdown');
const { markdown } = require('../render/markdown');
const { csrfToken, checkCsrf, sameOrigin } = require('../auth/forms');
const { csrfToken, checkCsrf, sameOrigin } = require('../auth/forms');
@@ -49,89 +50,63 @@ function createPageViews(ctx) {
@@ -49,89 +50,63 @@ function createPageViews(ctx) {
const eventsSource = ctx.limits && ctx.limits.bySource ? ctx.limits.bySource('events') : null;
const eventsSource = ctx.limits && ctx.limits.bySource ? ctx.limits.bySource('events') : null;
const eventsLimits = eventsSource ? await ctx.limits.read(eventsSource) : 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)] })) : [];
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
// 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 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
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 {
return {
index: true, cache: PUBLIC_CACHE,
index: true, cache: PUBLIC_CACHE,
jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.FAQ)],
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({
body: html`${raw(showcase.hero({
eyebrow: 'OpenVibe.Services · the developer platform · alpha',
eyebrow: 'OpenVibe.Services · the developer platform · alpha',
title: 'Build with the whole', accent: 'network',
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' }],
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.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(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({
${raw(showcase.steps({
- title: 'From account to integration',
+ id: 'how', title: 'From account to integration',
items: [
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: '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(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' }))}`,
};
};
}
}
@@ -1,9 +1,10 @@
@@ -1,9 +1,10 @@
'use strict';
'use strict';
/**
/**
* The home page's own sections (server/http/home.js): numbers counted from the pinned contracts and SDK (services up
* 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 assert = require('assert');
const sdk = require('openvibe-sdk');
const sdk = require('openvibe-sdk');
@@ -21,11 +22,11 @@ const { boot, check, done } = require('./helpers/boot');
@@ -21,11 +22,11 @@ const { boot, check, done } = require('./helpers/boot');
assert.strictEqual(home.limitAmount(null, 'count'), 'unlimited');
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 html = home.quickStart({ sdkVersion: '0.60.0' });
const unesc = (s) => s.replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
const unesc = (s) => s.replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/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 seen = [];
const fetchStub = async (url) => {
const fetchStub = async (url) => {
const u = new URL(String(url));
const u = new URL(String(url));
@@ -39,7 +40,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -39,7 +40,7 @@ const { boot, check, done } = require('./helpers/boot');
const realFetch = globalThis.fetch;
const realFetch = globalThis.fetch;
globalThis.fetch = fetchStub;
globalThis.fetch = fetchStub;
try {
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 run = new Function('require', `return (async () => { ${samples[0]}\n return { results, services }; })();`);
const out = await run((id) => require(id));
const out = await run((id) => require(id));
assert.deepStrictEqual(out.results.map((r) => r.title), ['Patch notes']);
assert.deepStrictEqual(out.results.map((r) => r.title), ['Patch notes']);
@@ -56,33 +57,49 @@ const { boot, check, done } = require('./helpers/boot');
@@ -56,33 +57,49 @@ const { boot, check, done } = require('./helpers/boot');
const t = await boot();
const t = await boot();
try {
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('/');
let r = await t.get('/');
assert.strictEqual(r.status, 200);
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.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, /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');
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']) {
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.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"/);
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, ''')) && 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(!/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 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');
const faq = blocks.find((b) => b['@type'] === 'FAQPage');
assert.ok(faq && faq.mainEntity.length === home.FAQ.length, 'a FAQPage node with every question');
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, ''')}</h3>`), `the page shows "${q.name}"`));
+ faq.mainEntity.forEach((q) => assert.ok(r.text.includes(`<summary>${q.name.replace(/'/g, ''')}</summary>`), `the page shows "${q.name}"`));
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
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 }) };
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('/');
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(); }
} finally { await t.close(); }
done();
done();
@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
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
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)
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)
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)
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
externalFiles: 2, // 0
};
};