openvibers/openvibe.ai Commits
Sitemaps: every entry carries an honest lastmod (item changes, release time for static pages) (#58)
Changes
@@@ -449,6 +449,8 @@ The old `ai.openvibe.network` address was retired on 2026-10-10. The console nee`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them itanswers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.++Sitemap entries use the release commit for home and the newest recorded stats day for `/stats` when available.++## Not done yet- Live's VOD and clip overviews calling `media.analyze` instead of extracting frames and transcribing@@@ -22,6 +22,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keysapp.disable('x-powered-by');app.set('trust proxy', 'loopback');const release = createRelease({ service: 'ai', root: path.join(__dirname, '..') });++ const releaseLastmod = release.full().released_at === release.full().booted_at ? undefined : release.full().released_at;// HTTP golden signals by route template, process metrics, release_info and the AI gauges;// GET /metrics answers direct loopback callers only (Track O).const metrics = instrument(app, { service: 'ai', release: release.release });@@@ -123,8 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keysapp.get('/robots.txt', (_req, res) => {res.type('text/plain').send(`User-agent: *\nAllow: /$\nAllow: /stats\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);});-- app.get('/sitemap.xml', (_req, res) => {-- res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${['/', '/stats'].map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);++ app.get('/sitemap.xml', async (_req, res, next) => {++ try {++ const from = new Date(clock.now() - STATS_DAYS * 86400000).toISOString().slice(0, 10);++ const stats = await db.prepare('SELECT MAX(day) AS lastmod FROM provider_stats_daily WHERE day >= ?').get(from);++ const statsLastmod = [stats && stats.lastmod ? new Date(stats.lastmod).toISOString() : null, releaseLastmod].filter(Boolean).sort().pop();++ const entries = [['/', releaseLastmod], ['/stats', statsLastmod]];++ res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map(([p, lastmod]) => ` <url><loc>${config.baseUrl}${p}</loc>${lastmod ? `<lastmod>${lastmod.slice(0, 10)}</lastmod>` : ''}</url>`).join('\n')}\n</urlset>\n`);++ } catch (e) { next(e); }});app.get('/llms.txt', (_req, res) => {res.setHeader('Cache-Control', 'public, max-age=3600');@@@ -4,6 +4,17 @@// /shared, and answers robots.txt, sitemap.xml and llms.txt. The vhosts: the developer-app routes are the only// API the public vhost proxies, and the old ai.openvibe.network address is retired (no vhost of its own).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, request, suite } = require('./helpers');@@@ -61,6 +72,7 @@ t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () =assert.strictEqual(map.status, 200);assert.match(map.headers.get('content-type'), /xml/);assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`]);++ assert.strictEqual(assertSitemapLastmods(map.text).length, 2);const llms = await request(h.base, 'GET', '/llms.txt');assert.strictEqual(llms.status, 200);assert.match(llms.headers.get('content-type'), /^text\/plain/);
@@ -449,6 +449,8 @@ The old `ai.openvibe.network` address was retired on 2026-10-10. The console nee
@@ -449,6 +449,8 @@ The old `ai.openvibe.network` address was retired on 2026-10-10. The console nee
`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them it
`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them it
answers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.
answers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.
+Sitemap entries use the release commit for home and the newest recorded stats day for `/stats` when available.
+
## Not done yet
## Not done yet
- Live's VOD and clip overviews calling `media.analyze` instead of extracting frames and transcribing
- Live's VOD and clip overviews calling `media.analyze` instead of extracting frames and transcribing
@@ -22,6 +22,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -22,6 +22,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
app.disable('x-powered-by');
app.disable('x-powered-by');
app.set('trust proxy', 'loopback');
app.set('trust proxy', 'loopback');
const release = createRelease({ service: 'ai', root: path.join(__dirname, '..') });
const release = createRelease({ service: 'ai', root: path.join(__dirname, '..') });
+ const releaseLastmod = release.full().released_at === release.full().booted_at ? undefined : release.full().released_at;
// HTTP golden signals by route template, process metrics, release_info and the AI gauges;
// HTTP golden signals by route template, process metrics, release_info and the AI gauges;
// GET /metrics answers direct loopback callers only (Track O).
// GET /metrics answers direct loopback callers only (Track O).
const metrics = instrument(app, { service: 'ai', release: release.release });
const metrics = instrument(app, { service: 'ai', release: release.release });
@@ -123,8 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -123,8 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
app.get('/robots.txt', (_req, res) => {
app.get('/robots.txt', (_req, res) => {
res.type('text/plain').send(`User-agent: *\nAllow: /$\nAllow: /stats\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
res.type('text/plain').send(`User-agent: *\nAllow: /$\nAllow: /stats\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
});
});
- app.get('/sitemap.xml', (_req, res) => {
- res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${['/', '/stats'].map((p) => ` <url><loc>${config.baseUrl}${p}</loc></url>`).join('\n')}\n</urlset>\n`);
+ app.get('/sitemap.xml', async (_req, res, next) => {
+ try {
+ const from = new Date(clock.now() - STATS_DAYS * 86400000).toISOString().slice(0, 10);
+ const stats = await db.prepare('SELECT MAX(day) AS lastmod FROM provider_stats_daily WHERE day >= ?').get(from);
+ const statsLastmod = [stats && stats.lastmod ? new Date(stats.lastmod).toISOString() : null, releaseLastmod].filter(Boolean).sort().pop();
+ const entries = [['/', releaseLastmod], ['/stats', statsLastmod]];
+ res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map(([p, lastmod]) => ` <url><loc>${config.baseUrl}${p}</loc>${lastmod ? `<lastmod>${lastmod.slice(0, 10)}</lastmod>` : ''}</url>`).join('\n')}\n</urlset>\n`);
+ } catch (e) { next(e); }
});
});
app.get('/llms.txt', (_req, res) => {
app.get('/llms.txt', (_req, res) => {
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Cache-Control', 'public, max-age=3600');
@@ -4,6 +4,17 @@
@@ -4,6 +4,17 @@
// /shared, and answers robots.txt, sitemap.xml and llms.txt. The vhosts: the developer-app routes are the only
// /shared, and answers robots.txt, sitemap.xml and llms.txt. The vhosts: the developer-app routes are the only
// API the public vhost proxies, and the old ai.openvibe.network address is retired (no vhost of its own).
// API the public vhost proxies, and the old ai.openvibe.network address is retired (no vhost of its own).
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, request, suite } = require('./helpers');
const { boot, request, suite } = require('./helpers');
@@ -61,6 +72,7 @@ t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () =
@@ -61,6 +72,7 @@ t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () =
assert.strictEqual(map.status, 200);
assert.strictEqual(map.status, 200);
assert.match(map.headers.get('content-type'), /xml/);
assert.match(map.headers.get('content-type'), /xml/);
assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`]);
assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`]);
+ assert.strictEqual(assertSitemapLastmods(map.text).length, 2);
const llms = await request(h.base, 'GET', '/llms.txt');
const llms = await request(h.base, 'GET', '/llms.txt');
assert.strictEqual(llms.status, 200);
assert.strictEqual(llms.status, 200);
assert.match(llms.headers.get('content-type'), /^text\/plain/);
assert.match(llms.headers.get('content-type'), /^text\/plain/);