openvibers/openvibe.host Commits

Host: the signed-out front page is a showcase home with the enforced limits table. (#25)

OP OpenVibers committed 7 Oct 2026 · 49fcd4657a · 9 files, +71 −31

The signed-out front page of openvibe.host (its public, indexable page) is now a product home built with the shared showcase kit (openvibe-shared v2.10.1), in place of a heading and two paragraphs.

- server/render/pages.js: signedOut({ limits, limitsUrl }) renders a hero ("Publish a static site. Roll back in one step.", with the alpha note: static files only, nothing uploaded is executed, hosted bots/mods/apps not available; actions Sign in with OpenVibe / See the limits), a "What you get" grid (browser or /api/v1 upload, archives checked in memory and never extracted; immutable deploys with rollback; TXT-verified custom domains re-checked daily; projects with members, sandbox out of search), "Get a site online" steps, and a Limits table. The table is limitsOf(config), the very object /limits.json serves, so the page shows what Host enforces and links to it. The internal "Stage C" plan code is gone from public copy.
- server/http/dashboard.js: / passes the limits and asks for showcase.css; page() forwards styles.
- server/render/layout.js: renderPage links openvibe-shared stylesheets a page names.
- test/host-secrets-dashboard.test.js: the signed-out page check now asserts the alpha note, the limits table and the showcase stylesheet.
- test/perf-budget.test.js: budgets raised for the showcase (html 15.9 -> 23.8 KB raw; css 3.1 -> 14.8 KB raw for showcase.css).
- package.json / lockfile, README, STATUS.json: openvibe-shared v2.6.0 -> v2.10.1.

Tests: ov test test/host-secrets-dashboard.test.js test/render-layout.test.js test/perf-budget.test.js test/host-isolation.test.js: 4 passed.

Changes

ddiff --git a/README.md b/README.md
iindex ed484c2..d654c0d 100644
---- a/README.md
++++ b/README.md
@@@ -52,7 +52,7 @@ Stage A adds a few safety rules of its own:
- OpenVibe.Contracts (`openvibe-contracts` v0.83.0; `host.*` capabilities and the `host` manifest were released in v0.24.0, and v0.32.0 adds the takedown routes to `host.site.manage`): service manifests (vhost rendering, snapshots, the first-party domain list), ids, problem+json, service-token verification, capability checks.
- OpenVibe.Network (Stage B): SSO for the dashboard, the JWKS that verifies user and service tokens, client-credentials tokens for the outbox relay.
- OpenVibe.Events (Stage B): `host.*` events through the `openvibe-sdk` v0.26.0 transactional outbox (openvibe-sdk/limits for the per-actor limits).
--- `openvibe-shared` v2.6.0 (Stage B): shared chrome (Host's own pages are composed with `openvibe-shared/shell`), legal pages, `/release.json`, `/metrics`, `/api/ready`.
++- `openvibe-shared` v2.10.1 (Stage B): shared chrome (Host's own pages are composed with `openvibe-shared/shell`), legal pages, `/release.json`, `/metrics`, `/api/ready`.
- OpenVibe.Media (Stage B, opt-in): with `HOST_OBJECT_STORE=media`, deploy objects are written through to Media's Object API v2 (`openvibe-sdk/media`) and local disk becomes the read cache; unset keeps them on local disk (see [Storage](#storage)).
## Capabilities
@@@ -407,7 +407,7 @@ These tenant events have subject `deploy` and visibility `internal`; `host.deplo
### Dashboard
--Server-rendered pages with the shared chrome (`openvibe-shared` v2.6.0 `shell.page`: navbar and theme loader from the Network, `<noscript>` navigation, SSR footer, app icon, legal pages). Every action is a plain form, so it works without JavaScript: projects, quotas and usage, members, sites, folder or archive upload, deploys with activate/rollback/delete, activation history, upload logs and file lists, domains with their DNS records and a "check DNS now" button. Pages are `private, no-store`, `noindex`, `frame-ancestors 'none'`.
++Server-rendered pages with the shared chrome (`openvibe-shared` v2.10.1 `shell.page`: navbar and theme loader from the Network, `<noscript>` navigation, SSR footer, app icon, legal pages). Every action is a plain form, so it works without JavaScript: projects, quotas and usage, members, sites, folder or archive upload, deploys with activate/rollback/delete, activation history, upload logs and file lists, domains with their DNS records and a "check DNS now" button. Pages are `private, no-store`, `noindex`, `frame-ancestors 'none'`.
### Observability
@@@ -531,5 +531,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.83.0
- openvibe-sdk: v0.26.0
--- openvibe-shared: v2.6.0
++- openvibe-shared: v2.10.1
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex aa5e10f..9d5fad8 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -16,11 +16,11 @@
"tenantDomain": "*.openvibe.host",
"contracts": "openvibe-contracts v0.83.0",
"sdk": "openvibe-sdk v0.26.0",
-- "shared": "openvibe-shared v2.6.0",
++ "shared": "openvibe-shared v2.10.1",
"packages": [
"openvibe-contracts v0.83.0",
"openvibe-sdk v0.26.0",
-- "openvibe-shared v2.6.0"
++ "openvibe-shared v2.10.1"
],
"capabilities": [
"host.site.manage",
@@@ -63,5 +63,5 @@
"Stage B git deploys, Phase 1: a site's Git source (repo + branch); the tenant's own CI posts each build, which lands as a source=git preview with immutable commit provenance and goes live only through activate (Host never clones, fetches, holds a credential or builds)",
"per-actor limits on Stage B writes (openvibe-sdk/limits)"
],
-- "updated": "2026-10-05"
++ "updated": "2026-10-07"
}
ddiff --git a/package-lock.json b/package-lock.json
iindex a67654b..95e4da4 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -19,7 +19,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.83.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0"
},
"bin": {
@@@ -1546,9 +1546,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.6.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
-- "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
++ "version": "2.10.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
++ "integrity": "sha512-6fUOw22bSvxg0K33BefAyAkNRkETe9euLNT0CHs2mLIvzYR1q38KiHRUQIWj8FESbVANF0pXMojhBz5TeqpMtg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 6712003..79ed133 100644
---- a/package.json
++++ b/package.json
@@@ -35,7 +35,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.83.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.10.1",
"pg": "^8.23.0"
},
"engines": {
ddiff --git a/server/http/dashboard.js b/server/http/dashboard.js
iindex 0fcd988..0f6196f 100644
---- a/server/http/dashboard.js
++++ b/server/http/dashboard.js
@@@ -10,6 +10,8 @@ const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const { renderPage } = require('../render/layout');
const pages = require('../render/pages');
++const showcase = require('openvibe-shared/showcase');
++const { limitsOf } = require('../limits');
const { csrfToken, checkCsrf, sameOrigin } = require('../auth/forms');
const { readUpload, UploadError } = require('./upload');
const { ApiError, privateNoStore } = require('./errors');
@@@ -28,8 +30,8 @@ function createDashboard(ctx) {
if (ctx.actorLimits) router.use(ctx.actorLimits);
router.use((req, res, next) => { privateNoStore(res); next(); });
-- function page(req, res, status, title, body, notice, { indexable = false } = {}) {
-- res.status(status).type('html').send(renderPage({ title, body, viewer: req.viewer, config, path: req.originalUrl, notice, indexable }));
++ function page(req, res, status, title, body, notice, { indexable = false, styles } = {}) {
++ res.status(status).type('html').send(renderPage({ title, body, viewer: req.viewer, config, path: req.originalUrl, notice, indexable, styles }));
}
function fail(req, res, err) {
@@@ -69,7 +71,10 @@ function createDashboard(ctx) {
router.get('/', view(async (req, res) => {
// The signed-out front page is the public page of openvibe.host (sitemap.xml): indexable.
// Everything behind sign-in stays noindex and private.
-- if (req.viewer.kind !== 'user') return page(req, res, 200, null, pages.signedOut(), null, { indexable: !req.query || !Object.keys(req.query).length });
++ if (req.viewer.kind !== 'user') {
++ return page(req, res, 200, null, pages.signedOut({ limits: limitsOf(config), limitsUrl: `${config.baseUrl}/limits.json` }), null,
++ { indexable: !req.query || !Object.keys(req.query).length, styles: [showcase.STYLESHEET] });
++ }
page(req, res, 200, 'Projects', pages.home({ projects: await projects.listFor(req.viewer), csrf: csrfToken(config, req.viewer) }), notice(req));
}));
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 041d7f5..0bf3f00 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -75,6 +75,8 @@ function renderPage(o) {
head: [
appIcon.headTags({ site: 'host' }),
`<link rel="stylesheet" href="${asset('css/host.css')}">`,
++ // openvibe-shared stylesheets a page asks for by name (the front page's showcase.css)
++ ...(o.styles || []).map((name) => `<link rel="stylesheet" href="${esc(ovServe.url(name))}">`),
`<meta name="ov-boost" content="host@${esc(RELEASE)}">`,
`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
].join('\n'),
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 73aa2e9..023b371 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -3,6 +3,7 @@
* Dashboard page bodies. Every value is escaped; every action is a POST form carrying the form
* token, so the dashboard works without JavaScript.
*/
++const showcase = require('openvibe-shared/showcase');
const { esc } = require('./layout');
const bytes = (n) => (n >= 1073741824 ? `${(n / 1073741824).toFixed(2)} GiB` : n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`);
@@@ -15,16 +16,44 @@ function form(action, csrf, inner, { cls = '', confirm = null, enctype = null }
+ `${confirm ? `<label class="confirm"><input type="checkbox" name="confirm" value="yes" required> ${esc(confirm)}</label>` : ''}</form>`;
}
--function signedOut() {
-- return `<section class="intro">
--<h1>OpenVibe.Host</h1>
--<p>Static site hosting for OpenVibe projects. Upload a folder or a <code>.tar.gz</code> of HTML, CSS, JavaScript, images and fonts;
--Host keeps every deploy as an immutable, content-addressed artifact, serves the active one at
--<code>&lt;site&gt;.openvibe.host</code> (and on custom domains you verify), and lets you roll back to any earlier deploy in one step.</p>
--<p><strong>Alpha.</strong> Static files only: there is no build step and no server-side code, and nothing you upload is ever executed.
--Hosted bots, mods and apps (Stage C) are not available.</p>
--<p><a class="button" href="/auth/login?next=%2F">Sign in with OpenVibe</a></p>
--</section>`;
++/** A limit as the front page prints it: bytes in binary units, 0 as "none" (the environment allows none). */
++const limitValue = (n, unit) => (n === 0 ? 'none' : unit === 'bytes' ? bytes(n) : Number(n).toLocaleString('en-US'));
++
++/**
++ * The signed-out front page: the public page of openvibe.host, built with openvibe-shared/showcase. The limits table
++ * is limitsOf(config), the same object /limits.json serves, so the page never promises more than Host enforces.
++ */
++function signedOut({ limits, limitsUrl } = {}) {
++ return showcase.hero({
++ eyebrow: 'OpenVibe.Host · alpha',
++ title: 'Publish a static site.', accent: 'Roll back in one step.',
++ lede: 'Upload a folder or a .tar.gz of HTML, CSS, JavaScript, images and fonts. Every deploy is kept as an immutable, content-addressed artifact; the active one is served at <site>.openvibe.host and on custom domains you verify.',
++ actions: [{ label: 'Sign in with OpenVibe', href: '/auth/login?next=%2F', primary: true }, { label: 'See the limits', href: '#limits' }],
++ note: 'Alpha. Static files only: there is no build step and no server-side code, and nothing you upload is ever executed. Hosted bots, mods and apps are not available.',
++ }) + showcase.features({
++ title: 'What you get',
++ items: [
++ { icon: 'ov:upload', title: 'Upload from the browser or the API', text: 'A folder or an archive from the dashboard, or POST it to /api/v1. Archives are checked in memory and never extracted.' },
++ { icon: 'ov:history', title: 'Every deploy kept', text: 'Deploys never change once made. Activate an earlier one to roll back, from the dashboard or the API.' },
++ { icon: 'ov:dns', title: 'Your own domain', text: 'Add a domain and prove it with a DNS TXT record. It is served once verified and re-checked every day.' },
++ { icon: 'ov:account', title: 'Projects for a team', text: 'Owners add maintainers and members per project. Sandbox projects have smaller quotas and stay out of search.' },
++ ],
++ }) + showcase.steps({
++ title: 'Get a site online',
++ items: [
++ { title: 'Sign in', text: 'With your OpenVibe account.' },
++ { title: 'Create a project', text: 'Production, or a sandbox to try things out.' },
++ { title: 'Add a site', text: 'It gets its own name under openvibe.host.' },
++ { title: 'Upload a deploy', text: 'Activate it, and roll back to any earlier deploy whenever you need to.' },
++ ],
++ }) + (limits ? showcase.limits({
++ id: 'limits',
++ title: 'Limits',
++ lede: 'The defaults a new project starts with, by environment (the first row counts the projects one person owns).',
++ columns: ['Production', 'Sandbox'],
++ rows: limits.limits.map((l) => ({ label: l.label, values: [limitValue(l.production, l.unit), limitValue(l.sandbox, l.unit)] })),
++ source: limitsUrl,
++ }) : '');
}
function home({ projects, csrf }) {
ddiff --git a/test/host-secrets-dashboard.test.js b/test/host-secrets-dashboard.test.js
iindex a8a41d4..3a99714 100644
---- a/test/host-secrets-dashboard.test.js
++++ b/test/host-secrets-dashboard.test.js
@@@ -54,7 +54,11 @@ const ORIGIN = 'https://openvibe.host';
assert.match(r.text, /<noscript><nav aria-label="Site"/);
assert.match(r.text, /id="ov-footer"/);
assert.match(r.text, /Sign in with OpenVibe/);
-- assert.match(r.text, /Stage C\) are not available/);
++ assert.match(r.text, /nothing you upload is ever executed/);
++ // The limits table is the one /limits.json serves (limitsOf(config)), never a second copy.
++ assert.match(r.text, /id="limits"/);
++ assert.match(r.text, /Files in one deploy/);
++ assert.match(r.text, /href="\/shared\/showcase\.css\?v=[0-9a-f]{12}"/);
assert.match(r.headers['content-security-policy'], /frame-ancestors 'none'/);
// Release notifications: release-watch's EventSource on the Events realtime stream (openvibe-shared 1.17).
assert.match(r.headers['content-security-policy'], /connect-src 'self' https:\/\/openvibe\.network https:\/\/events\.openvibe\.network;/);
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 5569c6a..487bc76 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
-- htmlRawKB: 18, // measured 15.9 (fresh database)
-- htmlBrotliKB: 5, // 4.3
-- jsFiles: 5, // 4
++ htmlRawKB: 26, // measured 23.8 (fresh database; 15.9 before the showcase front page, 2026-10-07)
++ htmlBrotliKB: 7, // 6.3 (4.3 before the showcase)
++ jsFiles: 5, // 5 (openvibe-shared 2.10's shell adds web-runtime.js)
jsRawKB: 250, // 239.1 (openvibe-shared 2.6.0 served scripts; was 212.2 on 2.2.0)
jsBrotliKB: 60, // 56.3 (was 49.9)
-- cssFiles: 2, // 1
-- cssRawKB: 5, // 3.1
-- cssBrotliKB: 2, // 0.9
++ cssFiles: 2, // 2 (host.css, openvibe-shared showcase.css)
++ cssRawKB: 16, // 14.8 (3.1 before showcase.css)
++ cssBrotliKB: 4.2, // 3.6 (0.9 before showcase.css)
externalFiles: 1, // 0
};