openvibers/openvibe.bot Commits

Bot: readable accent fills, and /favicon.ico is the app icon (#58)

OP OpenVibers committed 10 Oct 2026 · 0f0d57e413 · 3 files, +10 −1

Accessibility (WCAG AA contrast). Filled buttons, chips and badges painted with the plain theme accent and white text now use the theme's readable pair: `background: var(--accent-strong, var(--accent, …))` with `color: var(--on-accent-strong, …)`.

White on the default accent `#3b82f6` is 3.68:1, under AA's 4.5:1 for normal-size text. The strong pair is 4.62:1 in the default theme, and every built-in theme defines it, using dark text on the light accents. The release UX check's axe run flagged this as `color-contrast` (serious), for example on the search buttons of openvibe.trade and openvibe.deals.

The change is mechanical and limited to rule blocks that set both an accent background and a white or `--on-accent` text colour. Bars, dots and outlines are untouched. A border drawn in the same accent follows the fill, and an existing `--on-accent` fallback is kept. Affected tests pass (`ov test`). The matching shared half is openvibe-shared 2.20.3.

Also: `/favicon.ico` answers with the app icon (SVG). Browsers request it on their own, and a 404 page links no icon, so it used to 404 (flagged by the same check). Covered in `test/legal.test.js`.

Changes

ddiff --git a/public/docs.css b/public/docs.css
iindex 9a4e68c..6b834c8 100644
---- a/public/docs.css
++++ b/public/docs.css
@@@ -32,7 +32,7 @@
.docs-main .field-label { font-size: 13px; font-weight: 700; color: var(--text-secondary, #96a7c2); }
.docs-main textarea { width: 100%; box-sizing: border-box; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; line-height: 1.5; color: var(--text-primary, #e6edf7); background: var(--bg-primary, #0a0f1c); border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 12px 14px; resize: vertical; }
.docs-main textarea:focus { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
--.docs-main button.primary { justify-self: start; font: inherit; font-weight: 700; font-size: 14px; color: #fff; background: var(--accent, #3b82f6); border: 0; border-radius: 10px; padding: 10px 18px; cursor: pointer; }
++.docs-main button.primary { justify-self: start; font: inherit; font-weight: 700; font-size: 14px; color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); border: 0; border-radius: 10px; padding: 10px 18px; cursor: pointer; }
.docs-main button.primary:hover { filter: brightness(1.08); }
.docs-main .error { border: 1px solid rgba(239, 68, 68, .45); background: rgba(239, 68, 68, .08); border-radius: 10px; padding: 12px 14px; }
.docs-main [role="status"] strong { color: #22c55e; }
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex dfc4a05..ca86834 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -71,6 +71,8 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
r.get('/sitemap.xml', (req, res) => {
res.type('application/xml').send(seo.sitemapXml(PUBLIC_PAGES.map((p) => ({ loc: `${config.baseUrl}${p}` }))));
});
++ // Browsers ask for /favicon.ico on their own (a 404 page links no icon): the app icon, as SVG.
++ r.get('/favicon.ico', (req, res) => res.type('image/svg+xml').set('Cache-Control', 'public, max-age=86400').send(appIcon.favicon({ site: 'bot' })));
// The web app manifest (openvibe-shared/app-icon): the same icon design and start URL every OpenVibe site ships.
r.get('/manifest.webmanifest', (req, res) => {
res.type('application/manifest+json').send(JSON.stringify(appIcon.manifest({
ddiff --git a/test/legal.test.js b/test/legal.test.js
iindex 47e4a10..cebd41a 100644
---- a/test/legal.test.js
++++ b/test/legal.test.js
@@@ -36,6 +36,13 @@ const { boot, check, done } = require('./helpers/app');
}
});
++ await check('GET /favicon.ico: the app icon as SVG (browsers ask for it even on a 404 page)', async () => {
++ const r = await get('/favicon.ico');
++ assert.strictEqual(r.status, 200);
++ assert.match(r.headers.get('content-type'), /^image\/svg\+xml/);
++ assert.match(await r.text(), /<svg/);
++ });
++
await check('GET /manifest.webmanifest: JSON, OpenVibe.Bot / Bot', async () => {
const r = await get('/manifest.webmanifest');
assert.strictEqual(r.status, 200);