openvibers/openvibe.vip Commits
VIP moves between its pages without a reload (plan T11): openvibe-shared v2.2.0 boost swaps <main> styles-first with the shared transition and does a normal load across a deploy (the ov-boost marker carries the release); page scripts that wire <main> also run on ov:boost:load. Tests pin the marker and the boost tag (verified).
Changes
@@@ -61,7 +61,7 @@ home that is not a cache.- **OpenVibe.Events** — delivery of Billing's events to `/internal/events`; relay of VIP's outbox.- **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.76.0, **openvibe-sdk** v0.20.1 (outbox, inbox, delivery signatures, per-actor-- limits), **openvibe-shared** v1.27.0 (Frame, legal pages, release manifest and update metrics, metrics,++ limits), **openvibe-shared** v2.2.0 (Frame, legal pages, release manifest and update metrics, metrics,readiness).- Consumers, through the client below: Chat (the member badge), Community (members-only spaces andthreads), Blog (members-only posts) and Wiki (VIP spaces and pages); Live is not wired yet.@@@ -16,7 +16,7 @@"jsonwebtoken": "^9.0.2","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": {@@@ -1162,9 +1162,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"@@@ -33,7 +33,7 @@"jsonwebtoken": "^9.0.2","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": {@@@ -2,7 +2,7 @@(function () {'use strict';document.documentElement.classList.add('js');-- document.addEventListener('DOMContentLoaded', function () {++ function wire() {// Dashboard rule form: show the plan/perk pickers only for the requirement that uses them.var form = document.querySelector('form[action$="/rules"]');if (!form) return;@@@ -14,5 +14,7 @@if (perk) perk.closest('label').hidden = req.value !== 'perk';}if (req) { req.addEventListener('change', sync); sync(); }-- });++ }++ document.addEventListener('DOMContentLoaded', wire);++ document.addEventListener('ov:boost:load', wire); // boost swaps <main> without a reload: wire the new content too})();@@@ -32,7 +32,7 @@ const { createActorLimits } = require('./api/actor-limits');const { createSessionRoutes } = require('./web/session');const { createWebRoutes } = require('./web/routes');const { createEmbedRoutes } = require('./web/embeds');--const { createLayout, assetVersion } = require('./web/layout');++const { createLayout, assetVersion, setRelease } = require('./web/layout');const { createVipReadiness } = require('./observability');const pages = require('./web/pages');@@@ -54,6 +54,7 @@ async function createApp(opts = {}) {const domain = createDomain({ db, config, outbox, billing, now, log });const apiAuth = createApiAuth({ config, keys, userAuth });const release = require('openvibe-shared/release').createRelease({ service: 'vip', root: path.join(__dirname, '..') });++ setRelease(release.release);const layout = createLayout({ config, release });const app = express();@@@ -31,6 +31,11 @@ function asset(rel) {}const assetVersion = (rel) => { asset(rel); return _hashes.get(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); }++const NAV_LINKS = [{ label: 'Memberships', href: '/me' },{ label: 'Dashboard', href: '/dashboard' },@@@ -54,7 +59,7 @@ function createLayout({ config, release }) {history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',-- loginUrl: `/auth/login?next=${encodeURIComponent(o.canonicalPath || '/')}`,++ 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)};@@@ -88,6 +93,8 @@ ${jsonLd}<script src="${ovServe.url('navbar.js')}" defer></script><script src="${ovServe.url('footer.js')}" defer></script><script src="${asset('js/vip.js')}" defer></script>++<meta name="ov-boost" content="vip@${esc(RELEASE)}">++<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script></head><body><div id="navbar-mount"></div>@@@ -112,4 +119,4 @@ document.addEventListener('DOMContentLoaded', function () {return { page, abs };}--module.exports = { createLayout, esc, asset, assetVersion, SITE_NAME, DEFAULT_DESCRIPTION };++module.exports = { createLayout, esc, asset, assetVersion, setRelease, SITE_NAME, DEFAULT_DESCRIPTION };@@@ -169,6 +169,9 @@ const { test, run } = harness('api');assert.match(p.text, /<link rel="canonical" href="http:\/\/vip\.test\/lena">/);assert.match(p.text, /\/shared\/navbar\.js\?v=[0-9a-f]{12}/);assert.match(p.text, /<noscript>/);++ assert.match(p.text, /<meta name="ov-boost" content="vip@[^"]+">/);++ assert.match(p.text, /\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer>/);++ assert.match(p.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/);assert.doesNotMatch(p.text, /\bfree\b|\$0|no ads/i);const hist = await t.page(`/lena/plans/${plan.slug}`);assert.strictEqual(hist.status, 200);@@@ -180,6 +183,18 @@ const { test, run } = harness('api');assert.match(sm.text, /http:\/\/vip\.test\/lena/);});++ test('every rendered page carries the boost marker, boost script (data-main) and the {path} sign-in template', async () => {++ const pages = [['/', null], ['/lena', null], ['/updates', null], ['/me', buyer], ['/dashboard', creator]];++ for (const [p, user] of pages) {++ const r = await t.page(p, user ? { user } : undefined);++ assert.strictEqual(r.status, 200, `${p} → ${r.status}`);++ assert.match(r.text, /<meta name="ov-boost" content="vip@[^"]+">/, `${p}: release marker`);++ assert.match(r.text, /\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer>/, `${p}: boost script`);++ assert.match(r.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/, `${p}: sign-in returns to the current page`);++ assert.match(r.text, /<main id="main"/, `${p}: the swap target`);++ }++ });++test('joining from the page: form with anti-forgery token → Billing credit → member page', async () => {const x = t.network.newUser('rita');t.billing.state.credit.set(x.subject, 600);
@@ -61,7 +61,7 @@ home that is not a cache.
@@ -61,7 +61,7 @@ home that is not a cache.
- **OpenVibe.Events** — delivery of Billing's events to `/internal/events`; relay of VIP's outbox.
- **OpenVibe.Events** — delivery of Billing's events to `/internal/events`; relay of VIP's outbox.
- **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.76.0, **openvibe-sdk** v0.20.1 (outbox, inbox, delivery signatures, per-actor
- **openvibe-contracts** v0.76.0, **openvibe-sdk** v0.20.1 (outbox, inbox, delivery signatures, per-actor
- limits), **openvibe-shared** v1.27.0 (Frame, legal pages, release manifest and update metrics, metrics,
+ limits), **openvibe-shared** v2.2.0 (Frame, legal pages, release manifest and update metrics, metrics,
readiness).
readiness).
- Consumers, through the client below: Chat (the member badge), Community (members-only spaces and
- Consumers, through the client below: Chat (the member badge), Community (members-only spaces and
threads), Blog (members-only posts) and Wiki (VIP spaces and pages); Live is not wired yet.
threads), Blog (members-only posts) and Wiki (VIP spaces and pages); Live is not wired yet.
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"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": {
@@ -1162,9 +1162,9 @@
@@ -1162,9 +1162,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"
@@ -33,7 +33,7 @@
@@ -33,7 +33,7 @@
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"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": {
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
(function () {
(function () {
'use strict';
'use strict';
document.documentElement.classList.add('js');
document.documentElement.classList.add('js');
- document.addEventListener('DOMContentLoaded', function () {
+ function wire() {
// Dashboard rule form: show the plan/perk pickers only for the requirement that uses them.
// Dashboard rule form: show the plan/perk pickers only for the requirement that uses them.
var form = document.querySelector('form[action$="/rules"]');
var form = document.querySelector('form[action$="/rules"]');
if (!form) return;
if (!form) return;
@@ -14,5 +14,7 @@
@@ -14,5 +14,7 @@
if (perk) perk.closest('label').hidden = req.value !== 'perk';
if (perk) perk.closest('label').hidden = req.value !== 'perk';
}
}
if (req) { req.addEventListener('change', sync); sync(); }
if (req) { req.addEventListener('change', sync); sync(); }
- });
+ }
+ document.addEventListener('DOMContentLoaded', wire);
+ document.addEventListener('ov:boost:load', wire); // boost swaps <main> without a reload: wire the new content too
})();
})();
@@ -32,7 +32,7 @@ const { createActorLimits } = require('./api/actor-limits');
@@ -32,7 +32,7 @@ const { createActorLimits } = require('./api/actor-limits');
const { createSessionRoutes } = require('./web/session');
const { createSessionRoutes } = require('./web/session');
const { createWebRoutes } = require('./web/routes');
const { createWebRoutes } = require('./web/routes');
const { createEmbedRoutes } = require('./web/embeds');
const { createEmbedRoutes } = require('./web/embeds');
-const { createLayout, assetVersion } = require('./web/layout');
+const { createLayout, assetVersion, setRelease } = require('./web/layout');
const { createVipReadiness } = require('./observability');
const { createVipReadiness } = require('./observability');
const pages = require('./web/pages');
const pages = require('./web/pages');
@@ -54,6 +54,7 @@ async function createApp(opts = {}) {
@@ -54,6 +54,7 @@ async function createApp(opts = {}) {
const domain = createDomain({ db, config, outbox, billing, now, log });
const domain = createDomain({ db, config, outbox, billing, now, log });
const apiAuth = createApiAuth({ config, keys, userAuth });
const apiAuth = createApiAuth({ config, keys, userAuth });
const release = require('openvibe-shared/release').createRelease({ service: 'vip', root: path.join(__dirname, '..') });
const release = require('openvibe-shared/release').createRelease({ service: 'vip', root: path.join(__dirname, '..') });
+ setRelease(release.release);
const layout = createLayout({ config, release });
const layout = createLayout({ config, release });
const app = express();
const app = express();
@@ -31,6 +31,11 @@ function asset(rel) {
@@ -31,6 +31,11 @@ function asset(rel) {
}
}
const assetVersion = (rel) => { asset(rel); return _hashes.get(rel); };
const assetVersion = (rel) => { asset(rel); return _hashes.get(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); }
+
const NAV_LINKS = [
const NAV_LINKS = [
{ label: 'Memberships', href: '/me' },
{ label: 'Memberships', href: '/me' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Dashboard', href: '/dashboard' },
@@ -54,7 +59,7 @@ function createLayout({ config, release }) {
@@ -54,7 +59,7 @@ function createLayout({ config, release }) {
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
- loginUrl: `/auth/login?next=${encodeURIComponent(o.canonicalPath || '/')}`,
+ 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)
};
};
@@ -88,6 +93,8 @@ ${jsonLd}
@@ -88,6 +93,8 @@ ${jsonLd}
<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>
<script src="${asset('js/vip.js')}" defer></script>
<script src="${asset('js/vip.js')}" defer></script>
+<meta name="ov-boost" content="vip@${esc(RELEASE)}">
+<script src="${ovServe.url('boost.js')}" data-main="#main" defer></script>
</head>
</head>
<body>
<body>
<div id="navbar-mount"></div>
<div id="navbar-mount"></div>
@@ -112,4 +119,4 @@ document.addEventListener('DOMContentLoaded', function () {
@@ -112,4 +119,4 @@ document.addEventListener('DOMContentLoaded', function () {
return { page, abs };
return { page, abs };
}
}
-module.exports = { createLayout, esc, asset, assetVersion, SITE_NAME, DEFAULT_DESCRIPTION };
+module.exports = { createLayout, esc, asset, assetVersion, setRelease, SITE_NAME, DEFAULT_DESCRIPTION };
@@ -169,6 +169,9 @@ const { test, run } = harness('api');
@@ -169,6 +169,9 @@ const { test, run } = harness('api');
assert.match(p.text, /<link rel="canonical" href="http:\/\/vip\.test\/lena">/);
assert.match(p.text, /<link rel="canonical" href="http:\/\/vip\.test\/lena">/);
assert.match(p.text, /\/shared\/navbar\.js\?v=[0-9a-f]{12}/);
assert.match(p.text, /\/shared\/navbar\.js\?v=[0-9a-f]{12}/);
assert.match(p.text, /<noscript>/);
assert.match(p.text, /<noscript>/);
+ assert.match(p.text, /<meta name="ov-boost" content="vip@[^"]+">/);
+ assert.match(p.text, /\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer>/);
+ assert.match(p.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/);
assert.doesNotMatch(p.text, /\bfree\b|\$0|no ads/i);
assert.doesNotMatch(p.text, /\bfree\b|\$0|no ads/i);
const hist = await t.page(`/lena/plans/${plan.slug}`);
const hist = await t.page(`/lena/plans/${plan.slug}`);
assert.strictEqual(hist.status, 200);
assert.strictEqual(hist.status, 200);
@@ -180,6 +183,18 @@ const { test, run } = harness('api');
@@ -180,6 +183,18 @@ const { test, run } = harness('api');
assert.match(sm.text, /http:\/\/vip\.test\/lena/);
assert.match(sm.text, /http:\/\/vip\.test\/lena/);
});
});
+ test('every rendered page carries the boost marker, boost script (data-main) and the {path} sign-in template', async () => {
+ const pages = [['/', null], ['/lena', null], ['/updates', null], ['/me', buyer], ['/dashboard', creator]];
+ for (const [p, user] of pages) {
+ const r = await t.page(p, user ? { user } : undefined);
+ assert.strictEqual(r.status, 200, `${p} → ${r.status}`);
+ assert.match(r.text, /<meta name="ov-boost" content="vip@[^"]+">/, `${p}: release marker`);
+ assert.match(r.text, /\/shared\/boost\.js\?v=[0-9a-f]{12}" data-main="#main" defer>/, `${p}: boost script`);
+ assert.match(r.text, /"loginUrl":"\/auth\/login\?next=\{path\}"/, `${p}: sign-in returns to the current page`);
+ assert.match(r.text, /<main id="main"/, `${p}: the swap target`);
+ }
+ });
+
test('joining from the page: form with anti-forgery token → Billing credit → member page', async () => {
test('joining from the page: form with anti-forgery token → Billing credit → member page', async () => {
const x = t.network.newUser('rita');
const x = t.network.newUser('rita');
t.billing.state.credit.set(x.subject, 600);
t.billing.state.credit.set(x.subject, 600);