openvibers/openvibe.host Commits
Host: the signed-out front page is a showcase home with the enforced limits table. (#25)
Changes
@@@ -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 -->@@@ -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"}@@@ -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"@@@ -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": {@@@ -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));}));@@@ -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'),@@@ -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><site>.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 }) {@@@ -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;/);@@@ -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};
@@ -52,7 +52,7 @@ Stage A adds a few safety rules of its own:
@@ -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.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.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.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)).
- 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
## Capabilities
@@ -407,7 +407,7 @@ These tenant events have subject `deploy` and visibility `internal`; `host.deplo
@@ -407,7 +407,7 @@ These tenant events have subject `deploy` and visibility `internal`; `host.deplo
### Dashboard
### 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
### Observability
@@ -531,5 +531,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -531,5 +531,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.83.0
- openvibe-contracts: v0.83.0
- openvibe-sdk: v0.26.0
- openvibe-sdk: v0.26.0
-- openvibe-shared: v2.6.0
+- openvibe-shared: v2.10.1
<!-- versions:end -->
<!-- versions:end -->
@@ -16,11 +16,11 @@
@@ -16,11 +16,11 @@
"tenantDomain": "*.openvibe.host",
"tenantDomain": "*.openvibe.host",
"contracts": "openvibe-contracts v0.83.0",
"contracts": "openvibe-contracts v0.83.0",
"sdk": "openvibe-sdk v0.26.0",
"sdk": "openvibe-sdk v0.26.0",
- "shared": "openvibe-shared v2.6.0",
+ "shared": "openvibe-shared v2.10.1",
"packages": [
"packages": [
"openvibe-contracts v0.83.0",
"openvibe-contracts v0.83.0",
"openvibe-sdk v0.26.0",
"openvibe-sdk v0.26.0",
- "openvibe-shared v2.6.0"
+ "openvibe-shared v2.10.1"
],
],
"capabilities": [
"capabilities": [
"host.site.manage",
"host.site.manage",
@@ -63,5 +63,5 @@
@@ -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)",
"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)"
"per-actor limits on Stage B writes (openvibe-sdk/limits)"
],
],
- "updated": "2026-10-05"
+ "updated": "2026-10-07"
}
}
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.83.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-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"
"pg": "^8.23.0"
},
},
"bin": {
"bin": {
@@ -1546,9 +1546,9 @@
@@ -1546,9 +1546,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -35,7 +35,7 @@
@@ -35,7 +35,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.83.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-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"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -10,6 +10,8 @@ const ovServe = require('openvibe-shared/serve');
@@ -10,6 +10,8 @@ const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const { renderPage } = require('../render/layout');
const { renderPage } = require('../render/layout');
const pages = require('../render/pages');
const pages = require('../render/pages');
+const showcase = require('openvibe-shared/showcase');
+const { limitsOf } = require('../limits');
const { csrfToken, checkCsrf, sameOrigin } = require('../auth/forms');
const { csrfToken, checkCsrf, sameOrigin } = require('../auth/forms');
const { readUpload, UploadError } = require('./upload');
const { readUpload, UploadError } = require('./upload');
const { ApiError, privateNoStore } = require('./errors');
const { ApiError, privateNoStore } = require('./errors');
@@ -28,8 +30,8 @@ function createDashboard(ctx) {
@@ -28,8 +30,8 @@ function createDashboard(ctx) {
if (ctx.actorLimits) router.use(ctx.actorLimits);
if (ctx.actorLimits) router.use(ctx.actorLimits);
router.use((req, res, next) => { privateNoStore(res); next(); });
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) {
function fail(req, res, err) {
@@ -69,7 +71,10 @@ function createDashboard(ctx) {
@@ -69,7 +71,10 @@ function createDashboard(ctx) {
router.get('/', view(async (req, res) => {
router.get('/', view(async (req, res) => {
// The signed-out front page is the public page of openvibe.host (sitemap.xml): indexable.
// The signed-out front page is the public page of openvibe.host (sitemap.xml): indexable.
// Everything behind sign-in stays noindex and private.
// 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));
page(req, res, 200, 'Projects', pages.home({ projects: await projects.listFor(req.viewer), csrf: csrfToken(config, req.viewer) }), notice(req));
}));
}));
@@ -75,6 +75,8 @@ function renderPage(o) {
@@ -75,6 +75,8 @@ function renderPage(o) {
head: [
head: [
appIcon.headTags({ site: 'host' }),
appIcon.headTags({ site: 'host' }),
`<link rel="stylesheet" href="${asset('css/host.css')}">`,
`<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)}">`,
`<meta name="ov-boost" content="host@${esc(RELEASE)}">`,
`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
`<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>`,
].join('\n'),
].join('\n'),
@@ -3,6 +3,7 @@
@@ -3,6 +3,7 @@
* Dashboard page bodies. Every value is escaped; every action is a POST form carrying the form
* Dashboard page bodies. Every value is escaped; every action is a POST form carrying the form
* token, so the dashboard works without JavaScript.
* token, so the dashboard works without JavaScript.
*/
*/
+const showcase = require('openvibe-shared/showcase');
const { esc } = require('./layout');
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`);
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 }
@@ -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>`;
+ `${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><site>.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 }) {
function home({ projects, csrf }) {
@@ -54,7 +54,11 @@ const ORIGIN = 'https://openvibe.host';
@@ -54,7 +54,11 @@ const ORIGIN = 'https://openvibe.host';
assert.match(r.text, /<noscript><nav aria-label="Site"/);
assert.match(r.text, /<noscript><nav aria-label="Site"/);
assert.match(r.text, /id="ov-footer"/);
assert.match(r.text, /id="ov-footer"/);
assert.match(r.text, /Sign in with OpenVibe/);
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'/);
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).
// 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;/);
assert.match(r.headers['content-security-policy'], /connect-src 'self' https:\/\/openvibe\.network https:\/\/events\.openvibe\.network;/);
@@ -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: 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)
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)
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
externalFiles: 1, // 0
};
};