openvibers/openvibe.host Commits
Host moves between its pages without a reload (plan T11): openvibe-shared v2.2.0 boost swaps <main> styles-first with the shared transition, prefetching on intent, and does a normal load across a deploy (the ov-boost marker carries the release). The navbar's sign-in returns to the current page.
Changes
@@@ -52,7 +52,7 @@ Stage A adds a few safety rules of its own:- OpenVibe.Contracts (`openvibe-contracts` v0.79.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.25.0 transactional outbox (openvibe-sdk/limits for the per-actor limits).--- `openvibe-shared` v2.0.0 (Stage B): shared chrome, legal pages, `/release.json`, `/metrics`, `/api/ready`.++- `openvibe-shared` v2.2.0 (Stage B): shared chrome, legal pages, `/release.json`, `/metrics`, `/api/ready`.- OpenVibe.Media: not yet. The roadmap stores artifacts "through Media where practical"; Stage B keeps them on local disk for now (see [Not done yet](#not-done-yet-stage-b)).## Capabilities@@@ -324,7 +324,7 @@ These tenant events have subject `deploy` and visibility `internal`; `host.deplo### Dashboard--Server-rendered pages with the shared chrome (`openvibe-shared` v2.0.0: 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.2.0: 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@@@ -12,7 +12,7 @@"packages": ["openvibe-contracts v0.79.0","openvibe-sdk v0.25.0",-- "openvibe-shared v2.0.0"++ "openvibe-shared v2.2.0"],"capabilities": ["host.site.manage",@@@ -19,7 +19,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"},"bin": {@@@ -1546,9 +1546,9 @@}},"node_modules/openvibe-shared": {-- "version": "2.0.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",-- "integrity": "sha512-n5CHC0hfxOo0jo/3kS4O3KiwhirhZDlx5zZTIuPyIf3Dq0yI/R6R64btx7NYwZe5dI0NjvqzEtnv19m8unwLMg==",++ "version": "2.2.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",++ "integrity": "sha512-1q+iUuUxzPeylTYmithW4jR++Jccvn5GEOrB1B3D18Dgn6OXQhPpvhPQAa1D1k1p+dWIa1/SmggyJwSGFnTNtw==","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.79.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0","pg": "^8.23.0"},"engines": {@@@ -38,7 +38,7 @@ const { createApi } = require('./http/api');const { createDashboard } = require('./http/dashboard');const { createHostReadiness } = require('./observability');const { createWorker } = require('./worker');--const { renderPage, assetVersion } = require('./render/layout');++const { renderPage, assetVersion, setRelease } = require('./render/layout');const pages = require('./render/pages');const PUBLIC_DIR = path.join(__dirname, '..', 'public');@@@ -107,6 +107,7 @@ async function createApp(opts = {}) {app.set('trust proxy', config.trustProxy);const release = require('openvibe-shared/release').createRelease({ service: 'host', root: path.join(__dirname, '..') });++ setRelease(release.release);const registry = sharedMetrics.createRegistry();const httpMetrics = sharedMetrics.httpMetrics(registry, { normalize: (req) => (req.hostTarget === 'site' ? 'tenant_site' : req.hostTarget === 'unknown' ? 'unknown_host' : null) });const proc = sharedMetrics.processMetrics(registry);@@@ -28,6 +28,11 @@ function assetVersion(rel) {}const asset = (rel) => `/${rel}?v=${assetVersion(rel)}`;++// The deployed release (app.js sets it from openvibe-shared/release): openvibe-shared/boost swaps a page in place only++// between pages of the same release, and does a normal load across a deploy.++let RELEASE = 'dev';++function setRelease(id) { if (id) RELEASE = String(id); }++/** o: title, body (HTML), viewer, config, path, notice { kind, text } */function renderPage(o) {const viewer = o.viewer || { kind: 'anonymous' };@@@ -40,7 +45,7 @@ function renderPage(o) {history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',-- loginUrl: `/auth/login?next=${loginNext}`,++ loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session toonotificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)};@@@ -67,6 +72,8 @@ ${appIcon.headTags({ site: 'host' })}<script src="${ovServe.url('theme-loader.js')}" defer></script><script src="${ovServe.url('navbar.js')}" defer></script><script src="${ovServe.url('footer.js')}" defer></script>++<meta name="ov-boost" content="host@${esc(RELEASE)}">++<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script></head><body><a class="skip" href="#main">Skip to content</a>@@@ -90,4 +97,4 @@ document.addEventListener('DOMContentLoaded', function () {</html>`;}--module.exports = { renderPage, esc, asset, assetVersion, SITE_NAME, NETWORK_URL };++module.exports = { renderPage, esc, asset, assetVersion, setRelease, SITE_NAME, NETWORK_URL };nnew file mode 100644@@@ -0,0 +1,29 @@++'use strict';++/**++ * The page shell carries openvibe-shared boost (plan T11): every rendered page is marked with the++ * deployed release and loads boost.js with data-main="#main", and the shared navbar's sign-in++ * returns to whatever page is showing (the {path} template) instead of a path baked at render time.++ */++const assert = require('assert');++const { test, runTests } = require('./helpers');++const layout = require('../server/render/layout');++++const config = { baseUrl: 'https://openvibe.host' };++const page = (o = {}) => layout.renderPage({ title: 'A page', body: '<p>body</p>', viewer: { kind: 'anonymous' }, config, path: '/projects/abc', ...o });++++runTests([++ test('every page carries the boost marker and the boost script with data-main', () => {++ layout.setRelease('20260929-120000-deadbeef');++ for (const p of [page(), page({ path: '/', indexable: true })]) {++ assert.match(p, /<meta name="ov-boost" content="host@20260929-120000-deadbeef">/);++ assert.match(p, /<script src="\/shared\/boost\.js\?v=[0-9a-f]+" data-main="#main" defer><\/script>/);++ assert.match(p, /<main id="main"/, 'the swapped part is <main id="main">');++ }++ }),++++ test("the shared navbar's sign-in returns to the current page ({path} template)", () => {++ const cfg = JSON.parse(page().match(/window\.__OV_PAGE = (.+);/)[1]);++ assert.strictEqual(cfg.navbar.loginUrl, '/auth/login?next={path}');++ assert.strictEqual(cfg.navbar.logoutUrl, '/auth/logout?next={path}');++ }),++]);
@@ -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.79.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.79.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.25.0 transactional outbox (openvibe-sdk/limits for the per-actor limits).
- OpenVibe.Events (Stage B): `host.*` events through the `openvibe-sdk` v0.25.0 transactional outbox (openvibe-sdk/limits for the per-actor limits).
-- `openvibe-shared` v2.0.0 (Stage B): shared chrome, legal pages, `/release.json`, `/metrics`, `/api/ready`.
+- `openvibe-shared` v2.2.0 (Stage B): shared chrome, legal pages, `/release.json`, `/metrics`, `/api/ready`.
- OpenVibe.Media: not yet. The roadmap stores artifacts "through Media where practical"; Stage B keeps them on local disk for now (see [Not done yet](#not-done-yet-stage-b)).
- OpenVibe.Media: not yet. The roadmap stores artifacts "through Media where practical"; Stage B keeps them on local disk for now (see [Not done yet](#not-done-yet-stage-b)).
## Capabilities
## Capabilities
@@ -324,7 +324,7 @@ These tenant events have subject `deploy` and visibility `internal`; `host.deplo
@@ -324,7 +324,7 @@ These tenant events have subject `deploy` and visibility `internal`; `host.deplo
### Dashboard
### Dashboard
-Server-rendered pages with the shared chrome (`openvibe-shared` v2.0.0: 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.2.0: 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
@@ -12,7 +12,7 @@
@@ -12,7 +12,7 @@
"packages": [
"packages": [
"openvibe-contracts v0.79.0",
"openvibe-contracts v0.79.0",
"openvibe-sdk v0.25.0",
"openvibe-sdk v0.25.0",
- "openvibe-shared v2.0.0"
+ "openvibe-shared v2.2.0"
],
],
"capabilities": [
"capabilities": [
"host.site.manage",
"host.site.manage",
@@ -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.79.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"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.0.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
- "integrity": "sha512-n5CHC0hfxOo0jo/3kS4O3KiwhirhZDlx5zZTIuPyIf3Dq0yI/R6R64btx7NYwZe5dI0NjvqzEtnv19m8unwLMg==",
+ "version": "2.2.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
+ "integrity": "sha512-1q+iUuUxzPeylTYmithW4jR++Jccvn5GEOrB1B3D18Dgn6OXQhPpvhPQAa1D1k1p+dWIa1/SmggyJwSGFnTNtw==",
"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.79.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.0.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -38,7 +38,7 @@ const { createApi } = require('./http/api');
@@ -38,7 +38,7 @@ const { createApi } = require('./http/api');
const { createDashboard } = require('./http/dashboard');
const { createDashboard } = require('./http/dashboard');
const { createHostReadiness } = require('./observability');
const { createHostReadiness } = require('./observability');
const { createWorker } = require('./worker');
const { createWorker } = require('./worker');
-const { renderPage, assetVersion } = require('./render/layout');
+const { renderPage, assetVersion, setRelease } = require('./render/layout');
const pages = require('./render/pages');
const pages = require('./render/pages');
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
@@ -107,6 +107,7 @@ async function createApp(opts = {}) {
@@ -107,6 +107,7 @@ async function createApp(opts = {}) {
app.set('trust proxy', config.trustProxy);
app.set('trust proxy', config.trustProxy);
const release = require('openvibe-shared/release').createRelease({ service: 'host', root: path.join(__dirname, '..') });
const release = require('openvibe-shared/release').createRelease({ service: 'host', root: path.join(__dirname, '..') });
+ setRelease(release.release);
const registry = sharedMetrics.createRegistry();
const registry = sharedMetrics.createRegistry();
const httpMetrics = sharedMetrics.httpMetrics(registry, { normalize: (req) => (req.hostTarget === 'site' ? 'tenant_site' : req.hostTarget === 'unknown' ? 'unknown_host' : null) });
const httpMetrics = sharedMetrics.httpMetrics(registry, { normalize: (req) => (req.hostTarget === 'site' ? 'tenant_site' : req.hostTarget === 'unknown' ? 'unknown_host' : null) });
const proc = sharedMetrics.processMetrics(registry);
const proc = sharedMetrics.processMetrics(registry);
@@ -28,6 +28,11 @@ function assetVersion(rel) {
@@ -28,6 +28,11 @@ function assetVersion(rel) {
}
}
const asset = (rel) => `/${rel}?v=${assetVersion(rel)}`;
const asset = (rel) => `/${rel}?v=${assetVersion(rel)}`;
+// The deployed release (app.js sets it from openvibe-shared/release): openvibe-shared/boost swaps a page in place only
+// between pages of the same release, and does a normal load across a deploy.
+let RELEASE = 'dev';
+function setRelease(id) { if (id) RELEASE = String(id); }
+
/** o: title, body (HTML), viewer, config, path, notice { kind, text } */
/** o: title, body (HTML), viewer, config, path, notice { kind, text } */
function renderPage(o) {
function renderPage(o) {
const viewer = o.viewer || { kind: 'anonymous' };
const viewer = o.viewer || { kind: 'anonymous' };
@@ -40,7 +45,7 @@ function renderPage(o) {
@@ -40,7 +45,7 @@ function renderPage(o) {
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
- loginUrl: `/auth/login?next=${loginNext}`,
+ loginUrl: '/auth/login?next={path}', // filled from the current page (boost moves between pages)
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
};
};
@@ -67,6 +72,8 @@ ${appIcon.headTags({ site: 'host' })}
@@ -67,6 +72,8 @@ ${appIcon.headTags({ site: 'host' })}
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
<script src="${ovServe.url('footer.js')}" defer></script>
<script src="${ovServe.url('footer.js')}" defer></script>
+<meta name="ov-boost" content="host@${esc(RELEASE)}">
+<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>
</head>
</head>
<body>
<body>
<a class="skip" href="#main">Skip to content</a>
<a class="skip" href="#main">Skip to content</a>
@@ -90,4 +97,4 @@ document.addEventListener('DOMContentLoaded', function () {
@@ -90,4 +97,4 @@ document.addEventListener('DOMContentLoaded', function () {
</html>`;
</html>`;
}
}
-module.exports = { renderPage, esc, asset, assetVersion, SITE_NAME, NETWORK_URL };
+module.exports = { renderPage, esc, asset, assetVersion, setRelease, SITE_NAME, NETWORK_URL };
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+'use strict';
+/**
+ * The page shell carries openvibe-shared boost (plan T11): every rendered page is marked with the
+ * deployed release and loads boost.js with data-main="#main", and the shared navbar's sign-in
+ * returns to whatever page is showing (the {path} template) instead of a path baked at render time.
+ */
+const assert = require('assert');
+const { test, runTests } = require('./helpers');
+const layout = require('../server/render/layout');
+
+const config = { baseUrl: 'https://openvibe.host' };
+const page = (o = {}) => layout.renderPage({ title: 'A page', body: '<p>body</p>', viewer: { kind: 'anonymous' }, config, path: '/projects/abc', ...o });
+
+runTests([
+ test('every page carries the boost marker and the boost script with data-main', () => {
+ layout.setRelease('20260929-120000-deadbeef');
+ for (const p of [page(), page({ path: '/', indexable: true })]) {
+ assert.match(p, /<meta name="ov-boost" content="host@20260929-120000-deadbeef">/);
+ assert.match(p, /<script src="\/shared\/boost\.js\?v=[0-9a-f]+" data-main="#main" defer><\/script>/);
+ assert.match(p, /<main id="main"/, 'the swapped part is <main id="main">');
+ }
+ }),
+
+ test("the shared navbar's sign-in returns to the current page ({path} template)", () => {
+ const cfg = JSON.parse(page().match(/window\.__OV_PAGE = (.+);/)[1]);
+ assert.strictEqual(cfg.navbar.loginUrl, '/auth/login?next={path}');
+ assert.strictEqual(cfg.navbar.logoutUrl, '/auth/logout?next={path}');
+ }),
+]);