openvibers/openvibe.bot Commits
Bot: code blocks are keyboard-focusable (axe scrollable-region-focusable) (#121)
Changes
@@@ -271,7 +271,7 @@ function submissionRow(sub, { review = false, by = null } = {}) {const note = sub.review_note ? `<p class="muted">Reviewer: ${esc(sub.review_note)}</p>` : '';const done = sub.status === 'approved' ? `<p>${link(`/models/${sub.profile_id}`, 'Its model page')}</p>` : '';const withdraw = !review && sub.status === 'pending' ? `<form method="post" action="/robots/submissions/${esc(sub.id)}/withdraw"><button type="submit" class="dir-btn">Withdraw</button></form>` : '';-- const decide = review ? `<details><summary>The profile</summary><pre class="dir-pre">${esc(JSON.stringify(p, null, 2))}</pre></details>++ const decide = review ? `<details><summary>The profile</summary><pre tabindex="0" class="dir-pre">${esc(JSON.stringify(p, null, 2))}</pre></details>${sub.notes ? `<p class="muted">Notes: ${esc(sub.notes)}</p>` : ''}<form class="dir-review" method="post" action="/robots/submissions/${esc(sub.id)}/review"><label class="dir-field dir-wide"><span class="dir-field-label">Note (needed to reject)</span><input type="text" name="note" maxlength="1000"></label>@@@ -6,7 +6,7 @@** They are rendered with the app's own page helper (server/web/render.js, the one the robots page uses), which* emits the site stylesheet and no script at all, so these pages run under the site-wide `default-src 'self'`-- * CSP. Every value is escaped; the code samples are `<pre class="installer"><code>…</code></pre>`, escaped.++ * CSP. Every value is escaped; the code samples are `<pre tabindex="0" class="installer"><code>…</code></pre>`, escaped.** renderDocsIndex({ config }) GET /docs* renderDriversPage({ config }) GET /docs/drivers@@@ -24,8 +24,8 @@ const NODE = 'https://github.com/OpenVibers/OpenVibe.Node';const TREE = `${NODE}/tree/main`;const BLOB = `${NODE}/blob/main`;--/** A code sample, in a `<pre><code>` block, escaped. */--const code = (text) => `<pre class="installer"><code>${esc(text)}</code></pre>`;++/** A code sample, in a `<pre tabindex="0"><code>` block, escaped. */++const code = (text) => `<pre tabindex="0" class="installer"><code>${esc(text)}</code></pre>`;/** An external link (openvibe.bot's CSP allows none of these origins in a frame, so it is a plain link). */const out = (href, text) => `<a href="${esc(href)}" rel="noopener">${esc(text)}</a>`;/** A link on this site. */@@@ -562,7 +562,7 @@ function renderPairingPage({ robot, pairing, profile = null, phoneUrl = null })<p class="muted">It opens the robot's page on the phone with this code filled in. Tap Pair, allow the camera, and keep the page open: the phone is the robot now. No app to install.</p><p class="muted">No camera app handy? Open <b>openvibe.bot/device/phone</b> on the phone and enter the robot and code by hand: <code>${esc(robot.id)}</code></p></li>`: `<li class="card"><h2>Run this on the machine that serves the robot${profile ? ` <span class="pill">${esc(profile.name)}</span>` : ''}</h2>--<pre class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre>++<pre tabindex="0" class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre><button type="button" class="copy" data-copy>${icon('copy')}<span data-copy-label>Copy the command</span></button></li>`;const body = `${topbar(`<span class="crumb">${esc(robot.name)}</span>`)}<main class="page pairing" data-pair-robot="${esc(robot.id)}">@@@ -620,11 +620,11 @@ function renderMachinesPage({ machines = [], pairing = null }) {: `<div class="empty">${icon('plug', 'icon empty-icon')}<p class="empty-title">No machines yet.</p><p>Add your computer or a server you own. It runs the jobs you start, such as a coding task on openvibe.codes, and nobody else's.</p></div>`;const add = pairing? `<ol class="steps"><li class="card"><h2>Run this on the machine</h2>--<pre class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre>++<pre tabindex="0" class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre><button type="button" class="copy" data-copy>${icon('copy')}<span data-copy-label>Copy the command</span></button><p class="muted">Linux with systemd. It installs OpenVibe Node, turns its job worker on (jobs run as a separate user, in a sandbox, with the limits you set) and pairs the machine with your account. For coding jobs the machine also needs Node.js 22 or newer and git, and a coding agent: Claude Code, Codex, OpenCode, or a DeepSeek key.</p></li><li class="card"><h2>Or let your agents use this computer</h2>--<pre class="installer"><code>${esc(pairing.installer.replace(/ --worker$/, ' --computer ask'))}</code></pre>++<pre tabindex="0" class="installer"><code>${esc(pairing.installer.replace(/ --worker$/, ' --computer ask'))}</code></pre><p class="muted">Instead of running jobs, the machine's terminal, files and screen become usable by your agents, at the level you set on the machine: <code>ask</code> waits for <code>openvibe-node approve</code> there before anything runs or changes. The Node runs as your own account, never root. <a href="https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs/computer.md">How it works</a>.</p></li><li class="card"><h2>Or enter the pairing code</h2><p><strong class="code">${esc(pairing.code)}</strong></p><p class="muted">Valid until <time datetime="${esc(pairing.expires_at)}">${esc(pairing.expires_at)}</time>. It works once.</p></li></ol>@@@ -38,8 +38,10 @@ const { HOME_CSP: SITE_CSP } = require('../server/web/home');await check('GET /docs/drivers: the guide, connection kinds, every message, safety rules and testing', async () => {const r = await get('/docs/drivers');assert.strictEqual(r.status, 200);++ // Code blocks scroll sideways on a phone, so each is a keyboard stop (axe scrollable-region-focusable).assert.strictEqual(r.headers.get('content-security-policy'), SITE_CSP);const html = await r.text();++ assert.ok(!/<pre(?![^>]*tabindex)[\s>]/.test(html), 'every <pre> is focusable');assert.match(html, /Write a driver/);assert.match(html, /stdin\/stdout/);assert.match(html, /openvibe_plugin/);
@@ -271,7 +271,7 @@ function submissionRow(sub, { review = false, by = null } = {}) {
@@ -271,7 +271,7 @@ function submissionRow(sub, { review = false, by = null } = {}) {
const note = sub.review_note ? `<p class="muted">Reviewer: ${esc(sub.review_note)}</p>` : '';
const note = sub.review_note ? `<p class="muted">Reviewer: ${esc(sub.review_note)}</p>` : '';
const done = sub.status === 'approved' ? `<p>${link(`/models/${sub.profile_id}`, 'Its model page')}</p>` : '';
const done = sub.status === 'approved' ? `<p>${link(`/models/${sub.profile_id}`, 'Its model page')}</p>` : '';
const withdraw = !review && sub.status === 'pending' ? `<form method="post" action="/robots/submissions/${esc(sub.id)}/withdraw"><button type="submit" class="dir-btn">Withdraw</button></form>` : '';
const withdraw = !review && sub.status === 'pending' ? `<form method="post" action="/robots/submissions/${esc(sub.id)}/withdraw"><button type="submit" class="dir-btn">Withdraw</button></form>` : '';
- const decide = review ? `<details><summary>The profile</summary><pre class="dir-pre">${esc(JSON.stringify(p, null, 2))}</pre></details>
+ const decide = review ? `<details><summary>The profile</summary><pre tabindex="0" class="dir-pre">${esc(JSON.stringify(p, null, 2))}</pre></details>
${sub.notes ? `<p class="muted">Notes: ${esc(sub.notes)}</p>` : ''}
${sub.notes ? `<p class="muted">Notes: ${esc(sub.notes)}</p>` : ''}
<form class="dir-review" method="post" action="/robots/submissions/${esc(sub.id)}/review">
<form class="dir-review" method="post" action="/robots/submissions/${esc(sub.id)}/review">
<label class="dir-field dir-wide"><span class="dir-field-label">Note (needed to reject)</span><input type="text" name="note" maxlength="1000"></label>
<label class="dir-field dir-wide"><span class="dir-field-label">Note (needed to reject)</span><input type="text" name="note" maxlength="1000"></label>
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
*
*
* They are rendered with the app's own page helper (server/web/render.js, the one the robots page uses), which
* They are rendered with the app's own page helper (server/web/render.js, the one the robots page uses), which
* emits the site stylesheet and no script at all, so these pages run under the site-wide `default-src 'self'`
* emits the site stylesheet and no script at all, so these pages run under the site-wide `default-src 'self'`
- * CSP. Every value is escaped; the code samples are `<pre class="installer"><code>…</code></pre>`, escaped.
+ * CSP. Every value is escaped; the code samples are `<pre tabindex="0" class="installer"><code>…</code></pre>`, escaped.
*
*
* renderDocsIndex({ config }) GET /docs
* renderDocsIndex({ config }) GET /docs
* renderDriversPage({ config }) GET /docs/drivers
* renderDriversPage({ config }) GET /docs/drivers
@@ -24,8 +24,8 @@ const NODE = 'https://github.com/OpenVibers/OpenVibe.Node';
@@ -24,8 +24,8 @@ const NODE = 'https://github.com/OpenVibers/OpenVibe.Node';
const TREE = `${NODE}/tree/main`;
const TREE = `${NODE}/tree/main`;
const BLOB = `${NODE}/blob/main`;
const BLOB = `${NODE}/blob/main`;
-/** A code sample, in a `<pre><code>` block, escaped. */
-const code = (text) => `<pre class="installer"><code>${esc(text)}</code></pre>`;
+/** A code sample, in a `<pre tabindex="0"><code>` block, escaped. */
+const code = (text) => `<pre tabindex="0" class="installer"><code>${esc(text)}</code></pre>`;
/** An external link (openvibe.bot's CSP allows none of these origins in a frame, so it is a plain link). */
/** An external link (openvibe.bot's CSP allows none of these origins in a frame, so it is a plain link). */
const out = (href, text) => `<a href="${esc(href)}" rel="noopener">${esc(text)}</a>`;
const out = (href, text) => `<a href="${esc(href)}" rel="noopener">${esc(text)}</a>`;
/** A link on this site. */
/** A link on this site. */
@@ -562,7 +562,7 @@ function renderPairingPage({ robot, pairing, profile = null, phoneUrl = null })
@@ -562,7 +562,7 @@ function renderPairingPage({ robot, pairing, profile = null, phoneUrl = null })
<p class="muted">It opens the robot's page on the phone with this code filled in. Tap Pair, allow the camera, and keep the page open: the phone is the robot now. No app to install.</p>
<p class="muted">It opens the robot's page on the phone with this code filled in. Tap Pair, allow the camera, and keep the page open: the phone is the robot now. No app to install.</p>
<p class="muted">No camera app handy? Open <b>openvibe.bot/device/phone</b> on the phone and enter the robot and code by hand: <code>${esc(robot.id)}</code></p></li>`
<p class="muted">No camera app handy? Open <b>openvibe.bot/device/phone</b> on the phone and enter the robot and code by hand: <code>${esc(robot.id)}</code></p></li>`
: `<li class="card"><h2>Run this on the machine that serves the robot${profile ? ` <span class="pill">${esc(profile.name)}</span>` : ''}</h2>
: `<li class="card"><h2>Run this on the machine that serves the robot${profile ? ` <span class="pill">${esc(profile.name)}</span>` : ''}</h2>
-<pre class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre>
+<pre tabindex="0" class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre>
<button type="button" class="copy" data-copy>${icon('copy')}<span data-copy-label>Copy the command</span></button></li>`;
<button type="button" class="copy" data-copy>${icon('copy')}<span data-copy-label>Copy the command</span></button></li>`;
const body = `${topbar(`<span class="crumb">${esc(robot.name)}</span>`)}
const body = `${topbar(`<span class="crumb">${esc(robot.name)}</span>`)}
<main class="page pairing" data-pair-robot="${esc(robot.id)}">
<main class="page pairing" data-pair-robot="${esc(robot.id)}">
@@ -620,11 +620,11 @@ function renderMachinesPage({ machines = [], pairing = null }) {
@@ -620,11 +620,11 @@ function renderMachinesPage({ machines = [], pairing = null }) {
: `<div class="empty">${icon('plug', 'icon empty-icon')}<p class="empty-title">No machines yet.</p><p>Add your computer or a server you own. It runs the jobs you start, such as a coding task on openvibe.codes, and nobody else's.</p></div>`;
: `<div class="empty">${icon('plug', 'icon empty-icon')}<p class="empty-title">No machines yet.</p><p>Add your computer or a server you own. It runs the jobs you start, such as a coding task on openvibe.codes, and nobody else's.</p></div>`;
const add = pairing
const add = pairing
? `<ol class="steps"><li class="card"><h2>Run this on the machine</h2>
? `<ol class="steps"><li class="card"><h2>Run this on the machine</h2>
-<pre class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre>
+<pre tabindex="0" class="installer"><code data-copy-source>${esc(pairing.installer)}</code></pre>
<button type="button" class="copy" data-copy>${icon('copy')}<span data-copy-label>Copy the command</span></button>
<button type="button" class="copy" data-copy>${icon('copy')}<span data-copy-label>Copy the command</span></button>
<p class="muted">Linux with systemd. It installs OpenVibe Node, turns its job worker on (jobs run as a separate user, in a sandbox, with the limits you set) and pairs the machine with your account. For coding jobs the machine also needs Node.js 22 or newer and git, and a coding agent: Claude Code, Codex, OpenCode, or a DeepSeek key.</p></li>
<p class="muted">Linux with systemd. It installs OpenVibe Node, turns its job worker on (jobs run as a separate user, in a sandbox, with the limits you set) and pairs the machine with your account. For coding jobs the machine also needs Node.js 22 or newer and git, and a coding agent: Claude Code, Codex, OpenCode, or a DeepSeek key.</p></li>
<li class="card"><h2>Or let your agents use this computer</h2>
<li class="card"><h2>Or let your agents use this computer</h2>
-<pre class="installer"><code>${esc(pairing.installer.replace(/ --worker$/, ' --computer ask'))}</code></pre>
+<pre tabindex="0" class="installer"><code>${esc(pairing.installer.replace(/ --worker$/, ' --computer ask'))}</code></pre>
<p class="muted">Instead of running jobs, the machine's terminal, files and screen become usable by your agents, at the level you set on the machine: <code>ask</code> waits for <code>openvibe-node approve</code> there before anything runs or changes. The Node runs as your own account, never root. <a href="https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs/computer.md">How it works</a>.</p></li>
<p class="muted">Instead of running jobs, the machine's terminal, files and screen become usable by your agents, at the level you set on the machine: <code>ask</code> waits for <code>openvibe-node approve</code> there before anything runs or changes. The Node runs as your own account, never root. <a href="https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs/computer.md">How it works</a>.</p></li>
<li class="card"><h2>Or enter the pairing code</h2><p><strong class="code">${esc(pairing.code)}</strong></p>
<li class="card"><h2>Or enter the pairing code</h2><p><strong class="code">${esc(pairing.code)}</strong></p>
<p class="muted">Valid until <time datetime="${esc(pairing.expires_at)}">${esc(pairing.expires_at)}</time>. It works once.</p></li></ol>
<p class="muted">Valid until <time datetime="${esc(pairing.expires_at)}">${esc(pairing.expires_at)}</time>. It works once.</p></li></ol>
@@ -38,8 +38,10 @@ const { HOME_CSP: SITE_CSP } = require('../server/web/home');
@@ -38,8 +38,10 @@ const { HOME_CSP: SITE_CSP } = require('../server/web/home');
await check('GET /docs/drivers: the guide, connection kinds, every message, safety rules and testing', async () => {
await check('GET /docs/drivers: the guide, connection kinds, every message, safety rules and testing', async () => {
const r = await get('/docs/drivers');
const r = await get('/docs/drivers');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
+ // Code blocks scroll sideways on a phone, so each is a keyboard stop (axe scrollable-region-focusable).
assert.strictEqual(r.headers.get('content-security-policy'), SITE_CSP);
assert.strictEqual(r.headers.get('content-security-policy'), SITE_CSP);
const html = await r.text();
const html = await r.text();
+ assert.ok(!/<pre(?![^>]*tabindex)[\s>]/.test(html), 'every <pre> is focusable');
assert.match(html, /Write a driver/);
assert.match(html, /Write a driver/);
assert.match(html, /stdin\/stdout/);
assert.match(html, /stdin\/stdout/);
assert.match(html, /openvibe_plugin/);
assert.match(html, /openvibe_plugin/);