From dd038f8e4643792f8923a763b3ef7e52557e070d Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Tue, 28 Jul 2026 02:26:54 +0000 Subject: [PATCH] Make the server installable as a home-screen PWA Adds a web manifest, hand-drawn cup+frame icons, and a presence-only service worker (no offline caching) so mobile browsers offer "Add to Home Screen" for the server UI. --- server/app/main.py | 9 ++++++++- server/app/static/common.js | 9 +++++++++ server/app/static/icons/apple-touch-icon.png | Bin 0 -> 1245 bytes server/app/static/icons/favicon.png | Bin 0 -> 501 bytes server/app/static/icons/icon-192.png | Bin 0 -> 1382 bytes server/app/static/icons/icon-512.png | Bin 0 -> 4189 bytes server/app/static/manifest.json | 14 ++++++++++++++ server/app/static/sw.js | 8 ++++++++ server/app/templates/app_base.html | 8 ++++++++ server/app/templates/base.html | 8 ++++++++ 10 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 server/app/static/icons/apple-touch-icon.png create mode 100644 server/app/static/icons/favicon.png create mode 100644 server/app/static/icons/icon-192.png create mode 100644 server/app/static/icons/icon-512.png create mode 100644 server/app/static/manifest.json create mode 100644 server/app/static/sw.js diff --git a/server/app/main.py b/server/app/main.py index 2e7aad2..a0d2b7c 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -18,7 +18,7 @@ from __future__ import annotations import logging from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from sqlalchemy import select @@ -60,6 +60,13 @@ def health() -> dict: return {"status": "ok"} +@app.get("/sw.js") +def service_worker() -> FileResponse: + # Served from / rather than /static/sw.js so its default scope is the + # whole app -- a SW can only ever control paths at or below its own URL. + return FileResponse("app/static/sw.js", media_type="application/javascript") + + def _device_credential_redirect(request: Request, db, allow_legacy: bool) -> str | None: """The on-frame manage QR points at the server root with the device's own credentials (new firmware: ?id=&token=; deployed firmware: diff --git a/server/app/static/common.js b/server/app/static/common.js index 56e2ace..a9d6372 100644 --- a/server/app/static/common.js +++ b/server/app/static/common.js @@ -58,6 +58,15 @@ } })(); +// Registering this is what makes Chrome/Android offer the "Add to Home +// screen" install prompt -- a manifest link alone isn't enough. Served +// from /sw.js (not /static/sw.js) so its scope is the whole app. +if ("serviceWorker" in navigator) { + window.addEventListener("load", function () { + navigator.serviceWorker.register("/sw.js"); + }); +} + // Shared display names for widget_type, everywhere one shows up in the // UI (Layout canvas, Add-a-widget buttons, button-assignment dropdowns). const WIDGET_LABELS = { diff --git a/server/app/static/icons/apple-touch-icon.png b/server/app/static/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d2e387c39d2f414aeede05a672b19f3e274e3e0a GIT binary patch literal 1245 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQWbofn|xOi(^Q|oVRx`=H50CX-!=A zf+Kce>>AnClS^C6I_x+&dQP}6N}2S1-t7O?S{Dv*v~kfBt>@TY3Aszq7xcPFT0P`Ny^5+^2sX z+ZEPVU+X{gGcone`xcd|wyDJkUH7(c>aW->_~_{LzsIDx)8Gs>;+vH!qx#OHnmP*{6&T6*0OUs0d=33sfm7P>={qbtM)Q*>P-aO7wW_$T< z;-WHn(MK8Uc`tQ87ijf&>^gGs-8aLO>+5g1&$Wp9;_1BCx##unsmtf}wcJ|U-v4yz z+Gq_?_t$a@J*z7>+O0Zz(aJ63)t~s4{Y`~iUMt&vJ9FTd&Evqc7D@VjGtLQDvp%-4 z&GMQ1Q+sdvN-_86H+)_#4w76OAv670T7~@4QyE7e-LRIM-B^1?dvR!2vb~LMvhkAs zNvT_m3h!t?exUyQ_lhXD)q=-gt54egTO-CzTj2PLhdVF2zcrfmHT0&W_$h1o&96*< z@yDe1c^?yH->d~Ba#_VDW$g%U}d+200?*Dh~Sqp#7Ijqs`zOS?5NaC!c z%Ew)J&RKFzxo9)L)lsHnk}cnHS-*|ddN&_CEVxp5<89XRJ@W!zSqPkd@4O+&=>8uk znM6K5!`t<(>P|9@R_ARF2|t{ZY`S>=bw?StMZX?w5PJXdK;eg}yI%L}uhidDS*ulW z_^SQgO*{Dcv=jNwDkC^xr?Y*5dKI`j(N8#QP5AxO9Qn;jM)c9|bnWO|al`S*xPjceUqV zgUoXo#%&+%Y_D?c&u_IjB%>lU?`WGvlj_R{n&&KmIRfijCiI<%BS1X_3)C4+2 zM1Z}0iQhy0kTYil|84nv`h!qw?YW5$Pd=7-j*-)`yL)ol>P|`r7F!IS Lu6{1-oD!MhyRV8aby z6JR50z~lbgn+F_{rqezN=8C`Gp6#p*QOkSzA@Czz80`sb(D+IbvU~)%0c-+nzzisz zO5^rDr(uWzn}A#)ar@G~t$-L1XJk!CeEoWR7WK8jMoF5R3!z*Aar@G~t$-FnGr(7aYyxbETVgFh6)lLoU>*U;vK4V}(aSW-L^Y-q=(#u{Ftrt08 zPjleDATh-?bqj;-fzzczGSPCq?w5Mb-Sf9O&^6(|ztqWXpFTakz22VHsn)8B`GE(M z!YqakHHJl;4H@XnZ}G3~|NMOX`Stw$b^q_CCMWdD8UMKSDRIYl`wD)h*_U~WH&t?%Xr(if4+AmYOy1 zba!0y?(5&?O=ZZtQMIBXEbZAX)&wS6pQ*X;?lemq4t>nLc zmJbK^K78|c;nujSynH4`of}CrqV_)sI{ab1dV}>gnPs|427hu{3J%Lj-t_;u_s5xY7*B0HeC5*&;lua+YZ#PyF3;j{yK&FBlRx2P&iq>@ ze_jLQ`y=BW>nyoKleMq_)p+{&gN&t*>20G+-_D)O4OnE!Ea1!FBFu1+#leIT%3M?* zT=PoYb5G$nwNJept{mL2)^b2~!o&LH_!ia&tp7D?=O1C2@Xtx#^PagNqssmRTZ6s_ zx-b-+f644&yJ@S(o$%%S4AWfp3m!{OujguI*s-$AvgBv==C*=gx{ z^XtHxZHwzQnI80QJ-p}3cX!)g)0G&+*3{a)=&=#+aP{WjzgF_k#QuLvIU2ll-G5$e z-!I~=U}kjq15lZn!D$d>H9PZ%cOmbAsizs%$9_I<_CuWM!LnNdX|jK~88>8nW2`@7 ziW2%r5dmXb*D|F4HwgFd{Qc?+Fx3LefK&RpZVjlKV9OF`9(&DJ3<`yV3D1>iamMhy{(cr-;_^T zwu3w6!*TO$j%f}q*;dSVtd;T$MGC&mKOUFfIPddw9#G=Q{lSp7cZZkp>(?eV3>%he z-eb@ByVW))|C;juqautkXZCHD>{k^)PDn8K!)!!NX|I2^&RDx|`24VHE%X literal 0 HcmV?d00001 diff --git a/server/app/static/icons/icon-512.png b/server/app/static/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..99bfaab3c89c868585354ea0bf08dbe84df6e61f GIT binary patch literal 4189 zcmc&%dpOkj8h?Ld8keRJMY*JgqB@%B;yQ+eY8Sh16){eiEnQGia{Ec7a*0-{Xr{JO zw9{0|q?lP#O~r{rkvk2E5hFCn<$PWCIqiAQo;}a*bI$%X^S%DQ@8|uzpZ9%_J3HD> zQ<$Rw0Mphw*nSH@Kz|7!M?ycnhh7Z=DCw@XwRSy}{-LhMO`~X5>^Fg=c_vfLm&p|+ zn@&lOE{V=1y?JVPmYsF4o!JxU!+NFHH<+jVwyCGQC`kpBS|YR60E!m^=$rz;>F9of zJb;|;|92uOTTQa6p&(*V#0(D^DPZV9z&Xde;_+9$K6yx-A1%zza?*v?uE^}*5yL(T zIB7$$+n%SX^0=QY2@>6RR;0?|>tu;wmu1wmjtD@l{0P^-wE^n-n5tY0WZM9mB+fvH zIRMagFo39kJ8{@69}ji={P_jkD0?ZKNqZ^P?);Aje*4NlamV|>K0>4Sm&k6k(>@rg zAd-jb!~PCKBVFVKQG!M1C)Ib|6xcLa$>O%~L`IaPhn6u9i15M79LVuo;>S0rK+Zn# z@diG(g^Oil7^xn~_jz*i=kRB5PJ&JHjBU~rQHaa$N(*UAIibKI}n zfYNh*e{P~MQ*_=}52G)U5rtAf;JD>C@{@xG^4wHB7s*GV?*?rbBCR7gY`!a4W`^c?vQMU)qOk}}g zTFWhg^!=mEfTb8em>lmDq&;F>;32e(p`hJc8ACY&3A5YM{DXn!8sPlc8+E%Jjq}Yr z3^dmQ=iaNR+ad|`cI=B#1$!0Py>t(w!(ynU?31dU64*M-R+kryF4^YftBTR%>tyZ9 zBvv)yPQJPr{rk8qzMsiC0=Nq^%D3N#jR0LlfDxB)v{ad0 zkjY5_d{x$TvS2jWO{(gy0+I`UG6D(D=bmMZ0udfjd>L2Bqj!&5#_W=V{*wwaWwOJX zi~9lvp+p!GB+6_N3J)ao3KhR20FC-rhS$DrR&U!IB%gP_zG$OI07H(Tv9@WM=JRfy z48W%bX{%>c(o#e=Mi{8Ihn>vrskYze<*)!63FhPzTH1nSO~o>K=;vpx5{3LwZBoyW zM~~epjCvs2l4mz4I%bSPxC4-AzVPQDfxeHZCTUzR;?8DmO>J+oDFv*%z)36vpc zt8?wGP3RkG=s==%VEUr512l}jnDnIEeeA#rBBUd?%pFZkl26oy9a~Vf3M2`*UekQbs_9%sZc4f4rC?^>rmhm@L;xOw}z@vJ){;v zjF#KVFov_{uI9kpOlqZ|HS_G;Y&A@JsH?o0e7yT-%>%h+``^p}zf-QqrIv!Ec2hpl zW-N;xmZui~{1E@y{jR#CHwnNZM>7o{gSiqc`A$-vs>Cw(Cu}tkFkf0kwiRo^nSi%j zS9Ov~6U^bO@A~gjN_OQb!&fc3&tF>b+H8TEaGD}-QPPIeTFmMBYb_eu=s-9K8A>q!)znt&o9>$>d-Fc~MSjY%1K<|Jqz#Oi0+R?7z(aSm)0Q7e^`%h2j-B)^s zR!*E779a8FG1Z^VLxbxQQ>;^=S>g#W#Z`U8-_27|xL+0+mtI6y)ZzILWPsz)(lqLR zHfW#LL4XBf6X&{VZ<5RbR8S7^5Aw0Pg$(de#zK_T<-nX00G9zrDp*e z4d=5KV!*GnrCW`aF9SL{_*26H#>I^ut;ic6t*sL*!Xge2+&-+_A=pD#@47h8T(n{l z;jn}P_<{n9NM0ycyL$!gtqn^SgHrzASJ$|i5rD->_UNGS?mvV+dB87k-+c9<#;LUc zs>?(5llmh%39l9)^EL*p>2##e!oc}i`@FDT=|MG?AsRexZ;Eu^rWLOTpm2TVfh4b_ zhp=L@u9}q{+!y)IkY7LH7?|poKWLR*K--HfODjym{FDJs9(a(~dB>4WnJB|Huxi|b za{E3mLuM|omvufi;T^^uc5eAY_66w|6&qubKk=sZM8QJ+M!KqO62bqX^7(@I=p^rG zW%%%;$vJ8&Pk%%7KRJOv&nrk~i}OO56La{^M;K>11{`{BOk#N#BWJlJd9chIn zqq~r4Q{8xZ>8+uc4Ti}!uzA+;VW$>`YYP6BtZ0o|;Bc^%PpqwE%hzm2HTLsl_R;ZF&9{9KppB>%sI#Lli`CRVmp%h>99=`@j*J`xr-sWD=)NoQd0U*E5 zYgmeWz&+X2a2hdfdu*f+0q_gzBl!q&_V#}17#mMC@epwkWKC5Fv5(etIGu59$5qAd zfw-S|a}lX$wHdiSym?y8MPzqwd5Dol>uWL4XH|24ysw?uIvwcBGdoG@Lr8?_%lwZ? z;?=3W#>nVGyZV{b{=4WOsa~U^(l*Msqzi3vi8Ygs#nW#TvR}CKS4!PKw1a;Mg^o`? zQNKtWJyvf|5-h+Z*R;vWTg?^0HrZ0(^J+VfdUz|k>W)t_)IBU4gXo(H_@(rr38?}G z2$=0e5SS_xUQr;;hIs06PsVg2l$#i&@O#n?iQr;tddIlL-wdf?tM2o?=-bjq*R(MH z49B#N!k#oX5!RcAl$~o`nqfH^>;YyOjuUFkjr*wtUDhIpEi&q@LK z{!8oeH`o3L%ABRGX`17W8Xl`^Mm@5BJ<=cKV!F#ZEt8b}>$I`bG_@rG#MYFr^l0*@ zYBGGdG@b_c%umd(VYiJOoHgn<%^>oFA8|15(MhKNFBtM`9o8SB-LG9b*0xc?J`e8f zn{jR6<4}0t45&{%U|1i8GRZFkWs^44FDKB?E8O)DL2Y!;7vhQ2i)8o self.skipWaiting()); +self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim())); +self.addEventListener("fetch", (event) => event.respondWith(fetch(event.request))); diff --git a/server/app/templates/app_base.html b/server/app/templates/app_base.html index 48b9793..ae85d1f 100644 --- a/server/app/templates/app_base.html +++ b/server/app/templates/app_base.html @@ -17,6 +17,14 @@ })(); + + + + + + + + {% block extra_head %}{% endblock %} diff --git a/server/app/templates/base.html b/server/app/templates/base.html index b3e3afc..45a8010 100644 --- a/server/app/templates/base.html +++ b/server/app/templates/base.html @@ -17,6 +17,14 @@ })(); + + + + + + + + {% block extra_head %}{% endblock %}