openvibers/openvibe.codes Commits
Codes 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. test/layout.test.js pins the marker, the boost tag and the login template.
Changes
@@@ -48,7 +48,7 @@ Everything is server-rendered and usable without JavaScript. The only scripts of- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).--- **openvibe-contracts** v0.79.0, **openvibe-sdk v0.25.0**, **openvibe-shared v2.0.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).++- **openvibe-contracts** v0.79.0, **openvibe-sdk v0.25.0**, **openvibe-shared v2.2.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).@@@ -11,7 +11,7 @@"packages": ["openvibe-contracts v0.79.0","openvibe-sdk v0.25.0",-- "openvibe-shared v2.0.0"++ "openvibe-shared v2.2.0"],"summary": "Deployed and public since 2026-09-23: https://openvibe.codes is served by this portal (deployed with `sudo ovhost deploy codes`, unit openvibe-codes on 127.0.0.1:4900 behind nginx), and the domain left OpenVibe.Sites. Sign-in uses the registered Network OAuth client `codes` (PKCE S256); the principal `codes` holds events.event.publish and the Events relay is configured. On 2026-09-23 production held 0 manifests, 0 releases and 0 playground runs. The external-developer path (account, project, sandbox app, grants, Media upload, app events) was run once on production on 2026-09-23 with public endpoints and curl, not through these playgrounds; that check is not committed to a repository or CI.","works": [@@@ -18,7 +18,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"},"devDependencies": {@@@ -1202,9 +1202,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"@@@ -30,7 +30,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": {@@@ -74,6 +74,7 @@ async function createApp(opts = {}) {app.disable('x-powered-by');app.set('trust proxy', config.trustProxy);const release = require('openvibe-shared/release').createRelease({ service: 'codes', root: path.join(__dirname, '..') });++ require('./render/layout').setRelease(release.release);const metrics = require('openvibe-shared/metrics').instrument(app, { service: 'codes', release: release.release });app.locals.metrics = metrics.registry;app.locals.ctx = ctx;@@@ -39,6 +39,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, description, body (html), viewer, config, path, index (default false), scripts [rel],* crumbs [{ label, href }], jsonLd [nodes] (serialised by openvibe-shared/seo)@@@ -56,7 +61,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)};@@@ -89,6 +94,8 @@ ${(o.jsonLd || []).filter(Boolean).map(seo.jsonLdTag).join('\n')}<script src="${ovServe.url('navbar.js')}" defer></script><script src="${ovServe.url('footer.js')}" defer></script>${render(scripts)}++<meta name="ov-boost" content="codes@${esc(RELEASE)}">++<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script></head><body><a class="skip" href="#main">Skip to content</a>@@@ -123,4 +130,4 @@ function send(res, status, o) {res.status(status).type('html').send(renderPage(o));}--module.exports = { renderPage, send, asset, assetVersion, SITE_NAME, NETWORK_URL, NAV, raw, html };++module.exports = { renderPage, send, asset, assetVersion, setRelease, SITE_NAME, NETWORK_URL, NAV, raw, html };nnew file mode 100644@@@ -0,0 +1,44 @@++'use strict';++/**++ * Every page carries the openvibe-shared boost marker and self-starting script (plan T11), so a++ * same-site click swaps <main> in place without a reload. The marker names the layout's release,++ * which the app sets from openvibe-shared/release (a swap only happens between pages of the same++ * release; across a deploy it is a normal load), the boost tag names #main in data-main, and the++ * shared navbar's sign-in follows the current page through the {path} template.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/boot');++++const MARKER = /<meta name="ov-boost" content="(codes@[0-9A-Za-z._+-]{1,64})">/;++const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer><\/script>/;++++(async () => {++ const t = await boot();++ const user = t.network.addUser('kim');++ const release = (await t.get('/release.json')).json().release;++++ await check('every rendered page carries the ov-boost marker (the app\'s release) and the data-main boost script', async () => {++ for (const p of ['/', '/docs', '/oauth', '/manifests/validate']) {++ const r = await t.get(p);++ assert.strictEqual(r.status, 200, p);++ const m = r.text.match(MARKER);++ assert.ok(m, `${p}: no ov-boost marker`);++ assert.strictEqual(m[1], `codes@${release}`, `${p}: the marker is not the app's release`);++ assert.match(r.text, BOOST, `${p}: no boost script with data-main`);++ assert.match(r.text, /<main id="main"/, `${p}: the page's main is not #main`);++ }++ const signedIn = await t.get('/projects', { as: user });++ assert.strictEqual(signedIn.status, 200);++ assert.match(signedIn.text, MARKER, '/projects: no ov-boost marker');++ assert.match(signedIn.text, BOOST, '/projects: no boost script');++ });++++ await check('the navbar config uses the {path} login template, not the path baked in', async () => {++ const r = await t.get('/docs');++ assert.match(r.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/);++ assert.ok(!/"loginUrl":"\/auth\/login\?next=%2F/.test(r.text), 'loginUrl still has the current path baked in');++ });++++ await t.close();++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -48,7 +48,7 @@ Everything is server-rendered and usable without JavaScript. The only scripts of
@@ -48,7 +48,7 @@ Everything is server-rendered and usable without JavaScript. The only scripts of
- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).
- **OpenVibe.Events** — the outbox relay publishes `codes.app.*` with Codes' service token (`events.event.publish`); the Events playground calls it with the app's token (`events.app.publish`); the project archive pulls the project's app events with a Network export token (`events.app.read`).
- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).
- **OpenVibe.Media** — the Media playground uploads with the app's token into the project's namespace; the project archive lists the project's objects, namespaces and download URLs with a Network export token (`media.object.list`, `media.object.read`).
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional).
-- **openvibe-contracts** v0.79.0, **openvibe-sdk v0.25.0**, **openvibe-shared v2.0.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).
+- **openvibe-contracts** v0.79.0, **openvibe-sdk v0.25.0**, **openvibe-shared v2.2.0** (pinned tag tarballs; the docs show the tag and the package version, and say so when they differ).
No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).
No path in Codes sends or accepts a shared loopback key (tested by grep and at runtime).
@@ -11,7 +11,7 @@
@@ -11,7 +11,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"
],
],
"summary": "Deployed and public since 2026-09-23: https://openvibe.codes is served by this portal (deployed with `sudo ovhost deploy codes`, unit openvibe-codes on 127.0.0.1:4900 behind nginx), and the domain left OpenVibe.Sites. Sign-in uses the registered Network OAuth client `codes` (PKCE S256); the principal `codes` holds events.event.publish and the Events relay is configured. On 2026-09-23 production held 0 manifests, 0 releases and 0 playground runs. The external-developer path (account, project, sandbox app, grants, Media upload, app events) was run once on production on 2026-09-23 with public endpoints and curl, not through these playgrounds; that check is not committed to a repository or CI.",
"summary": "Deployed and public since 2026-09-23: https://openvibe.codes is served by this portal (deployed with `sudo ovhost deploy codes`, unit openvibe-codes on 127.0.0.1:4900 behind nginx), and the domain left OpenVibe.Sites. Sign-in uses the registered Network OAuth client `codes` (PKCE S256); the principal `codes` holds events.event.publish and the Events relay is configured. On 2026-09-23 production held 0 manifests, 0 releases and 0 playground runs. The external-developer path (account, project, sandbox app, grants, Media upload, app events) was run once on production on 2026-09-23 with public endpoints and curl, not through these playgrounds; that check is not committed to a repository or CI.",
"works": [
"works": [
@@ -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.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"
},
},
"devDependencies": {
"devDependencies": {
@@ -1202,9 +1202,9 @@
@@ -1202,9 +1202,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"
@@ -30,7 +30,7 @@
@@ -30,7 +30,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": {
@@ -74,6 +74,7 @@ async function createApp(opts = {}) {
@@ -74,6 +74,7 @@ async function createApp(opts = {}) {
app.disable('x-powered-by');
app.disable('x-powered-by');
app.set('trust proxy', config.trustProxy);
app.set('trust proxy', config.trustProxy);
const release = require('openvibe-shared/release').createRelease({ service: 'codes', root: path.join(__dirname, '..') });
const release = require('openvibe-shared/release').createRelease({ service: 'codes', root: path.join(__dirname, '..') });
+ require('./render/layout').setRelease(release.release);
const metrics = require('openvibe-shared/metrics').instrument(app, { service: 'codes', release: release.release });
const metrics = require('openvibe-shared/metrics').instrument(app, { service: 'codes', release: release.release });
app.locals.metrics = metrics.registry;
app.locals.metrics = metrics.registry;
app.locals.ctx = ctx;
app.locals.ctx = ctx;
@@ -39,6 +39,11 @@ function assetVersion(rel) {
@@ -39,6 +39,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, description, body (html), viewer, config, path, index (default false), scripts [rel],
* o: title, description, body (html), viewer, config, path, index (default false), scripts [rel],
* crumbs [{ label, href }], jsonLd [nodes] (serialised by openvibe-shared/seo)
* crumbs [{ label, href }], jsonLd [nodes] (serialised by openvibe-shared/seo)
@@ -56,7 +61,7 @@ function renderPage(o) {
@@ -56,7 +61,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)
};
};
@@ -89,6 +94,8 @@ ${(o.jsonLd || []).filter(Boolean).map(seo.jsonLdTag).join('\n')}
@@ -89,6 +94,8 @@ ${(o.jsonLd || []).filter(Boolean).map(seo.jsonLdTag).join('\n')}
<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>
${render(scripts)}
${render(scripts)}
+<meta name="ov-boost" content="codes@${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>
@@ -123,4 +130,4 @@ function send(res, status, o) {
@@ -123,4 +130,4 @@ function send(res, status, o) {
res.status(status).type('html').send(renderPage(o));
res.status(status).type('html').send(renderPage(o));
}
}
-module.exports = { renderPage, send, asset, assetVersion, SITE_NAME, NETWORK_URL, NAV, raw, html };
+module.exports = { renderPage, send, asset, assetVersion, setRelease, SITE_NAME, NETWORK_URL, NAV, raw, html };
new file mode 100644
new file mode 100644
@@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
+'use strict';
+/**
+ * Every page carries the openvibe-shared boost marker and self-starting script (plan T11), so a
+ * same-site click swaps <main> in place without a reload. The marker names the layout's release,
+ * which the app sets from openvibe-shared/release (a swap only happens between pages of the same
+ * release; across a deploy it is a normal load), the boost tag names #main in data-main, and the
+ * shared navbar's sign-in follows the current page through the {path} template.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/boot');
+
+const MARKER = /<meta name="ov-boost" content="(codes@[0-9A-Za-z._+-]{1,64})">/;
+const BOOST = /<script src="\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer><\/script>/;
+
+(async () => {
+ const t = await boot();
+ const user = t.network.addUser('kim');
+ const release = (await t.get('/release.json')).json().release;
+
+ await check('every rendered page carries the ov-boost marker (the app\'s release) and the data-main boost script', async () => {
+ for (const p of ['/', '/docs', '/oauth', '/manifests/validate']) {
+ const r = await t.get(p);
+ assert.strictEqual(r.status, 200, p);
+ const m = r.text.match(MARKER);
+ assert.ok(m, `${p}: no ov-boost marker`);
+ assert.strictEqual(m[1], `codes@${release}`, `${p}: the marker is not the app's release`);
+ assert.match(r.text, BOOST, `${p}: no boost script with data-main`);
+ assert.match(r.text, /<main id="main"/, `${p}: the page's main is not #main`);
+ }
+ const signedIn = await t.get('/projects', { as: user });
+ assert.strictEqual(signedIn.status, 200);
+ assert.match(signedIn.text, MARKER, '/projects: no ov-boost marker');
+ assert.match(signedIn.text, BOOST, '/projects: no boost script');
+ });
+
+ await check('the navbar config uses the {path} login template, not the path baked in', async () => {
+ const r = await t.get('/docs');
+ assert.match(r.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/);
+ assert.ok(!/"loginUrl":"\/auth\/login\?next=%2F/.test(r.text), 'loginUrl still has the current path baked in');
+ });
+
+ await t.close();
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });