From 16cbd63a040b271de353f609ef112aefaa74b740 Mon Sep 17 00:00:00 2001 From: oliver <54379826+ok@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:19:47 +0200 Subject: [PATCH] [feat] Use the blind-relay mark as the pages' icon Replaces the hand-drawn SVG with the armband artwork the StartOS package already ships as its icon, at 64px. Served as icon.png beside each page, with a relative href, so it works behind a proxy prefix and survives MIRALL_RELAY_ADMIN_UI=false on the members page. --- src/operator/html.js | 18 ++++++------------ src/operator/icon.png | Bin 0 -> 6316 bytes src/operator/members/page.js | 3 ++- src/operator/status/page.js | 3 ++- test/integration/admin-ui.test.js | 11 +++++++++++ test/smoke/docker-smoke.test.js | 2 +- test/unit/admin-ui.test.js | 25 ++++++++++++++----------- 7 files changed, 36 insertions(+), 26 deletions(-) create mode 100644 src/operator/icon.png diff --git a/src/operator/html.js b/src/operator/html.js index 3bc5f3a..93529f2 100644 --- a/src/operator/html.js +++ b/src/operator/html.js @@ -12,15 +12,9 @@ export function plain (value, fallback = 'โ€”') { return value === null || value === undefined || value === '' ? fallback : escapeHtml(value) } -// The Mirall "M" and its orange square, from docs/media/logo-*.svg, on a dark tile -// so it reads on light and dark tab strips alike. Inlined as a data: URI rather -// than served: both pages live under different prefixes behind platform proxies, -// MIRALL_RELAY_ADMIN_UI=false removes the status page's routes, and the CSP -// already allows data: images. -const FAVICON_SVG = "" + - "" + - "" + - "" + - '' - -export const FAVICON_LINK = `` +// The blind-relay mark, the same artwork the StartOS package ships as its icon. +// A relative href on purpose: both pages sit behind platform proxies under +// prefixes they cannot know, and each serves its own copy (icon.png beside the +// status page, admin/icon.png beside the members page) so MIRALL_RELAY_ADMIN_UI=false +// does not take the members page's icon away with the status page's routes. +export const FAVICON_LINK = '' diff --git a/src/operator/icon.png b/src/operator/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d72c8d5d4972bc6817e86254f4e9e8e7daf5d445 GIT binary patch literal 6316 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 i0R<_bm5a~D!2bimRi_f;(;1Nf0000 { + const relay = await withHttpRelay(t) + for (const path of ['/icon.png', '/admin/icon.png']) { + const res = await fetch(urlOf(relay, path)) + assert.equal(res.status, 200, path) + assert.equal(res.headers.get('content-type'), 'image/png') + const body = Buffer.from(await res.arrayBuffer()) + assert.deepEqual([...body.subarray(0, 4)], [0x89, 0x50, 0x4e, 0x47], 'the bytes survive the trip, unmangled') + } +}) diff --git a/test/smoke/docker-smoke.test.js b/test/smoke/docker-smoke.test.js index 5b50fa3..03e9e6a 100644 --- a/test/smoke/docker-smoke.test.js +++ b/test/smoke/docker-smoke.test.js @@ -106,7 +106,7 @@ test('the image starts, serves /healthz and derives the expected identity', { sk const page = await (await fetch(`http://127.0.0.1:${ADMIN_PORT}/`)).text() assert.ok(page.includes(expected), 'the key the image derived must be on the page') assert.match(page, /Settings โ†’ Network/) - for (const asset of ['ui.css', 'ui.js', 'format.js', 'copy-button.js']) { + for (const asset of ['ui.css', 'ui.js', 'format.js', 'copy-button.js', 'icon.png']) { const res = await fetch(`http://127.0.0.1:${ADMIN_PORT}/${asset}`) assert.equal(res.status, 200, `${asset} must be in the image`) } diff --git a/test/unit/admin-ui.test.js b/test/unit/admin-ui.test.js index 0f94c91..8a5c235 100644 --- a/test/unit/admin-ui.test.js +++ b/test/unit/admin-ui.test.js @@ -404,16 +404,19 @@ test('the hero headline carries the mode at the same weight as the verdict', () assert.ok(!hero.includes('class="pill'), 'and the hero no longer needs a pill of its own') }) -test('both pages carry the same real icon, inline', async () => { +test('both pages carry the same icon, and each serves its own copy', async () => { // An empty data: icon silenced the favicon request but left the tab blank. - const { renderManagePage } = await import('../../src/operator/members/page.js') - const icon = // - const onStatus = renderPage(status()).match(icon) - const onMembers = renderManagePage().match(icon) - assert.ok(onStatus && onMembers, 'each page declares an svg icon') - assert.equal(onStatus[1], onMembers[1]) - const svg = decodeURIComponent(onStatus[1].slice('data:image/svg+xml,'.length)) - assert.match(svg, /^