openvibers/openvibe.space Commits
Share: one Share sheet on a space's page (#208)
Changes
@@@ -2,6 +2,7 @@## Unreleased++- **Share:** a space's page carries the network's one Share button (`data-ov-share-sheet`) in its actions; it shares the published space's own address (or the page's address for a draft), with its name and description. `test/spaces.test.js`.- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.- Usage readings (plan T5 step 14, Space slice): `server/billing.js` turns the bytes the local object store keeps (`space_blobs`, each blob once per project) into one `platform.usage-sample@1` `gb-month` reading per production project and owner per closed UTC day, queued in `billing_readings` (migration `0006_billing_readings.sql`) and relayed to OpenVibe.Billing's `billing.usage.record`. Off unless `SPACE_BILLING_INTERVAL_MS` > 0; with `SPACE_OBJECT_STORE=media` Space emits none, because Media bills the stored bytes.- The forum moved back to OpenVibe.Community (owner decision, 2026-10-08), so Space keeps none of it. Removed: `server/forum/*`, `server/relay/*`, `server/vip/*`, `server/media/*`, `server/identity/*`, `server/{chat-rooms,votes,limits,actor-limits,events,events-consumer}.js`, `server/http/v1.js`, `server/render/{board,forum,markdown,jsonld,highlight}.js`, `public/js/space.js`, the forum suites and their fixtures, `docs/discord-relay.md` and the roadmap JSON.@@@ -5,6 +5,7 @@* Plain template strings, every value escaped; the editor's behaviour is public/js/space-editor.js.*/const sc = require('openvibe-shared/showcase');++const kit = require('openvibe-shared/kit');const { renderPage, escapeHtml: esc, asset } = require('../render/layout');const CAP_TEXT = {@@@ -23,7 +24,7 @@ const ago = (ms) => {function page(o) {return renderPage({title: o.title, description: o.description, canonicalPath: o.path, robots: o.robots || 'index,follow',-- active: 'spaces', styles: o.showcase ? [sc.STYLESHEET] : [], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,++ active: 'spaces', styles: [...(o.showcase ? [sc.STYLESHEET] : []), ...(o.styles || [])], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,});}@@@ -135,8 +136,9 @@ function minePage({ spaces }) {function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {const live = Boolean(space.site_id);++ const share = kit.share({ title: space.name, url: live && url ? url : null, text: space.description || '' });const actions = [-- live ? `<a class="sp-btn" href="${esc(url)}" rel="noopener"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Open</a>` : '',++ live ? `<a class="sp-btn" href="${esc(url)}" rel="noopener"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Open</a>` : '', share,owner ? `<a class="sp-btn ${live ? 'sp-btn-quiet' : ''}" href="/spaces/${esc(space.slug)}/edit"><i class="fa-solid fa-pen" aria-hidden="true"></i> Edit</a>` : '',viewer ? `<form method="post" action="/spaces/${esc(space.slug)}/fork"><input type="hidden" name="csrf" value="${esc(csrf)}"><button class="sp-btn sp-btn-quiet"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</button></form>`: `<a class="sp-btn sp-btn-quiet" href="${signInHref(`/spaces/${space.slug}`)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Sign in to fork</a>`,@@@ -153,7 +155,7 @@ function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {</div>`;return page({title: space.name, description: space.description || `${space.name}: a space on OpenVibe.Space by @${space.owner_username}.`, path: `/spaces/${space.slug}`,-- robots: live && space.visibility === 'public' ? 'index,follow' : 'noindex,nofollow', body,++ styles: ['kit.css'], robots: live && space.visibility === 'public' ? 'index,follow' : 'noindex,nofollow', body,jsonLd: live ? { '@context': 'https://schema.org', '@type': 'WebApplication', name: space.name, url, description: space.description || undefined, applicationCategory: 'WebApplication', author: { '@type': 'Person', name: space.owner_username } } : undefined,});}@@@ -177,6 +177,10 @@ function fakeAi() {assert.strictEqual(p.status, 200);assert.match(p.text, new RegExp(`<iframe src="${url}"`));assert.match(p.headers['content-security-policy'], /frame-src [^;]*https:\/\/\*\.openvibe\.website/);++ assert.strictEqual((p.text.match(/data-ov-share-sheet/g) || []).length, 1, 'one Share button');++ assert.ok(p.text.includes(`data-url="${url}"`), 'the published space address');++ assert.ok(p.text.includes('data-title="Guestbook"'), 'its title');++ assert.ok(p.text.includes('class="ovk-btn ovk-btn--quiet"') && p.text.includes('<span>Share</span>'), 'the shared kit button');});// A visitor signs in: the connect page, a code back to the space, traded from the space's origin.
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
## Unreleased
## Unreleased
+- **Share:** a space's page carries the network's one Share button (`data-ov-share-sheet`) in its actions; it shares the published space's own address (or the page's address for a draft), with its name and description. `test/spaces.test.js`.
- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.
- **Guest drafts** (openvibe.website, plan 5.9: "making websites and code without needing OpenVibe logins"): a visitor who is not signed in makes a draft on openvibe.space (`GET/POST /spaces/guest`) from a website template or a sentence, and edits it with a secret link (`/spaces/<id>/edit?key=…`; the cookie only remembers it, the link alone works). The draft has no owner and of the secret only its sha256 is kept. It is static: never published, no visitor's data, no server functions; in the preview the SDK answers "sign in to use this" where a visitor's account would be needed. A signed-in person who holds the secret claims it (`POST /spaces/<id>/claim`): it becomes theirs, the secret is cleared, and `space.app.claimed` is logged. The limits (per address, per draft, per service) and the seven-day expiry are in `server/hosting-config.js`; the abuse screen (`server/spaces/content-check.js`) refuses a phishing page and any form in a guest draft that posts to another origin.
- Usage readings (plan T5 step 14, Space slice): `server/billing.js` turns the bytes the local object store keeps (`space_blobs`, each blob once per project) into one `platform.usage-sample@1` `gb-month` reading per production project and owner per closed UTC day, queued in `billing_readings` (migration `0006_billing_readings.sql`) and relayed to OpenVibe.Billing's `billing.usage.record`. Off unless `SPACE_BILLING_INTERVAL_MS` > 0; with `SPACE_OBJECT_STORE=media` Space emits none, because Media bills the stored bytes.
- Usage readings (plan T5 step 14, Space slice): `server/billing.js` turns the bytes the local object store keeps (`space_blobs`, each blob once per project) into one `platform.usage-sample@1` `gb-month` reading per production project and owner per closed UTC day, queued in `billing_readings` (migration `0006_billing_readings.sql`) and relayed to OpenVibe.Billing's `billing.usage.record`. Off unless `SPACE_BILLING_INTERVAL_MS` > 0; with `SPACE_OBJECT_STORE=media` Space emits none, because Media bills the stored bytes.
- The forum moved back to OpenVibe.Community (owner decision, 2026-10-08), so Space keeps none of it. Removed: `server/forum/*`, `server/relay/*`, `server/vip/*`, `server/media/*`, `server/identity/*`, `server/{chat-rooms,votes,limits,actor-limits,events,events-consumer}.js`, `server/http/v1.js`, `server/render/{board,forum,markdown,jsonld,highlight}.js`, `public/js/space.js`, the forum suites and their fixtures, `docs/discord-relay.md` and the roadmap JSON.
- The forum moved back to OpenVibe.Community (owner decision, 2026-10-08), so Space keeps none of it. Removed: `server/forum/*`, `server/relay/*`, `server/vip/*`, `server/media/*`, `server/identity/*`, `server/{chat-rooms,votes,limits,actor-limits,events,events-consumer}.js`, `server/http/v1.js`, `server/render/{board,forum,markdown,jsonld,highlight}.js`, `public/js/space.js`, the forum suites and their fixtures, `docs/discord-relay.md` and the roadmap JSON.
@@ -5,6 +5,7 @@
@@ -5,6 +5,7 @@
* Plain template strings, every value escaped; the editor's behaviour is public/js/space-editor.js.
* Plain template strings, every value escaped; the editor's behaviour is public/js/space-editor.js.
*/
*/
const sc = require('openvibe-shared/showcase');
const sc = require('openvibe-shared/showcase');
+const kit = require('openvibe-shared/kit');
const { renderPage, escapeHtml: esc, asset } = require('../render/layout');
const { renderPage, escapeHtml: esc, asset } = require('../render/layout');
const CAP_TEXT = {
const CAP_TEXT = {
@@ -23,7 +24,7 @@ const ago = (ms) => {
@@ -23,7 +24,7 @@ const ago = (ms) => {
function page(o) {
function page(o) {
return renderPage({
return renderPage({
title: o.title, description: o.description, canonicalPath: o.path, robots: o.robots || 'index,follow',
title: o.title, description: o.description, canonicalPath: o.path, robots: o.robots || 'index,follow',
- active: 'spaces', styles: o.showcase ? [sc.STYLESHEET] : [], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,
+ active: 'spaces', styles: [...(o.showcase ? [sc.STYLESHEET] : []), ...(o.styles || [])], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,
});
});
}
}
@@ -135,8 +136,9 @@ function minePage({ spaces }) {
@@ -135,8 +136,9 @@ function minePage({ spaces }) {
function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
const live = Boolean(space.site_id);
const live = Boolean(space.site_id);
+ const share = kit.share({ title: space.name, url: live && url ? url : null, text: space.description || '' });
const actions = [
const actions = [
- live ? `<a class="sp-btn" href="${esc(url)}" rel="noopener"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Open</a>` : '',
+ live ? `<a class="sp-btn" href="${esc(url)}" rel="noopener"><i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i> Open</a>` : '', share,
owner ? `<a class="sp-btn ${live ? 'sp-btn-quiet' : ''}" href="/spaces/${esc(space.slug)}/edit"><i class="fa-solid fa-pen" aria-hidden="true"></i> Edit</a>` : '',
owner ? `<a class="sp-btn ${live ? 'sp-btn-quiet' : ''}" href="/spaces/${esc(space.slug)}/edit"><i class="fa-solid fa-pen" aria-hidden="true"></i> Edit</a>` : '',
viewer ? `<form method="post" action="/spaces/${esc(space.slug)}/fork"><input type="hidden" name="csrf" value="${esc(csrf)}"><button class="sp-btn sp-btn-quiet"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</button></form>`
viewer ? `<form method="post" action="/spaces/${esc(space.slug)}/fork"><input type="hidden" name="csrf" value="${esc(csrf)}"><button class="sp-btn sp-btn-quiet"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Fork</button></form>`
: `<a class="sp-btn sp-btn-quiet" href="${signInHref(`/spaces/${space.slug}`)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Sign in to fork</a>`,
: `<a class="sp-btn sp-btn-quiet" href="${signInHref(`/spaces/${space.slug}`)}"><i class="fa-solid fa-code-fork" aria-hidden="true"></i> Sign in to fork</a>`,
@@ -153,7 +155,7 @@ function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
@@ -153,7 +155,7 @@ function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
</div>`;
</div>`;
return page({
return page({
title: space.name, description: space.description || `${space.name}: a space on OpenVibe.Space by @${space.owner_username}.`, path: `/spaces/${space.slug}`,
title: space.name, description: space.description || `${space.name}: a space on OpenVibe.Space by @${space.owner_username}.`, path: `/spaces/${space.slug}`,
- robots: live && space.visibility === 'public' ? 'index,follow' : 'noindex,nofollow', body,
+ styles: ['kit.css'], robots: live && space.visibility === 'public' ? 'index,follow' : 'noindex,nofollow', body,
jsonLd: live ? { '@context': 'https://schema.org', '@type': 'WebApplication', name: space.name, url, description: space.description || undefined, applicationCategory: 'WebApplication', author: { '@type': 'Person', name: space.owner_username } } : undefined,
jsonLd: live ? { '@context': 'https://schema.org', '@type': 'WebApplication', name: space.name, url, description: space.description || undefined, applicationCategory: 'WebApplication', author: { '@type': 'Person', name: space.owner_username } } : undefined,
});
});
}
}
@@ -177,6 +177,10 @@ function fakeAi() {
@@ -177,6 +177,10 @@ function fakeAi() {
assert.strictEqual(p.status, 200);
assert.strictEqual(p.status, 200);
assert.match(p.text, new RegExp(`<iframe src="${url}"`));
assert.match(p.text, new RegExp(`<iframe src="${url}"`));
assert.match(p.headers['content-security-policy'], /frame-src [^;]*https:\/\/\*\.openvibe\.website/);
assert.match(p.headers['content-security-policy'], /frame-src [^;]*https:\/\/\*\.openvibe\.website/);
+ assert.strictEqual((p.text.match(/data-ov-share-sheet/g) || []).length, 1, 'one Share button');
+ assert.ok(p.text.includes(`data-url="${url}"`), 'the published space address');
+ assert.ok(p.text.includes('data-title="Guestbook"'), 'its title');
+ assert.ok(p.text.includes('class="ovk-btn ovk-btn--quiet"') && p.text.includes('<span>Share</span>'), 'the shared kit button');
});
});
// A visitor signs in: the connect page, a code back to the space, traded from the space's origin.
// A visitor signs in: the connect page, a code back to the space, traded from the space's origin.