From a2ea15b557245d7738da77c6aecb934654bfa810 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 16 Apr 2026 20:11:02 -0700 Subject: [PATCH] fix: add favicon (SVG + PNG + ICO), fix static MIME types (#613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash-merges PR #613. Adds favicon to the app (was missing entirely — blank tab icon). 1371 tests passing, QA harness green. Review by independent agent (see PR comments). Follow-up commit addresses all three reviewer notes: hoisted _STATIC_MIME to module scope, fixed charset=utf-8 being appended to binary MIME types, confirmed correct MIME types on all three favicon formats. Co-authored-by: tiansiyuan --- api/routes.py | 41 +++++++++++++++++++++++++++++++++++------ static/favicon-32.png | Bin 0 -> 1602 bytes static/favicon.ico | Bin 0 -> 2299 bytes static/favicon.svg | 20 ++++++++++++++++++++ static/index.html | 3 +++ 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 static/favicon-32.png create mode 100644 static/favicon.ico create mode 100644 static/favicon.svg diff --git a/api/routes.py b/api/routes.py index de65440..da3933a 100644 --- a/api/routes.py +++ b/api/routes.py @@ -420,8 +420,19 @@ def handle_get(handler, parsed) -> bool: return j(handler, {"auth_enabled": is_auth_enabled(), "logged_in": logged_in}) if parsed.path == "/favicon.ico": - handler.send_response(204) - handler.end_headers() + static_root = Path(__file__).parent.parent / "static" + ico_path = (static_root / "favicon.ico").resolve() + if ico_path.exists() and ico_path.is_file(): + data = ico_path.read_bytes() + handler.send_response(200) + handler.send_header("Content-Type", "image/x-icon") + handler.send_header("Content-Length", str(len(data))) + handler.send_header("Cache-Control", "public, max-age=86400") + handler.end_headers() + handler.wfile.write(data) + else: + handler.send_response(204) + handler.end_headers() return True if parsed.path == "/health": @@ -1313,6 +1324,25 @@ def handle_post(handler, parsed) -> bool: # ── GET route helpers ───────────────────────────────────────────────────────── +# MIME types for static file serving. Hoisted to module scope to avoid +# rebuilding the dict on every request. +_STATIC_MIME = { + "css": "text/css", + "js": "application/javascript", + "html": "text/html", + "svg": "image/svg+xml", + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "ico": "image/x-icon", + "gif": "image/gif", + "webp": "image/webp", + "woff": "font/woff", + "woff2": "font/woff2", +} +# MIME types that are text-based and should carry charset=utf-8 +_TEXT_MIME_TYPES = {"text/css", "application/javascript", "text/html", "image/svg+xml", "text/plain"} + def _serve_static(handler, parsed): static_root = (Path(__file__).parent.parent / "static").resolve() @@ -1326,11 +1356,10 @@ def _serve_static(handler, parsed): if not static_file.exists() or not static_file.is_file(): return j(handler, {"error": "not found"}, status=404) ext = static_file.suffix.lower() - ct = {"css": "text/css", "js": "application/javascript", "html": "text/html"}.get( - ext.lstrip("."), "text/plain" - ) + ct = _STATIC_MIME.get(ext.lstrip("."), "text/plain") + ct_header = f"{ct}; charset=utf-8" if ct in _TEXT_MIME_TYPES else ct handler.send_response(200) - handler.send_header("Content-Type", f"{ct}; charset=utf-8") + handler.send_header("Content-Type", ct_header) handler.send_header("Cache-Control", "no-store") raw = static_file.read_bytes() handler.send_header("Content-Length", str(len(raw))) diff --git a/static/favicon-32.png b/static/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..25a023c9bf72685d1ec096a6f41836908fd82ca6 GIT binary patch literal 1602 zcmV-I2EF--P)y(#XEMIjm0QTgiYU<1M#5?ngai_5T2&1p4Hd0ag1AYgP$VQO z#7p0bP~ZW=Llg+D5Dy3;0ip!~m3m2<3L;Dh7PS*MPHQLj*mF6Ty%!J1lccfZ8LR8% zjLzA!{(oKezxEcu01n8O%jK^C`+=Q62Q5q_eX6!v3BV$7${6$03l}b&c#n&6xjd+< z&l_WQrw&_tGy;iBfBObp8ziL37>@bAe|U0oGBUlryfM>j)^U|p*jY#oK z%bsY`R1x{AFCrgMRk`lNRWK0{1QAd`P@JXN&(%ShQz|uJ2C$_SLf_hh%v(WFGc{^esBWkln)MN*>!uK zp}qj$gN3SNY9{7iZ#9^ii!o*;3IE0ys&LytmeG+M8DFS{4r7GU=75JrJNf3pUe28l z`ORM!a5m-s(0xVjAM57!!7Tr~7;|Ko$8U3Crp}cm%X}501R62q zvxZ`)S#_7JAOKJgm0U*X?e^&@Kz9e^GtgU9hI?v^|Fg!aw+xT$83f?9H)pZ)j*kvP zPhNPp;!}@cp$4;yuvk~>p|VN^RwrO2Ek(7V%vT{!Ql%4to}%#CdxRYW2D>bO+xvyc z8jk;6nXRNsY6Jow0x$Ko8aTkJP1rPD2EF=c0q0=8s$_lT&7%Vd!X1a-rV%Q|4)Cr6 za(d0j&8(&BUGGR8R2z<6BN?7P)JrB143;PM3fuaj5pi|TH)lgDbzCz8H(D4wzVyIm zzV-Pn6mr6)nMJ}{gQRkWTB!WwPs%S}Rx+y=c6DnhzW~Ohkw1B$m+ybApGN37@ZCuk zDh(oQcdq3R8Q=jW24jtb+dWk>0vPi}ke9!T> zaplPG9gP?s+aug{n@|l?;_sUPn$GbBG%QsU#PfJ~xA4VJz^mt!or8k!K`{?j-)9D#Q*v3u z$$!^bTC!{|`rJ|qIPu4ME?rJg=Rh5Ei^^|LD*c<%nX%Y_Gv^`ew>~SbNkA3yS>g2A zIzM<}mc%N)5k9@UK!0CAF6ZIn*wPL6?i4&DSO>@cqKr=~xuA6;wk1RgSgVYEsFOGT zQ)mC)4Ltqm78>&l%udY^Tj6IfDZe|d>=+PEPNYY(cC2kB6oG2pk;@2`s$<)5mJ?5A ziRvMTerTDvly1i;PA6x(^4hj&szO(X0fh|(pSk&%TFoHhu~17WfvU@ z>%ew^GsgJV+E;+R1~45R9bZ<}VFMbsKn%$L0SVl*0p4N%Jpcdz07*qoM6N<$f^{qX A?EnA( literal 0 HcmV?d00001 diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9ba4cd2e898668dd29d32be5568d7fe3115fc975 GIT binary patch literal 2299 zcmV40096301yxW0096X0Fwd$044wc03aX$0096X073=;0J#DH0EtjeM-2)Z z3IG5A4M|8uQUCw|5C8xG5C{eU001BJ|6u?C0$NE#K~#90Ws^NPlO>Efae-5rXKf{Kf?_%n-Bt01@)R|P4$h=M~0aT29fC>Vv5YWi!^ zZ3}fa$ABR zm~tHFnza@Y+6SQ-TgK1s1K{QRWzwEOQB*+{j4>oha!-OFXo`pnNEEBItdbibi=}GBm+ukPb>+uWOs!!tf=U>M z3hi8pRWd0hmw_V%<KES@VSN+ok>D6Xqz%_;Tm(ElwTZ8BR8}2dy5TF@ZjuunQJQ$`}@EGWA@#L!EP(g@V>{2HJfHAPTk?>~n zFrOBhEG#VJc^>KH4`V}yE0dO74_N(Y&>=7an+@goh|kHf9?FX`y=lSsER_{FQH0VU zG~)K%ZMi#f6!6mq2Vwk7j`{f}aWi5xF9?wK+GYPA!EVJz1fp2Ed~q+s2YgPCSXP&o znVn0htiX#8isyElbak+;dc(3-hsW15w4#`sPh0e71>f6Uq6^dke~Ur@`g_A0p!@Rne930p0&V+ zs!nIK*;{`BtkCV;xrn*&00000NkvXXu0mjfiBL{Q4GJ0x0000DNk~Le0000W0000W z2nGNE0CReJ^Z)<`2}wjjRCt`Fms@OHR~3f8wa+0gApxQV0hM}5nhGLJ2o|*y zH%@CO_Skbdm%SGc$CIS7;~A^#<&4hRv;Kcw_P_QPzyJ=&mdoX@0Q-TRKnE>MBz>y3 zS_!}+aLO3-(+d|aoOq9ma=ARHs?Qr^cBc+odo%)xOMm+YTpJ{$$rz6LzJGXfaxyZ# zy}ezk`YX@#cB^Wls;-@IYN3)1glr(x!uC?2s!mm{h&&oa(T@OUf*?5TdEUpIa|z&+ zmSl}6AdHlKV+CG3GQx{TM%Xu2AdHl20p8RMV4QQ-81q=ERC>@?)q|Smd`;jcxOGtI zG(7Y8R)+3;1b}C}pYzhGDve0-Ov|2V(o_-osxKlRP*u6^!&NX55CjoWKv0~f3spg* z6iJugiI5UNuB}1S=X*(cs)~qw#OEg69>q!+D~VOCO9293$Y+I0q&$0khUYfD0Kl`y zXQ)&io(D@cB~BD2CF6T(p9KD@^(jcXTwakd=akasfE~j*hDrh5orc81)H^Z%I9KQF zREROc_%B8P*nV&V=adf)W!ZImo}s<~--Cs!V`?VmUvD*-nTs)IB?QKm?{|W5y>#>Y*bSK*om+ov^XXW1y60|0g#wT+R{27J%V$ zj(Vtk=f`hzVW!TNCChvjq68W-=^{$wKr$6^Nx=WLQh_Jx8hTeV4()Hi?CQ%>Y=hq1y(0uBrQd?q0Cny zPEw^4fu5r9*?WW?0|vV+f7|<;|l52*MqQ-=+~N#SZYU19E!J$IYyz>Rs5q>(>)pqKA|t)E8dIPl#`7Ag%QYk2CoQjg%<`-CstC)A^x*L>Xsj1iV#fys}8f7ktn0w{hjj?;VX89@`__b(>HPQ{wNN0GiJ6L!a76CU2OV zOE`V5PNCDoS;bnVD+lA#@NNY>3jKijdeFCfs@n7!eww$g5Du1P=@@>${Z**ZUXVV1-yz7amXyFhOPEMpp zvv#a)B@}^b-I2=(m8xUgaF!EKW{K({hkj_8xRh?kC{8D5yYkw$XsSY2hXI8R1)sV3 zm|D#s;;~RmDCE*XjMX>zN(%w#*DRl zG;ba@mm2W;e-k_-EHzTuT?e)k5mD8%QYw`m^gQp^s%n=Bv^7SYrbLAFiq`J%S+>L& z<2&cR(YzZTHO4&doU_XW+UntE#OuIzfHTJU*4kHqy#_EH9UWg*)ng*!iL?^WYqsJx z(`vnl5fR@x_p*pQeDUJNTGNLDY~8x`fQUQ++zsT`;kzBH0cS+y`RVEDBR~wu{{acy VvjN^=|2+Tz002ovPDHLkV1f_sHB + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/index.html b/static/index.html index 9916ce0..e670f61 100644 --- a/static/index.html +++ b/static/index.html @@ -4,6 +4,9 @@ Hermes + + +