openvibers/openvibe.bot Commits
Sitemaps: every entry carries an honest lastmod (item changes, release time for static pages) (#84)
Changes
@@@ -23,6 +23,8 @@ OpenVibe.Bot pairs a robot's machines, keeps their state and gates every operato- The robot's streaming and recording toggles, off by default: `media` records live sessions to OpenVibe.Media and `live` shows them on the owner's OpenVibe.Live channel. They live only on the robot's OpenRestream stream (`recording_mode`, `mirror_to_live`); Bot stores no copy and the owner, not an operator, flips them (`GET`/`POST /api/v1/robots/:id/streaming`).- Dispatching `platform.job@1` jobs to a paired Node over `/device` (`server/jobs/index.js` over `server/jobs/dispatch.js`; the internal `bot.job.dispatch` API is `POST /api/v1/jobs`, `POST /jobs/:id/cancel`, `GET /jobs/:id`) and metering them per wall-clock second (`run_jobs`, `run_usage_outbox`; docs/protocol.md §1.2).++Sitemap entries use the release commit time for these code-backed pages.++## Does not own- Identity, subjects and tokens (OpenVibe.Network) and the robot's record on Network.@@@ -134,7 +134,7 @@ function createApp(opts = {}) {{ const legal = require('openvibe-shared/legal');app.get(legal.PATHS, (req, res, next) => { res.setHeader('Content-Security-Policy', HOME_CSP); next(); }, legal.handler({ id: 'bot', service: 'bot', host: 'openvibe.bot', name: 'OpenVibe.Bot', profile: 'games' })); }app.use(viewerMiddleware(userAuth));-- app.use(createWebRoutes(config, { domain, sim, onvif, limits, identity, hub, log }));++ app.use(createWebRoutes(config, { domain, sim, onvif, limits, identity, hub, log, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));app.use((req, res) => {if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return http.sendProblem(res, 404, 'not_found', { ctx: req.ov });@@@ -104,7 +104,7 @@ const PANEL_CSP = ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`const embedCsp = (config) => ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`, `connect-src 'self' ${PANEL_CONNECT_SRC}`,`frame-ancestors ${frameAncestors(config)}`, "object-src 'none'", "base-uri 'self'"].join('; ');--function createWebRoutes(config, { domain = null, sim = null, onvif = null, limits = null, identity = null, hub = null, log = console } = {}) {++function createWebRoutes(config, { domain = null, sim = null, onvif = null, limits = null, identity = null, hub = null, log = console, releaseLastmod } = {}) {const r = express.Router();// The public front page is crawlable; sign-in, the API and every signed-in or per-robot page are not.r.get('/robots.txt', (req, res) => res.type('text/plain').send(['User-agent: *', 'Allow: /$', 'Disallow: /auth/', 'Disallow: /api/',@@@ -113,7 +113,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limir.get('/install', (req, res) => res.redirect(302, config.installer.sourceUrl));// The sitemap lists every public GET page (openvibe-shared/seo); robots.txt already names it.r.get('/sitemap.xml', (req, res) => {-- res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}` }))));++ res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}`, lastmod: releaseLastmod }))));});// /llms.txt and /llms-full.txt (llmstxt.org, openvibe-shared/seo): the public pages as a plain-text map for// language-model crawlers. Only the pages above; the signed-in pages, the API and /internal are not listed.@@@ -3,6 +3,17 @@// the web app manifest and the browser-facing 404. Bot serves them itself now, and the nginx vhost no longer// points at a Sites root.const assert = require('assert');++++function assertSitemapLastmods(xml, tag = 'url') {++ const entries = [...xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, 'g'))];++ for (const [, body] of entries) {++ const match = /<lastmod>(\d{4}-\d{2}-\d{2})<\/lastmod>/.exec(body);++ assert.ok(match, body);++ assert.strictEqual(new Date(match[1]).toISOString().slice(0, 10), match[1]);++ }++ return entries;++}++const fs = require('fs');const path = require('path');const { boot, check, done } = require('./helpers/app');@@@ -34,6 +45,7 @@ const { boot, check, done } = require('./helpers/app');for (const p of ['/', '/install', '/terms', '/privacy', '/dmca']) {assert.ok(xml.includes(`<loc>http://bot.test${p}</loc>`), `sitemap lists ${p}`);}++ assert.ok(assertSitemapLastmods(xml).length >= 5);});await check('GET /favicon.ico: the app icon as SVG (browsers ask for it even on a 404 page)', async () => {
@@ -23,6 +23,8 @@ OpenVibe.Bot pairs a robot's machines, keeps their state and gates every operato
@@ -23,6 +23,8 @@ OpenVibe.Bot pairs a robot's machines, keeps their state and gates every operato
- The robot's streaming and recording toggles, off by default: `media` records live sessions to OpenVibe.Media and `live` shows them on the owner's OpenVibe.Live channel. They live only on the robot's OpenRestream stream (`recording_mode`, `mirror_to_live`); Bot stores no copy and the owner, not an operator, flips them (`GET`/`POST /api/v1/robots/:id/streaming`).
- The robot's streaming and recording toggles, off by default: `media` records live sessions to OpenVibe.Media and `live` shows them on the owner's OpenVibe.Live channel. They live only on the robot's OpenRestream stream (`recording_mode`, `mirror_to_live`); Bot stores no copy and the owner, not an operator, flips them (`GET`/`POST /api/v1/robots/:id/streaming`).
- Dispatching `platform.job@1` jobs to a paired Node over `/device` (`server/jobs/index.js` over `server/jobs/dispatch.js`; the internal `bot.job.dispatch` API is `POST /api/v1/jobs`, `POST /jobs/:id/cancel`, `GET /jobs/:id`) and metering them per wall-clock second (`run_jobs`, `run_usage_outbox`; docs/protocol.md §1.2).
- Dispatching `platform.job@1` jobs to a paired Node over `/device` (`server/jobs/index.js` over `server/jobs/dispatch.js`; the internal `bot.job.dispatch` API is `POST /api/v1/jobs`, `POST /jobs/:id/cancel`, `GET /jobs/:id`) and metering them per wall-clock second (`run_jobs`, `run_usage_outbox`; docs/protocol.md §1.2).
+Sitemap entries use the release commit time for these code-backed pages.
+
## Does not own
## Does not own
- Identity, subjects and tokens (OpenVibe.Network) and the robot's record on Network.
- Identity, subjects and tokens (OpenVibe.Network) and the robot's record on Network.
@@ -134,7 +134,7 @@ function createApp(opts = {}) {
@@ -134,7 +134,7 @@ function createApp(opts = {}) {
{ const legal = require('openvibe-shared/legal');
{ const legal = require('openvibe-shared/legal');
app.get(legal.PATHS, (req, res, next) => { res.setHeader('Content-Security-Policy', HOME_CSP); next(); }, legal.handler({ id: 'bot', service: 'bot', host: 'openvibe.bot', name: 'OpenVibe.Bot', profile: 'games' })); }
app.get(legal.PATHS, (req, res, next) => { res.setHeader('Content-Security-Policy', HOME_CSP); next(); }, legal.handler({ id: 'bot', service: 'bot', host: 'openvibe.bot', name: 'OpenVibe.Bot', profile: 'games' })); }
app.use(viewerMiddleware(userAuth));
app.use(viewerMiddleware(userAuth));
- app.use(createWebRoutes(config, { domain, sim, onvif, limits, identity, hub, log }));
+ app.use(createWebRoutes(config, { domain, sim, onvif, limits, identity, hub, log, releaseLastmod: release.full().released_at === release.full().booted_at ? undefined : release.full().released_at }));
app.use((req, res) => {
app.use((req, res) => {
if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return http.sendProblem(res, 404, 'not_found', { ctx: req.ov });
if (req.path.startsWith('/api/') || req.path.startsWith('/internal/')) return http.sendProblem(res, 404, 'not_found', { ctx: req.ov });
@@ -104,7 +104,7 @@ const PANEL_CSP = ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`
@@ -104,7 +104,7 @@ const PANEL_CSP = ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`
const embedCsp = (config) => ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`, `connect-src 'self' ${PANEL_CONNECT_SRC}`,
const embedCsp = (config) => ["default-src 'self'", `script-src 'self' ${PANEL_SCRIPT_SRC}`, `connect-src 'self' ${PANEL_CONNECT_SRC}`,
`frame-ancestors ${frameAncestors(config)}`, "object-src 'none'", "base-uri 'self'"].join('; ');
`frame-ancestors ${frameAncestors(config)}`, "object-src 'none'", "base-uri 'self'"].join('; ');
-function createWebRoutes(config, { domain = null, sim = null, onvif = null, limits = null, identity = null, hub = null, log = console } = {}) {
+function createWebRoutes(config, { domain = null, sim = null, onvif = null, limits = null, identity = null, hub = null, log = console, releaseLastmod } = {}) {
const r = express.Router();
const r = express.Router();
// The public front page is crawlable; sign-in, the API and every signed-in or per-robot page are not.
// The public front page is crawlable; sign-in, the API and every signed-in or per-robot page are not.
r.get('/robots.txt', (req, res) => res.type('text/plain').send(['User-agent: *', 'Allow: /$', 'Disallow: /auth/', 'Disallow: /api/',
r.get('/robots.txt', (req, res) => res.type('text/plain').send(['User-agent: *', 'Allow: /$', 'Disallow: /auth/', 'Disallow: /api/',
@@ -113,7 +113,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
@@ -113,7 +113,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
r.get('/install', (req, res) => res.redirect(302, config.installer.sourceUrl));
r.get('/install', (req, res) => res.redirect(302, config.installer.sourceUrl));
// The sitemap lists every public GET page (openvibe-shared/seo); robots.txt already names it.
// The sitemap lists every public GET page (openvibe-shared/seo); robots.txt already names it.
r.get('/sitemap.xml', (req, res) => {
r.get('/sitemap.xml', (req, res) => {
- res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}` }))));
+ res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}`, lastmod: releaseLastmod }))));
});
});
// /llms.txt and /llms-full.txt (llmstxt.org, openvibe-shared/seo): the public pages as a plain-text map for
// /llms.txt and /llms-full.txt (llmstxt.org, openvibe-shared/seo): the public pages as a plain-text map for
// language-model crawlers. Only the pages above; the signed-in pages, the API and /internal are not listed.
// language-model crawlers. Only the pages above; the signed-in pages, the API and /internal are not listed.
@@ -3,6 +3,17 @@
@@ -3,6 +3,17 @@
// the web app manifest and the browser-facing 404. Bot serves them itself now, and the nginx vhost no longer
// the web app manifest and the browser-facing 404. Bot serves them itself now, and the nginx vhost no longer
// points at a Sites root.
// points at a Sites root.
const assert = require('assert');
const assert = require('assert');
+
+function assertSitemapLastmods(xml, tag = 'url') {
+ const entries = [...xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, 'g'))];
+ for (const [, body] of entries) {
+ const match = /<lastmod>(\d{4}-\d{2}-\d{2})<\/lastmod>/.exec(body);
+ assert.ok(match, body);
+ assert.strictEqual(new Date(match[1]).toISOString().slice(0, 10), match[1]);
+ }
+ return entries;
+}
+
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const { boot, check, done } = require('./helpers/app');
const { boot, check, done } = require('./helpers/app');
@@ -34,6 +45,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -34,6 +45,7 @@ const { boot, check, done } = require('./helpers/app');
for (const p of ['/', '/install', '/terms', '/privacy', '/dmca']) {
for (const p of ['/', '/install', '/terms', '/privacy', '/dmca']) {
assert.ok(xml.includes(`<loc>http://bot.test${p}</loc>`), `sitemap lists ${p}`);
assert.ok(xml.includes(`<loc>http://bot.test${p}</loc>`), `sitemap lists ${p}`);
}
}
+ assert.ok(assertSitemapLastmods(xml).length >= 5);
});
});
await check('GET /favicon.ico: the app icon as SVG (browsers ask for it even on a 404 page)', async () => {
await check('GET /favicon.ico: the app icon as SVG (browsers ask for it even on a 404 page)', async () => {