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)

OP OpenVibers committed 22 days ago · 74b284390b · 2 files, +78 −23

Changes

ddiff --git a/server/web/home.js b/server/web/home.js
iindex 2c15c89..96c02e2 100644
---- a/server/web/home.js
++++ b/server/web/home.js
@@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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, '&amp;').replace(/</g, '&lt;')}</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 };
ddiff --git a/test/site.test.js b/test/site.test.js
iindex e672a8a..24bcb85 100644
---- a/test/site.test.js
++++ b/test/site.test.js
@@@ -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(&#39;openvibe-sdk/run&#39;)'), '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/);