openvibers/openvibe.ai Commits
AI: /updates — what shipped on ai.openvibe.services (the shared update log) (#79)
Changes
@@@ -149,6 +149,7 @@ server {}location = /robots.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }++ location = /updates { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }location = /stats { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }location = /sitemap.xml { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }location = /llms.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }@@@ -124,14 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys// Discovery for the public home (ai.openvibe.services): the home and the price page are for search engines// and assistants; the console, its sign-in and the API are not.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\nAllow: /updates\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\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]];++ const entries = [['/', releaseLastmod], ['/stats', statsLastmod], ['/updates', releaseLastmod]];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); }});@@@ -143,6 +143,12 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys// GET /: the home page for a browser (server/home.js), the API index for curl and API clients (`*/*`, no// Accept, or any non-HTML Accept). Vary: Accept so a shared cache never serves one to the other; the home is// private because Cloudflare caches by URL and ignores Vary: Accept. The home carries its own CSP.++ // GET /updates: what shipped on this site (the shared update log).++ app.get('/updates', (req, res) => {++ res.type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'public, max-age=300')++ .send(home.renderUpdates({ siteUrl: config.baseUrl }));++ });++app.get('/', (req, res) => {res.vary('Accept');if (req.accepts(['text/plain', 'text/html']) === 'text/html') {@@@ -11,6 +11,7 @@* llmsTxt({ siteUrl }) /llms.txt, the same facts as plain text*/const shell = require('openvibe-shared/shell');++const frame = require('openvibe-shared/frame');const showcase = require('openvibe-shared/showcase');const ovServe = require('openvibe-shared/serve');const appIcon = require('openvibe-shared/app-icon');@@@ -115,10 +116,33 @@ function sections({ siteUrl }) {});}++const NAV_LINKS = [{ label: 'Prices', href: '/stats' }, { label: 'Guide', href: GUIDE }];++const FOOTER = { service: 'ai', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };++++/** /updates: what shipped on this site (the network's shared update log, openvibe-shared/frame updatesBody). */++function renderUpdates({ siteUrl }) {++ return shell.page({++ name: SITE_NAME, service: 'ai', lang: 'en',++ title: `What shipped on ${SITE_NAME}`,++ siteName: SITE_NAME,++ description: `Every change deployed to ${SITE_NAME}, newest first.`,++ canonical: `${siteUrl}/updates`,++ robots: 'index, follow',++ navbar: { service: 'ai', apiBase: NETWORK_URL, links: NAV_LINKS, history: { type: 'page', title: SITE_NAME } },++ footer: FOOTER, home: '/', navLinks: NAV_LINKS,++ head: appIcon.headTags({ site: 'ai' }),++ body: `<div id="navbar-mount"></div>++<main id="main" class="page">++${frame.updatesBody({ service: 'ai', siteName: SITE_NAME })}++</main>++<script src="${ovServe.url('shipped.js')}" defer></script>`,++ });++}++function renderHome({ siteUrl }) {-- const navLinks = [{ label: 'Prices', href: '/stats' }, { label: 'Guide', href: GUIDE }];++ const navLinks = NAV_LINKS;const nav = { service: 'ai', apiBase: NETWORK_URL, links: navLinks, history: { type: 'page', title: SITE_NAME } };-- const footer = { service: 'ai', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };++ const footer = FOOTER;return shell.page({name: SITE_NAME, service: 'ai', lang: 'en',title: `${SITE_NAME}: many models, one API, every answer explained`,@@@ -172,4 +196,4 @@ function renderNotFound() {'<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="/stats">Prices and latency</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');}--module.exports = { renderHome, llmsTxt, HOME_CSP, SITE_NAME, renderNotFound };++module.exports = { renderHome, renderUpdates, llmsTxt, HOME_CSP, SITE_NAME, renderNotFound };@@@ -67,15 +67,26 @@ t.test('the home links the pinned Frame stylesheet, and /shared serves it', asynassert.match(css.headers.get('cache-control'), /immutable/);});++t.test('/updates: what shipped on this site, the shared update log, with one heading and the footer link', async () => {++ const r = await request(h.base, 'GET', '/updates', { headers: { Accept: 'text/html' } });++ assert.strictEqual(r.status, 200);++ assert.match(r.headers.get('content-type'), /^text\/html/);++ assert.ok(r.text.includes('<h1>What shipped on OpenVibe.AI</h1>') && r.text.includes('data-ov-shipped="log" data-service="ai"'));++ assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1);++ assert.match(r.text, /<script src="\/shared\/shipped\.js\?v=[0-9a-f]+" defer><\/script>/);++ assert.match(r.text, new RegExp(`<link rel="canonical" href="${SITE.replace(/[.]/g, '\\.')}/updates">`));++ assert.ok(r.headers.get('content-security-policy').includes("connect-src 'self' https://openvibe.network"), 'the log reads the network changelog');++});++t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () => {const robots = await request(h.base, 'GET', '/robots.txt');assert.strictEqual(robots.status, 200);-- for (const line of ['Allow: /$', 'Allow: /stats', 'Disallow: /console', 'Disallow: /auth/', 'Disallow: /api/', `Sitemap: ${SITE}/sitemap.xml`]) assert.ok(robots.text.includes(line), line);++ for (const line of ['Allow: /$', 'Allow: /stats', 'Allow: /updates', 'Disallow: /console', 'Disallow: /auth/', 'Disallow: /api/', `Sitemap: ${SITE}/sitemap.xml`]) assert.ok(robots.text.includes(line), line);const map = await request(h.base, 'GET', '/sitemap.xml');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);++ assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`, `${SITE}/updates`]);++ assert.strictEqual(assertSitemapLastmods(map.text).length, 3);const llms = await request(h.base, 'GET', '/llms.txt');assert.strictEqual(llms.status, 200);assert.match(llms.headers.get('content-type'), /^text\/plain/);@@@ -87,7 +98,7 @@ const active = (file) => fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nt.test('the public vhost proxies the home, its files and exactly the developer-app API routes', () => {const conf = active('ai.openvibe.services.conf');-- for (const loc of ['location = / {', 'location = /stats', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);++ for (const loc of ['location = / {', 'location = /stats', 'location = /updates', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);const ops = conf.indexOf('location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {');const run = conf.indexOf('location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {');const completions = conf.indexOf('location = /v1/chat/completions {');
@@ -149,6 +149,7 @@ server {
@@ -149,6 +149,7 @@ server {
}
}
location = /robots.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /robots.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
+ location = /updates { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /stats { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /stats { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /sitemap.xml { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /sitemap.xml { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /llms.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
location = /llms.txt { proxy_pass http://127.0.0.1:4700; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header CF-Connecting-IP $remote_addr; }
@@ -124,14 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -124,14 +124,14 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
// Discovery for the public home (ai.openvibe.services): the home and the price page are for search engines
// Discovery for the public home (ai.openvibe.services): the home and the price page are for search engines
// and assistants; the console, its sign-in and the API are not.
// and assistants; the console, its sign-in and the API are not.
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\nAllow: /updates\nDisallow: /console\nDisallow: /auth/\nDisallow: /api/\n\nSitemap: ${config.baseUrl}/sitemap.xml\n`);
});
});
app.get('/sitemap.xml', async (_req, res, next) => {
app.get('/sitemap.xml', async (_req, res, next) => {
try {
try {
const from = new Date(clock.now() - STATS_DAYS * 86400000).toISOString().slice(0, 10);
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 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 statsLastmod = [stats && stats.lastmod ? new Date(stats.lastmod).toISOString() : null, releaseLastmod].filter(Boolean).sort().pop();
- const entries = [['/', releaseLastmod], ['/stats', statsLastmod]];
+ const entries = [['/', releaseLastmod], ['/stats', statsLastmod], ['/updates', releaseLastmod]];
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`);
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); }
} catch (e) { next(e); }
});
});
@@ -143,6 +143,12 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -143,6 +143,12 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
// GET /: the home page for a browser (server/home.js), the API index for curl and API clients (`*/*`, no
// GET /: the home page for a browser (server/home.js), the API index for curl and API clients (`*/*`, no
// Accept, or any non-HTML Accept). Vary: Accept so a shared cache never serves one to the other; the home is
// Accept, or any non-HTML Accept). Vary: Accept so a shared cache never serves one to the other; the home is
// private because Cloudflare caches by URL and ignores Vary: Accept. The home carries its own CSP.
// private because Cloudflare caches by URL and ignores Vary: Accept. The home carries its own CSP.
+ // GET /updates: what shipped on this site (the shared update log).
+ app.get('/updates', (req, res) => {
+ res.type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'public, max-age=300')
+ .send(home.renderUpdates({ siteUrl: config.baseUrl }));
+ });
+
app.get('/', (req, res) => {
app.get('/', (req, res) => {
res.vary('Accept');
res.vary('Accept');
if (req.accepts(['text/plain', 'text/html']) === 'text/html') {
if (req.accepts(['text/plain', 'text/html']) === 'text/html') {
@@ -11,6 +11,7 @@
@@ -11,6 +11,7 @@
* llmsTxt({ siteUrl }) /llms.txt, the same facts as plain text
* llmsTxt({ siteUrl }) /llms.txt, the same facts as plain text
*/
*/
const shell = require('openvibe-shared/shell');
const shell = require('openvibe-shared/shell');
+const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const appIcon = require('openvibe-shared/app-icon');
const appIcon = require('openvibe-shared/app-icon');
@@ -115,10 +116,33 @@ function sections({ siteUrl }) {
@@ -115,10 +116,33 @@ function sections({ siteUrl }) {
});
});
}
}
+const NAV_LINKS = [{ label: 'Prices', href: '/stats' }, { label: 'Guide', href: GUIDE }];
+const FOOTER = { service: 'ai', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };
+
+/** /updates: what shipped on this site (the network's shared update log, openvibe-shared/frame updatesBody). */
+function renderUpdates({ siteUrl }) {
+ return shell.page({
+ name: SITE_NAME, service: 'ai', lang: 'en',
+ title: `What shipped on ${SITE_NAME}`,
+ siteName: SITE_NAME,
+ description: `Every change deployed to ${SITE_NAME}, newest first.`,
+ canonical: `${siteUrl}/updates`,
+ robots: 'index, follow',
+ navbar: { service: 'ai', apiBase: NETWORK_URL, links: NAV_LINKS, history: { type: 'page', title: SITE_NAME } },
+ footer: FOOTER, home: '/', navLinks: NAV_LINKS,
+ head: appIcon.headTags({ site: 'ai' }),
+ body: `<div id="navbar-mount"></div>
+<main id="main" class="page">
+${frame.updatesBody({ service: 'ai', siteName: SITE_NAME })}
+</main>
+<script src="${ovServe.url('shipped.js')}" defer></script>`,
+ });
+}
+
function renderHome({ siteUrl }) {
function renderHome({ siteUrl }) {
- const navLinks = [{ label: 'Prices', href: '/stats' }, { label: 'Guide', href: GUIDE }];
+ const navLinks = NAV_LINKS;
const nav = { service: 'ai', apiBase: NETWORK_URL, links: navLinks, history: { type: 'page', title: SITE_NAME } };
const nav = { service: 'ai', apiBase: NETWORK_URL, links: navLinks, history: { type: 'page', title: SITE_NAME } };
- const footer = { service: 'ai', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };
+ const footer = FOOTER;
return shell.page({
return shell.page({
name: SITE_NAME, service: 'ai', lang: 'en',
name: SITE_NAME, service: 'ai', lang: 'en',
title: `${SITE_NAME}: many models, one API, every answer explained`,
title: `${SITE_NAME}: many models, one API, every answer explained`,
@@ -172,4 +196,4 @@ function renderNotFound() {
@@ -172,4 +196,4 @@ function renderNotFound() {
'<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="/stats">Prices and latency</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
'<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="/stats">Prices and latency</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
}
}
-module.exports = { renderHome, llmsTxt, HOME_CSP, SITE_NAME, renderNotFound };
+module.exports = { renderHome, renderUpdates, llmsTxt, HOME_CSP, SITE_NAME, renderNotFound };
@@ -67,15 +67,26 @@ t.test('the home links the pinned Frame stylesheet, and /shared serves it', asyn
@@ -67,15 +67,26 @@ t.test('the home links the pinned Frame stylesheet, and /shared serves it', asyn
assert.match(css.headers.get('cache-control'), /immutable/);
assert.match(css.headers.get('cache-control'), /immutable/);
});
});
+t.test('/updates: what shipped on this site, the shared update log, with one heading and the footer link', async () => {
+ const r = await request(h.base, 'GET', '/updates', { headers: { Accept: 'text/html' } });
+ assert.strictEqual(r.status, 200);
+ assert.match(r.headers.get('content-type'), /^text\/html/);
+ assert.ok(r.text.includes('<h1>What shipped on OpenVibe.AI</h1>') && r.text.includes('data-ov-shipped="log" data-service="ai"'));
+ assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1);
+ assert.match(r.text, /<script src="\/shared\/shipped\.js\?v=[0-9a-f]+" defer><\/script>/);
+ assert.match(r.text, new RegExp(`<link rel="canonical" href="${SITE.replace(/[.]/g, '\\.')}/updates">`));
+ assert.ok(r.headers.get('content-security-policy').includes("connect-src 'self' https://openvibe.network"), 'the log reads the network changelog');
+});
+
t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () => {
t.test('robots.txt, sitemap.xml and llms.txt name the public origin', async () => {
const robots = await request(h.base, 'GET', '/robots.txt');
const robots = await request(h.base, 'GET', '/robots.txt');
assert.strictEqual(robots.status, 200);
assert.strictEqual(robots.status, 200);
- for (const line of ['Allow: /$', 'Allow: /stats', 'Disallow: /console', 'Disallow: /auth/', 'Disallow: /api/', `Sitemap: ${SITE}/sitemap.xml`]) assert.ok(robots.text.includes(line), line);
+ for (const line of ['Allow: /$', 'Allow: /stats', 'Allow: /updates', 'Disallow: /console', 'Disallow: /auth/', 'Disallow: /api/', `Sitemap: ${SITE}/sitemap.xml`]) assert.ok(robots.text.includes(line), line);
const map = await request(h.base, 'GET', '/sitemap.xml');
const map = await request(h.base, 'GET', '/sitemap.xml');
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.strictEqual(assertSitemapLastmods(map.text).length, 2);
+ assert.deepStrictEqual([...map.text.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/stats`, `${SITE}/updates`]);
+ assert.strictEqual(assertSitemapLastmods(map.text).length, 3);
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/);
@@ -87,7 +98,7 @@ const active = (file) => fs.readFileSync(path.join(__dirname, '..', 'deploy', 'n
@@ -87,7 +98,7 @@ const active = (file) => fs.readFileSync(path.join(__dirname, '..', 'deploy', 'n
t.test('the public vhost proxies the home, its files and exactly the developer-app API routes', () => {
t.test('the public vhost proxies the home, its files and exactly the developer-app API routes', () => {
const conf = active('ai.openvibe.services.conf');
const conf = active('ai.openvibe.services.conf');
- for (const loc of ['location = / {', 'location = /stats', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);
+ for (const loc of ['location = / {', 'location = /stats', 'location = /updates', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);
const ops = conf.indexOf('location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {');
const ops = conf.indexOf('location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {');
const run = conf.indexOf('location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {');
const run = conf.indexOf('location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {');
const completions = conf.indexOf('location = /v1/chat/completions {');
const completions = conf.indexOf('location = /v1/chat/completions {');