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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
Q5leRkb!l{rHX=dmVnREq3_Ms+4m6511c1Y>d
zBtm*YqaNUEb}>T6Qkf)oc)V+4gjZCP`)JA0yryq+$&2BU#}oMSy^Il@4y%{f@PRX`
zSh|p&P)i^#OK^bx(uo=#+g!#yKjYq>3OwcLDqeL$f@qTLb$XB+r4kiv-d@ATyOQ|z
z))@MGYPjIFRlNRXR6H#%UyNKubVO62%2lF@LkFtZy$_5(V9*@Ls-wryErs(5OD4~Eb!kSEZ?;rpU`j7Q
zH93HTdnPctcO0d$JgU@qjZ336tmSayw`w>@?YD8cPE)NNj1N!zA&xKom*y}Tp_|9a
z%)UbvY`C4v7k$f_ZlAhb(!8lt5tZY;h3-87dPSCiAxH}Pzkd9t>2-*
z&y_IBfIGaq!mW1|yY_%F0KCYpVg6tj%87a8Ps^bMb;3$;vJfPvhL8vNj;VPfl}6v(
zHbw!uD22_B6p+f6dF{-r=QM>GuVE{{L2%DgPa?#X3cuM(d8jAbAF%(aZ82;gmOumz
zn9&)3f|h*rN#N9%sFCxZL3i_ZO*IDLVACRNCgL^h;%cK+Cdqn^Gb_^{=Y2*3xpq!f
z`fG{7cOt6;5ru3>?8!MEgLYGZ(?y;4P=U-afW*tgh?V!EXF(@wjNqjSMsMx56qOmN
za&+MVE>T-{Y7-=yPO=i`=c0LRqp)S0j$F5M42-PP$aU9r@GYo|i}cFjoqYCfRt0^
zyc|7L#@N9k^}!8De{bbBWtJ%*@u?+lLlgAx!bl!5
zN|S0&Q}ZeNKpLa<%hcmrUdiQ*>iojpRqPvyV>zvK#oH4&@x@%?XnrmI3P&jxHh!4G
zf3AX`{x&YX;J9VLjTZnz^Qa`o3eEot^A~Y{@vRNO#_z-CHuovjZ(1Afj7SKn8?C?&Q9?
zK1QJ!*{cbx7>uDa!*Yq7aQ&{505^mvkj|!v%lRIoRVPLm7qs+Av2^3k
zt^~Tes7xCCFcmSHFCkgjZFn@DBws`Pp#1z|fc7OZM)w$QyS<1*6J-p}D`KEGhB!S);2Y7_5$6Le8`qVqMCN
zo68DDOChHRW5|SJ06nZs&*NFlmc4vFj!|}ujB)#YP)`+mnD5vRY=4gNp*?~34R)YM
zqD%US_^LAn8kunl$Yr&ZbS!=~`cJ$Z*MIaM++Z?KecOg6N1=1v7Ifr*la5`AB@24c
zo>p|`SL8|oJ9ccRyKoE5jgoeM3EM<*g&+gkQm44QX}?mU_2Tpp#SoqBbgBc-9P<|p
zaZrZAmXVz?VB7A)c;vBXF-8xc;4`V1W70?>(=;x-;7nxapD0Ze2iuyG=_Js=j0zCS
zn(YEJSa#$pbac2YXW`S@ubtL7Ej2P(#UID2Sb-&2*t-(t_I8X>P+gQh_3UqP?++gk
zCJkfqxi1H%n9>)+>1$6yru%5#@Mi2touJ$2N4DbG=eAKUcPdP9%blc)X(^RV3G%!A
z$Yapc*Uz{me(8CJyY!WIPm(y9Iu-Qnbuj>F%+?lkoDbaQ8yrFUp5
zas8xMWWWCa={vDBMNRU6aVci*+yP#C_VY3(W6LRlqhf_>GkSpFbo|kpte?af6BJx3
z6~j6y)-=H-7VATcmtf_QMa
zFJr~B#dzmAXX1z>jzC9emtBX8wk}DA4~#i}0-?Mj3>opn$zL0f>=`qH4no2$0#PM+
zinAzy13Au=KumQ$)xvhg2qy+h+_8z--o80hfU}sCA}{EhGZ$B1aWO8rVjafDCrl7A
zAm#;QxR`5C(AnOG^WOC~9JBgZ^m1HQxaf8AI9m20XSPGM6rL}5^;3TfLDP@d!0m^RM;wzhVbnbQ&-UES#H?!hb8tidH8
zdcRE>g4!H*s8w;yD5ji!)*G>w-|`0*4ATAksMKz>cXSyp;mh)3R9uOkqwVY~WB8=0
zA_YllhTL}qx#-L5^n|5q0`paGghG*Deib-g-5tWL&+P!=Yo@5CYyL-J)&eA
z_Nq(W`h>5qygsjRlcc)hjg;v=iWsP~ju#-5d$H;s
z5bB$;DDnMZ2Uu61G3gZJVnf|Ir6)cq3Q~@har)_N@u3U<9@pRW8JP<2Jo_BH<~6To
z0PVLMU|CssJS8idtMLw@Er&%52W$`GwC}jkTVYV&53==dlMNgVr~s=&>ZU-rbGEWj
z8mW-w8*Uf`MFZlCL>xSas7i03FL37uy#Ipp@x*U`hw-ryocEq{(bqrE=DdXWmKG&T
zqQ&vD?&@P!;oIN%7aTmmeXqHmTRlwi^pLi;4h$_>#&ZC-<#pm7Q5{{q!8|}Ve16G8
z3bR|jv~-EME+3rr1G`ILAvI0A7;P|Qk~0}yTp4cr+|4}29b~zet44=yxV+_^f!s1_
z$`dNiSOwIhk2&516b~&Eg?D(^>*uFKI0f8Alqt>82CqorGLnY0K|~(V@B&G$5z0Lt
zS5UK_8ct5#7CgHs=K;t83XZ$i~Z|QmY6no&o~r-u)QMKbRxzA=o$k89l~#Z?dOK;{P*az2bx
ziNvAZD8b!G7ElR7?8*B;e%x`P_yT#Wp0)ey=I|9(1G)ZTAoj_
z!Bhi!YMbb5UYCoL$d2vc72P<&?QGppw|GmohU;sDb7Pf!(!f7ea(fE9>*sVap13sXaT9
zE!1$}zyVv98X(Ojw;t=jlHf&$&7Od`SBR8ovu2
zoz6QYs4iP-2FQ^5RBX0w8Xl@@LnfJ}1X~6-Z+;rDKjVxLNVKS1pSg4U`RmG48~;B*
z!p;$myaho0+^!xpqbO#t1qPkYaB3H&YBGnBxoc3G9Hqjv7hsQRP8y_3C$qevZsYkw
zv)fc#r|mXu24sgZb(jM5KQ@e
zo6hZGLLM?x3gb3N%})~|VA(Yq3h!a6m;!Qhv32ED{MGk!DDYRK(cdibevls8*VT#k
z9PbgiO4N6R4x7ETci6P)AuAg%eSLlS<ejF_QFRy9Wi$~X%f`C
zJwD$`9a-{qSBcaD2xbH}E2~IP)J5yWKpyS18bvqna9y&dek|3_+a_IXVn7W$vq&6Z5JYF?RQCIb?jod698Wgd711!L$
z%yZf3)X!z~6Zx0Ed=0->?6i)$iuSPM@++^hZAE?m+!#A+N#yL|%Zw($1L0kq@P>Af
zf|)M063*gEXprG#y;a^yc*!L9zxtYM>%xn^CddXAl%D+E@AxH%KUaCpU)zr#S~21v
zTfa@FCc#{rTPeb$B#S*kqlZ(qr8gLSAaDQ^7TzKANTazBi3De9dSB*9&CyN)FCB2`Q(#0;e-<~cm90ad`zbZk0_jr
zi7*)x+yjI-thZZRs3U+4`;FI66aQ1Z#bZ&hO!4w$$IhMj;6)eP{}?n2FEWA;^BRzL
z*}%Mc_`nA)#JTS{2g{c&)wQqfw_8Tv$jGUc840n|<~Ug~W~Sw6D&BC5T%8l$c-*#q
zJMO&moA~@~U&O9m{Qb@}X-2;M82xj>8vcuk{>?=HJgtASvTJVwh*u%eD{P&{JSKz(
zY9W|)83Ko_1&B>=;qZN}n3i?5oNs>m89c;|-GiGpVLvxZv!zzJW*ZZl$FqX|SpiIW
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, /^