openvibers/openvibe.run Commits
Run's home leads with running jobs on your own machine (pair it, start a job from Codes or the API), says what each runtime class does today, puts the API in one section with an SDK sample, and answers the catalog's questions (FAQPage) (#62)
Changes
@@@ -16,9 +16,14 @@ const appIcon = require('openvibe-shared/app-icon');const ovServe = require('openvibe-shared/serve');const SITE_NAME = 'OpenVibe.Run';++// The questions the catalog answers for Run (openvibe-contracts manifests/services/run.json, kept word for word): shown++// on the home and given to search engines as FAQPage data.++const FAQ = ((require('openvibe-contracts/manifests/services/run.json').site || {}).faq || [])++ .filter((f) => f && f.question && f.answer).map((f) => ({ q: String(f.question).trim(), a: String(f.answer).trim() }));++const esc = (t) => String(t).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');const NETWORK_URL = 'https://openvibe.network';--const DESCRIPTION = 'Run a job on a node that can: submit it to one API with your project\'s token, and Run places it on a node '-- + 'that offers its runtime, streams its output and meters it per second.';++const DESCRIPTION = 'Run jobs on your own machine or on a node that can: one API and a project token; Run places the job, streams '++ + 'its output and meters it per second.';const HOME_CSP = ["default-src 'self'",@@@ -36,6 +41,11 @@ const HOME_CSP = ["form-action 'self' https://openvibe.network",].join('; ');++// The questions' cards, in the kit's tokens (the home has no stylesheet of its own).++const FAQ_CSS = '<style>.run-faq{border:1px solid var(--border,#1f2d47);border-radius:10px;padding:4px 16px;margin:8px 0;background:var(--bg-card,#131c2e)}'++ + '.run-faq[open]{padding-bottom:12px}.run-faq summary{cursor:pointer;font-weight:600;padding:8px 0}'++ + '.run-faq p{margin:4px 0 0;color:var(--text-secondary,#96a7c2)}</style>';++const code = (s) => `<code>${s.replace(/&/g, '&').replace(/</g, '<')}</code>`;const SUBMIT_EXAMPLE = `curl -X POST https://openvibe.run/api/v1/jobs \\@@@ -47,17 +57,18 @@ function renderHome({ config }) {const nav = {service: 'run',apiBase: NETWORK_URL,-- links: [{ label: 'How it works', href: '#how' }, { label: 'API', href: '#api' }],++ links: [{ label: 'Your machine', href: '#machine' }, { label: 'How it works', href: '#how' }, { label: 'API', href: '#api' }],history: { type: 'page', title: SITE_NAME },};return shell.page({name: SITE_NAME, service: 'run', lang: 'en',-- title: `${SITE_NAME}: run a job on a node that can`,++ title: `${SITE_NAME}: run jobs on your own machine`,++ jsonLd: FAQ.length ? [{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) }] : undefined,siteName: SITE_NAME, description: DESCRIPTION, summary: DESCRIPTION,canonical: `${config.baseUrl}/`, robots: 'index, follow',navbar: nav, footer: FOOTER, home: '/',navLinks: nav.links,-- head: [appIcon.headTags({ site: 'run' }), '<link rel="manifest" href="/manifest.webmanifest">', `<link rel="apple-touch-icon" href="${require('openvibe-shared/app-icons').url('run', 192)}">`, `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`].join('\n'),++ head: [appIcon.headTags({ site: 'run' }), '<link rel="manifest" href="/manifest.webmanifest">', `<link rel="apple-touch-icon" href="${require('openvibe-shared/app-icons').url('run', 192)}">`, `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`, FAQ_CSS].join('\n'),body: `<div id="navbar-mount"></div>\n<main id="main">${body()}</main>`,});}@@@ -79,15 +90,35 @@ function renderUpdates({ config }) {});}++const SDK_EXAMPLE = `const { createClient } = require('openvibe-sdk/core');++const { createRunClient } = require('openvibe-sdk/run');++++const run = createRunClient(createClient({ tokenProvider: tokens })).forProject('prj_…');++const { job } = await run.jobs.submit({++ class: 'function', artifact: { name: 'thumbnail', version: '1.2.0' }, args: { width: 320 },++ limits: { wall_ms: 30000, cpu_ms: 30000, mem_bytes: 268435456 }, ttl_ms: 600000,++});++for await (const ev of run.jobs.events(job.id)) console.log(ev.type, ev.data); // state changes and output, until it ends`;++function body() {return showcase.hero({eyebrow: 'OpenVibe.Run · jobs on nodes · alpha',-- title: 'Run a job.',-- accent: 'On a node that can.',-- lede: 'Send a job to one API with your project\'s token. Run picks a node that offers the runtime the job asks for, '-- + 'hands it over, streams its output back to you and settles it, metered per second it runs.',-- actions: [{ label: 'How it works', href: '#how', primary: true }, { label: 'The API', href: '#api' }],++ title: 'Run jobs on your own machine,',++ accent: 'or on a node that can.',++ lede: 'Pair a computer once and the jobs you start run there, on your hardware, with their output streamed back to you. '++ + 'Or send a job to one API with your project\'s token: Run picks a node that offers the runtime it asks for and meters it per second.',++ actions: [{ label: 'Pair your machine', href: 'https://openvibe.bot/machines', primary: true }, { label: 'Try it from OpenVibe.Codes', href: 'https://openvibe.codes/run' }, { label: 'The API', href: '#api' }],note: 'Alpha. A job runs on nodes paired with OpenVibe (the OpenVibe Node); with none online for its class it waits, then expires.',++ }) + showcase.steps({++ id: 'machine',++ title: 'Your machine, your jobs',++ lede: 'A computer you pair runs only the jobs you start: placement matches its owner before anything else.',++ items: [++ { title: 'Pair it', href: 'https://openvibe.bot/machines', text: 'On openvibe.bot/machines, copy the one-line install command and run it on the computer. It joins as a worker under your account.' },++ { title: 'Start a job', href: 'https://openvibe.codes/run', text: 'OpenVibe.Codes\' "Run on your machine" sends a coding task there, or your own code submits one through the API.' },++ { title: 'Watch it run', text: 'The job\'s event stream shows each state change and the machine\'s output as it is written.' },++ { title: 'Keep the record', text: 'Every job ends succeeded, failed, cancelled or expired, with its timings, its exit and the seconds it ran.' },++ ],}) + showcase.steps({id: 'how',title: 'How a job runs',@@@ -97,6 +128,16 @@ function body() {{ title: 'Run', text: 'OpenVibe.Bot sends the job to the node over its device link and meters every wall-clock second it runs. The node\'s output comes back as it is written.' },{ title: 'End', text: 'Succeeded, failed, cancelled or expired. An end state never changes, and the job, its timings, its exit and its metered seconds stay readable to the project.' },],++ }) + showcase.compare({++ title: 'Runtime classes',++ lede: 'A job names the class of machine it needs. What each one is today:',++ columns: ['What runs', 'Today'],++ rows: [++ { label: 'function', values: ['A registered artifact on a paired OpenVibe Node', 'Works'] },++ { label: 'code', values: ['An OpenVibe.Codes task on your own machine', 'Works'] },++ { label: 'browser', values: ['A page in a managed browser', 'Built, not switched on yet'] },++ { label: 'linux, desktop, gpu', values: ['Reserved names for the machines they say', 'Not yet'] },++ ],}) + showcase.features({title: 'What you get',items: [@@@ -107,17 +148,25 @@ function body() {{ icon: 'ov:history', title: 'Metered per second', text: 'The node link measures each job\'s wall clock; the reading is what the job is charged against, never an estimate.' },{ icon: 'ov:error', title: 'Ends cleanly', text: 'Cancel a job, or let its time to live expire it. A node that disappears mid-run fails the job instead of leaving it running forever.' },],-- }) + `<section class="sc-sec" id="api"><h2>The API</h2>--<p class="sc-lede">Jobs are created with a project token from ${'<a href="https://openvibe.services">OpenVibe.Services</a>'} (capability ${code('run.job.submit')}; reading, listing, cancelling and streaming take ${code('run.job.read')}, ${code('run.job.list')}, ${code('run.job.cancel')} and ${code('run.job.stream')}). A job names its runtime class, a pre-registered artifact (a name and one exact version), its input and its limits (wall clock, CPU time, memory):</p>--</section>${showcase.code({ title: 'Submit a job', samples: [{ label: 'curl', lang: 'bash', code: SUBMIT_EXAMPLE }] })}<section class="sc-sec" aria-label="Job routes">--<ul class="facts">--<li>${code('GET /api/v1/jobs/:id')}: state, node, timings, exit and metered seconds.</li>--<li>${code('GET /api/v1/jobs')}: the project's jobs, newest first.</li>--<li>${code('POST /api/v1/jobs/:id/cancel')}: stops it; an ended job stays ended.</li>--<li>${code('POST /api/v1/jobs/:id/stream/ticket')} then ${code('GET /api/v1/jobs/:id/stream?ticket=…')}: its events and output as server-sent events (a service may send its own token instead).</li>--</ul>--<p>To run jobs on your own machine, pair it with the <a href="https://github.com/OpenVibers/OpenVibe.Node">OpenVibe Node</a>; it offers the runtimes it can run. The source is <a href="https://github.com/OpenVibers/OpenVibe.Run">OpenVibers/OpenVibe.Run</a> (AGPL-3.0).</p>--</section>`;++ }) + showcase.code({++ id: 'api',++ title: 'The API',++ lede: 'Jobs are created with a project token from OpenVibe.Services (run.job.submit; reading, listing, cancelling and streaming take run.job.read, run.job.list, run.job.cancel and run.job.stream). A job names its runtime class, a pre-registered artifact (a name and one exact version), its input and its limits.',++ samples: [{ label: 'Node (openvibe-sdk)', lang: 'js', code: SDK_EXAMPLE }, { label: 'curl', lang: 'bash', code: SUBMIT_EXAMPLE }],++ }) + showcase.features({++ title: 'The job routes',++ items: [++ { title: 'GET /api/v1/jobs/:id', text: 'State, node, timings, exit and metered seconds.' },++ { title: 'GET /api/v1/jobs', text: 'The project\'s jobs, newest first.' },++ { title: 'POST /api/v1/jobs/:id/cancel', text: 'Stops it; an ended job stays ended.' },++ { title: 'POST /api/v1/jobs/:id/stream/ticket', text: 'Then GET /api/v1/jobs/:id/stream?ticket=…: its events and output as server-sent events (a service may send its own token instead).' },++ ],++ }) + (FAQ.length ? `<section class="sc-sec" id="questions" aria-labelledby="run-faq-h"><h2 id="run-faq-h">Questions</h2>${FAQ.map((f) => `<details class="run-faq"><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</section>` : '')++ + showcase.cta({++ title: 'Open source',++ text: 'Run is AGPL-3.0, and so is the OpenVibe Node that turns a computer into a worker.',++ actions: [{ label: 'OpenVibers/OpenVibe.Run', href: 'https://github.com/OpenVibers/OpenVibe.Run' }, { label: 'The OpenVibe Node', href: 'https://github.com/OpenVibers/OpenVibe.Node' }],++ });}--module.exports = { renderHome, renderUpdates, HOME_CSP, SITE_NAME, DESCRIPTION };++module.exports = { renderHome, renderUpdates, HOME_CSP, SITE_NAME, DESCRIPTION, FAQ };@@@ -13,7 +13,13 @@ const { boot, check, done } = require('./helpers/app');const r = await t.call('GET', '/', { token: null, headers: { Accept: 'text/html' } });assert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type'), /text\/html/);-- assert.match(r.text, /<title>OpenVibe\.Run: run a job on a node that can/);++ assert.match(r.text, /<title>OpenVibe\.Run: run jobs on your own machine/);++ assert.ok(r.text.includes('href="https://openvibe.bot/machines"'), 'it leads with pairing your own machine');++ assert.match(r.text, /<th scope="row">browser<\/th>[\s\S]*?Built, not switched on yet/, 'the class table says what works today');++ const faq = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])).find((j) => j['@type'] === 'FAQPage');++ assert.ok(faq && faq.mainEntity.length >= 4, 'the catalog\'s questions as FAQPage data');++ for (const q of faq.mainEntity) assert.ok(r.text.includes(`<summary>${q.name}</summary>`), `and on the page: ${q.name}`);++ assert.ok(r.text.includes("require('openvibe-sdk/run')") || r.text.includes('require('openvibe-sdk/run')'), 'the SDK sample');assert.match(r.text, /id="navbar-mount"/);assert.match(r.headers.get('content-security-policy'), /frame-ancestors 'none'/);assert.match(r.headers.get('cache-control'), /max-age=300/);
@@ -16,9 +16,14 @@ const appIcon = require('openvibe-shared/app-icon');
@@ -16,9 +16,14 @@ const appIcon = require('openvibe-shared/app-icon');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const SITE_NAME = 'OpenVibe.Run';
const SITE_NAME = 'OpenVibe.Run';
+// The questions the catalog answers for Run (openvibe-contracts manifests/services/run.json, kept word for word): shown
+// on the home and given to search engines as FAQPage data.
+const FAQ = ((require('openvibe-contracts/manifests/services/run.json').site || {}).faq || [])
+ .filter((f) => f && f.question && f.answer).map((f) => ({ q: String(f.question).trim(), a: String(f.answer).trim() }));
+const esc = (t) => String(t).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
-const DESCRIPTION = 'Run a job on a node that can: submit it to one API with your project\'s token, and Run places it on a node '
- + 'that offers its runtime, streams its output and meters it per second.';
+const DESCRIPTION = 'Run jobs on your own machine or on a node that can: one API and a project token; Run places the job, streams '
+ + 'its output and meters it per second.';
const HOME_CSP = [
const HOME_CSP = [
"default-src 'self'",
"default-src 'self'",
@@ -36,6 +41,11 @@ const HOME_CSP = [
@@ -36,6 +41,11 @@ const HOME_CSP = [
"form-action 'self' https://openvibe.network",
"form-action 'self' https://openvibe.network",
].join('; ');
].join('; ');
+// The questions' cards, in the kit's tokens (the home has no stylesheet of its own).
+const FAQ_CSS = '<style>.run-faq{border:1px solid var(--border,#1f2d47);border-radius:10px;padding:4px 16px;margin:8px 0;background:var(--bg-card,#131c2e)}'
+ + '.run-faq[open]{padding-bottom:12px}.run-faq summary{cursor:pointer;font-weight:600;padding:8px 0}'
+ + '.run-faq p{margin:4px 0 0;color:var(--text-secondary,#96a7c2)}</style>';
+
const code = (s) => `<code>${s.replace(/&/g, '&').replace(/</g, '<')}</code>`;
const code = (s) => `<code>${s.replace(/&/g, '&').replace(/</g, '<')}</code>`;
const SUBMIT_EXAMPLE = `curl -X POST https://openvibe.run/api/v1/jobs \\
const SUBMIT_EXAMPLE = `curl -X POST https://openvibe.run/api/v1/jobs \\
@@ -47,17 +57,18 @@ function renderHome({ config }) {
@@ -47,17 +57,18 @@ function renderHome({ config }) {
const nav = {
const nav = {
service: 'run',
service: 'run',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
- links: [{ label: 'How it works', href: '#how' }, { label: 'API', href: '#api' }],
+ links: [{ label: 'Your machine', href: '#machine' }, { label: 'How it works', href: '#how' }, { label: 'API', href: '#api' }],
history: { type: 'page', title: SITE_NAME },
history: { type: 'page', title: SITE_NAME },
};
};
return shell.page({
return shell.page({
name: SITE_NAME, service: 'run', lang: 'en',
name: SITE_NAME, service: 'run', lang: 'en',
- title: `${SITE_NAME}: run a job on a node that can`,
+ title: `${SITE_NAME}: run jobs on your own machine`,
+ jsonLd: FAQ.length ? [{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: FAQ.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })) }] : undefined,
siteName: SITE_NAME, description: DESCRIPTION, summary: DESCRIPTION,
siteName: SITE_NAME, description: DESCRIPTION, summary: DESCRIPTION,
canonical: `${config.baseUrl}/`, robots: 'index, follow',
canonical: `${config.baseUrl}/`, robots: 'index, follow',
navbar: nav, footer: FOOTER, home: '/',
navbar: nav, footer: FOOTER, home: '/',
navLinks: nav.links,
navLinks: nav.links,
- head: [appIcon.headTags({ site: 'run' }), '<link rel="manifest" href="/manifest.webmanifest">', `<link rel="apple-touch-icon" href="${require('openvibe-shared/app-icons').url('run', 192)}">`, `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`].join('\n'),
+ head: [appIcon.headTags({ site: 'run' }), '<link rel="manifest" href="/manifest.webmanifest">', `<link rel="apple-touch-icon" href="${require('openvibe-shared/app-icons').url('run', 192)}">`, `<link rel="stylesheet" href="${ovServe.url(showcase.STYLESHEET)}">`, FAQ_CSS].join('\n'),
body: `<div id="navbar-mount"></div>\n<main id="main">${body()}</main>`,
body: `<div id="navbar-mount"></div>\n<main id="main">${body()}</main>`,
});
});
}
}
@@ -79,15 +90,35 @@ function renderUpdates({ config }) {
@@ -79,15 +90,35 @@ function renderUpdates({ config }) {
});
});
}
}
+const SDK_EXAMPLE = `const { createClient } = require('openvibe-sdk/core');
+const { createRunClient } = require('openvibe-sdk/run');
+
+const run = createRunClient(createClient({ tokenProvider: tokens })).forProject('prj_…');
+const { job } = await run.jobs.submit({
+ class: 'function', artifact: { name: 'thumbnail', version: '1.2.0' }, args: { width: 320 },
+ limits: { wall_ms: 30000, cpu_ms: 30000, mem_bytes: 268435456 }, ttl_ms: 600000,
+});
+for await (const ev of run.jobs.events(job.id)) console.log(ev.type, ev.data); // state changes and output, until it ends`;
+
function body() {
function body() {
return showcase.hero({
return showcase.hero({
eyebrow: 'OpenVibe.Run · jobs on nodes · alpha',
eyebrow: 'OpenVibe.Run · jobs on nodes · alpha',
- title: 'Run a job.',
- accent: 'On a node that can.',
- lede: 'Send a job to one API with your project\'s token. Run picks a node that offers the runtime the job asks for, '
- + 'hands it over, streams its output back to you and settles it, metered per second it runs.',
- actions: [{ label: 'How it works', href: '#how', primary: true }, { label: 'The API', href: '#api' }],
+ title: 'Run jobs on your own machine,',
+ accent: 'or on a node that can.',
+ lede: 'Pair a computer once and the jobs you start run there, on your hardware, with their output streamed back to you. '
+ + 'Or send a job to one API with your project\'s token: Run picks a node that offers the runtime it asks for and meters it per second.',
+ actions: [{ label: 'Pair your machine', href: 'https://openvibe.bot/machines', primary: true }, { label: 'Try it from OpenVibe.Codes', href: 'https://openvibe.codes/run' }, { label: 'The API', href: '#api' }],
note: 'Alpha. A job runs on nodes paired with OpenVibe (the OpenVibe Node); with none online for its class it waits, then expires.',
note: 'Alpha. A job runs on nodes paired with OpenVibe (the OpenVibe Node); with none online for its class it waits, then expires.',
+ }) + showcase.steps({
+ id: 'machine',
+ title: 'Your machine, your jobs',
+ lede: 'A computer you pair runs only the jobs you start: placement matches its owner before anything else.',
+ items: [
+ { title: 'Pair it', href: 'https://openvibe.bot/machines', text: 'On openvibe.bot/machines, copy the one-line install command and run it on the computer. It joins as a worker under your account.' },
+ { title: 'Start a job', href: 'https://openvibe.codes/run', text: 'OpenVibe.Codes\' "Run on your machine" sends a coding task there, or your own code submits one through the API.' },
+ { title: 'Watch it run', text: 'The job\'s event stream shows each state change and the machine\'s output as it is written.' },
+ { title: 'Keep the record', text: 'Every job ends succeeded, failed, cancelled or expired, with its timings, its exit and the seconds it ran.' },
+ ],
}) + showcase.steps({
}) + showcase.steps({
id: 'how',
id: 'how',
title: 'How a job runs',
title: 'How a job runs',
@@ -97,6 +128,16 @@ function body() {
@@ -97,6 +128,16 @@ function body() {
{ title: 'Run', text: 'OpenVibe.Bot sends the job to the node over its device link and meters every wall-clock second it runs. The node\'s output comes back as it is written.' },
{ title: 'Run', text: 'OpenVibe.Bot sends the job to the node over its device link and meters every wall-clock second it runs. The node\'s output comes back as it is written.' },
{ title: 'End', text: 'Succeeded, failed, cancelled or expired. An end state never changes, and the job, its timings, its exit and its metered seconds stay readable to the project.' },
{ title: 'End', text: 'Succeeded, failed, cancelled or expired. An end state never changes, and the job, its timings, its exit and its metered seconds stay readable to the project.' },
],
],
+ }) + showcase.compare({
+ title: 'Runtime classes',
+ lede: 'A job names the class of machine it needs. What each one is today:',
+ columns: ['What runs', 'Today'],
+ rows: [
+ { label: 'function', values: ['A registered artifact on a paired OpenVibe Node', 'Works'] },
+ { label: 'code', values: ['An OpenVibe.Codes task on your own machine', 'Works'] },
+ { label: 'browser', values: ['A page in a managed browser', 'Built, not switched on yet'] },
+ { label: 'linux, desktop, gpu', values: ['Reserved names for the machines they say', 'Not yet'] },
+ ],
}) + showcase.features({
}) + showcase.features({
title: 'What you get',
title: 'What you get',
items: [
items: [
@@ -107,17 +148,25 @@ function body() {
@@ -107,17 +148,25 @@ function body() {
{ icon: 'ov:history', title: 'Metered per second', text: 'The node link measures each job\'s wall clock; the reading is what the job is charged against, never an estimate.' },
{ icon: 'ov:history', title: 'Metered per second', text: 'The node link measures each job\'s wall clock; the reading is what the job is charged against, never an estimate.' },
{ icon: 'ov:error', title: 'Ends cleanly', text: 'Cancel a job, or let its time to live expire it. A node that disappears mid-run fails the job instead of leaving it running forever.' },
{ icon: 'ov:error', title: 'Ends cleanly', text: 'Cancel a job, or let its time to live expire it. A node that disappears mid-run fails the job instead of leaving it running forever.' },
],
],
- }) + `<section class="sc-sec" id="api"><h2>The API</h2>
-<p class="sc-lede">Jobs are created with a project token from ${'<a href="https://openvibe.services">OpenVibe.Services</a>'} (capability ${code('run.job.submit')}; reading, listing, cancelling and streaming take ${code('run.job.read')}, ${code('run.job.list')}, ${code('run.job.cancel')} and ${code('run.job.stream')}). A job names its runtime class, a pre-registered artifact (a name and one exact version), its input and its limits (wall clock, CPU time, memory):</p>
-</section>${showcase.code({ title: 'Submit a job', samples: [{ label: 'curl', lang: 'bash', code: SUBMIT_EXAMPLE }] })}<section class="sc-sec" aria-label="Job routes">
-<ul class="facts">
-<li>${code('GET /api/v1/jobs/:id')}: state, node, timings, exit and metered seconds.</li>
-<li>${code('GET /api/v1/jobs')}: the project's jobs, newest first.</li>
-<li>${code('POST /api/v1/jobs/:id/cancel')}: stops it; an ended job stays ended.</li>
-<li>${code('POST /api/v1/jobs/:id/stream/ticket')} then ${code('GET /api/v1/jobs/:id/stream?ticket=…')}: its events and output as server-sent events (a service may send its own token instead).</li>
-</ul>
-<p>To run jobs on your own machine, pair it with the <a href="https://github.com/OpenVibers/OpenVibe.Node">OpenVibe Node</a>; it offers the runtimes it can run. The source is <a href="https://github.com/OpenVibers/OpenVibe.Run">OpenVibers/OpenVibe.Run</a> (AGPL-3.0).</p>
-</section>`;
+ }) + showcase.code({
+ id: 'api',
+ title: 'The API',
+ lede: 'Jobs are created with a project token from OpenVibe.Services (run.job.submit; reading, listing, cancelling and streaming take run.job.read, run.job.list, run.job.cancel and run.job.stream). A job names its runtime class, a pre-registered artifact (a name and one exact version), its input and its limits.',
+ samples: [{ label: 'Node (openvibe-sdk)', lang: 'js', code: SDK_EXAMPLE }, { label: 'curl', lang: 'bash', code: SUBMIT_EXAMPLE }],
+ }) + showcase.features({
+ title: 'The job routes',
+ items: [
+ { title: 'GET /api/v1/jobs/:id', text: 'State, node, timings, exit and metered seconds.' },
+ { title: 'GET /api/v1/jobs', text: 'The project\'s jobs, newest first.' },
+ { title: 'POST /api/v1/jobs/:id/cancel', text: 'Stops it; an ended job stays ended.' },
+ { title: 'POST /api/v1/jobs/:id/stream/ticket', text: 'Then GET /api/v1/jobs/:id/stream?ticket=…: its events and output as server-sent events (a service may send its own token instead).' },
+ ],
+ }) + (FAQ.length ? `<section class="sc-sec" id="questions" aria-labelledby="run-faq-h"><h2 id="run-faq-h">Questions</h2>${FAQ.map((f) => `<details class="run-faq"><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</section>` : '')
+ + showcase.cta({
+ title: 'Open source',
+ text: 'Run is AGPL-3.0, and so is the OpenVibe Node that turns a computer into a worker.',
+ actions: [{ label: 'OpenVibers/OpenVibe.Run', href: 'https://github.com/OpenVibers/OpenVibe.Run' }, { label: 'The OpenVibe Node', href: 'https://github.com/OpenVibers/OpenVibe.Node' }],
+ });
}
}
-module.exports = { renderHome, renderUpdates, HOME_CSP, SITE_NAME, DESCRIPTION };
+module.exports = { renderHome, renderUpdates, HOME_CSP, SITE_NAME, DESCRIPTION, FAQ };
@@ -13,7 +13,13 @@ const { boot, check, done } = require('./helpers/app');
@@ -13,7 +13,13 @@ const { boot, check, done } = require('./helpers/app');
const r = await t.call('GET', '/', { token: null, headers: { Accept: 'text/html' } });
const r = await t.call('GET', '/', { token: null, headers: { Accept: 'text/html' } });
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /text\/html/);
assert.match(r.headers.get('content-type'), /text\/html/);
- assert.match(r.text, /<title>OpenVibe\.Run: run a job on a node that can/);
+ assert.match(r.text, /<title>OpenVibe\.Run: run jobs on your own machine/);
+ assert.ok(r.text.includes('href="https://openvibe.bot/machines"'), 'it leads with pairing your own machine');
+ assert.match(r.text, /<th scope="row">browser<\/th>[\s\S]*?Built, not switched on yet/, 'the class table says what works today');
+ const faq = [...r.text.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1])).find((j) => j['@type'] === 'FAQPage');
+ assert.ok(faq && faq.mainEntity.length >= 4, 'the catalog\'s questions as FAQPage data');
+ for (const q of faq.mainEntity) assert.ok(r.text.includes(`<summary>${q.name}</summary>`), `and on the page: ${q.name}`);
+ assert.ok(r.text.includes("require('openvibe-sdk/run')") || r.text.includes('require('openvibe-sdk/run')'), 'the SDK sample');
assert.match(r.text, /id="navbar-mount"/);
assert.match(r.text, /id="navbar-mount"/);
assert.match(r.headers.get('content-security-policy'), /frame-ancestors 'none'/);
assert.match(r.headers.get('content-security-policy'), /frame-ancestors 'none'/);
assert.match(r.headers.get('cache-control'), /max-age=300/);
assert.match(r.headers.get('cache-control'), /max-age=300/);