From 3e9b6095dd85ed1fb7e68be9067d84ea7e9fc111 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 21 Jul 2026 08:25:41 -0400 Subject: [PATCH] =?UTF-8?q?m4:=20PWA=20install=20=E2=80=94=20manifest,=20i?= =?UTF-8?q?cons,=20service=20worker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make ThoughtSync installable ("Add to Home Screen") without going offline-first (the Android app is the real offline client, M5): - web app manifest (name, icons incl. maskable + SVG, standalone, theme) - generated PNG icon set + apple-touch-icon + favicon, from committed SVG sources (a linked-thoughts constellation on the brand tile) - minimal service worker: installable shell only — caches just an offline fallback page, never the app shell / hashed assets / API, so data stays fresh and deploys never serve a stale shell - register the SW in main.ts (progressive enhancement; failures ignored) - index.html: manifest/icon links, apple-mobile meta, description - backend: register the .webmanifest MIME type so it serves as application/manifest+json - README: note that install needs a secure context (HTTPS/localhost) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- README.md | 4 + frontend/index.html | 11 +++ frontend/public/apple-touch-icon.png | Bin 0 -> 3405 bytes frontend/public/favicon-32.png | Bin 0 -> 781 bytes frontend/public/icon-192.png | Bin 0 -> 5298 bytes frontend/public/icon-512.png | Bin 0 -> 15062 bytes frontend/public/icon-maskable-512.png | Bin 0 -> 10252 bytes frontend/public/icon-maskable.svg | 24 ++++++ frontend/public/icon.svg | 25 ++++++ frontend/public/manifest.webmanifest | 18 ++++ frontend/public/offline.html | 114 ++++++++++++++++++++++++++ frontend/public/sw.js | 36 ++++++++ frontend/src/main.ts | 9 ++ src/thoughtsync/app.py | 5 ++ 14 files changed, 246 insertions(+) create mode 100644 frontend/public/apple-touch-icon.png create mode 100644 frontend/public/favicon-32.png create mode 100644 frontend/public/icon-192.png create mode 100644 frontend/public/icon-512.png create mode 100644 frontend/public/icon-maskable-512.png create mode 100644 frontend/public/icon-maskable.svg create mode 100644 frontend/public/icon.svg create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/public/offline.html create mode 100644 frontend/public/sw.js diff --git a/README.md b/README.md index 27134d3..7d31a90 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ Then open `http://:5000` and register — **the first account becomes the - The app waits for the database and runs migrations (`alembic upgrade head`) automatically on start. - **Image tags:** `:latest` (stable, built from `main`) · `:dev` (latest `dev` build) · `:` (immutable, for pinning / rollback). +- **Install as an app (PWA):** ThoughtSync is installable ("Add to Home Screen" / the + browser's install button) for an app-like window. Browsers only offer install over a + **secure context**, so put the app behind a reverse proxy terminating **HTTPS** (or reach + it via `localhost`) — plain `http://:5000` won't show the install prompt. ## Milestones diff --git a/frontend/index.html b/frontend/index.html index 3d7f9cc..5727c4e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,17 @@ + + + + + + + + ThoughtSync diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6b05fcab833e45aebfbba5dd006c5d0823ec15e4 GIT binary patch literal 3405 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 LNkvXXu0mjfP~2v7 literal 0 HcmV?d00001 diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..03161198f5e9a3fbe717828bcca5403baec6deba GIT binary patch literal 5298 zcmZu#byU<(wEynXi@-{UNOvPYLb}1ALmC97WC7`3aF<4;5s;8nQo0s!LFtlIY8NC# z8i7UXt?&K&&YYQh&S&P{Ip@aQ^O-mUJ#7jIGXwwt3SAuyBfRYSH%W={tI^^M61*UO z`cPW~xcN5K#{hLE|^gm=znV9yQ{mM3dEup+9*jrT0`LQJ8Zz9xMrdP%9-xftq-T zy0`M7{?o5;8`IkDuHfm)<74B-_HC5=iQ?H(zzS+p@D5>47Qo*sY)p z{=py{jDd;`aVVVe&}QB~h_3%(H=b)?gs1}suA+U4djJ>T|oe7rx-gk(IeVPO-f9S(& z?x`B(`<}$bTJGm%p_zN>Q35x!2_efQkn2>$y)Hno7 z56*0P-jXMSDQOR$1H*+P%*>7@Rap015o)dbSD8DR0(wy@@^6QFM7mAFy`l0P9XuDc zjs7|{lR%;KZ5Oo*ns2+e1?;VjE1S|uToZ{@jCChpv0G<*O(JxN(SN>?11)B;8~Ir2qE`nOkTe#MLm#Zxt?{O(CGM(D^jTD7e8>F6i=m4tT_t!CN@ zcIy~^EXXLzUv1x!1#%veM+i@`AgB^O1V!3^7&25iJXTF{n=0@oY_pW&NOh!&+T}Xt zoiY3BPvkQEgg?U$r^B|DXxHycbK&*k1FQp578hxMU~mdM(4<6BJj}aEuN)X zK;E2cYf32lOTeInJ~uVzI+Qtg!q!^6$bTPX4!1Sq6@#S=iCsSC5Yu+Ho;WPHAW%`Y z!CDj{bU2@d8PBQP6qe2Kl+xaUrCoRH(9lQimR{Nm`x4bfeRefE*JPe7xmO;2jl3!1 zPcO2FZ7lL_9gEoq(ej4auydrw$A%~x6pac4AlSIA6fNNMI1zF`X=&L zq~zz>kR}UMq*$gfWBOgDflC8_rx^?TVDprUY7y*fQiJhLm z30>0V2EE~C3#~bruk5LBVh#ynabXENf=}sjk0|AOEqak5oD7nL;(zA)8?NhN_OzvJ zt*Qg(MVjs6!DGzYtwGJm_mQwnJsqIcWl0O^zGpsf$Y2*=3JQbAz#7Fj7+s4OIe+O} z*Ka!_hdrmf&BL4Bsl+ezl4c5YUQ4#%)i9OJ_Im`()w^+Qm8BG|8{tXS8PtTZH-LEJ z#ksOgg(1U(b_cC60)yr!;ij-Ro-=jbHAY3Z*`}5kfXiLhHb^!v$bLTJY7kZXr)YD1 z?paP~F|kK2NNLymUVdNLktXh1Et?BuKk<9N{l-r;M892?R~KMxY+AVH+VFUYmirb* z2v_~D1Jk~3^O8th7u-|0Vb)sXU2w6f!$%DeA6DtL&<&L}Y2lSjq$aN9K7c2%B<99&8w17`cBlQ-wNB8T-_yj(cMt7!SR#Fp6p1)^2zsCN`H z`fg({wAmE9r_Dw3x!Q-6i|V2bz4-_HnnkM!mLaa+t|E#}q1!&kb0kKTvgqeU}Jb@ZbM#%Qf0anD$^ET* z*i95J+2Y%pDf_i!JOMuT6VudpFIpBboHDOxy9w;%aEvyGX%4!{PooLpp!NSw7uYgr zxs~Foo$9V61YhjQAbh9+vLADhoy*{s{;|Jj(J7@VowTXRGc#ks$c@atkI2-_nt)n;X#k_{im86KHrzfy}s|j#x%#d zfv0^kIg4ojjEupMS0{@n7``UuPVE@~H=a~l%2eIXgX9qNrAn^X`^WJTT}OSvuS9or zfxe0|N*DJA_baZ=>&q|HN$dOa-Ex6G?;v5)N2Y}#7;mPwDo$x?KmsE`;vZKdc~9VJ zO+j~+LEw2cZa>k^vgY8&cE0lX=7x>y(=Z)Ad;-RT^zXZSBq)XutSMc-(&tkWZp4r+ zIX>I)$(mOP+Ngi|R?`=6ujzWpZhV0K*51nT=Bx){vl*`~S&IpH-*fw1)|$AB+V9J= z=G-z7N^THTa~FmTlP1@f9oJ_^x15>Xxw_8RXpg~2(tdxDzx0h+v?KNrnBDTX=RHw2 zQfNG}R*kitX>Mhx-HZloD1kV7c$zNDrUsm%3&=k59*ZXtAoTtDz@lZ&8rbbF6tq zt;|y`6(z}l7=f{vTk+QCa_W6~9a6J>t!z((XG1@#^*0VHO)_OYHma280%63z1U=XK z8Gk=4|LX!<4qF)3F+V5_5aPU(JIwoG=3NRP{$@2#+w$&t!q<%MpMx%Y6pH@VHuQ>9 z$GsA|)q9EfQVQQNF0;68JCeEOvZY~2xr#OryNnZ3mcOD!C!ATx23ItQPG0iShD+JSNNVc1 zoeX>{Bkz-`tgW5IeatavGA?S%m^+G=DckUkUUa4;Y;P%3wtIhP{P(6PRmL@W9Q-EmAkr64rtItx;htIM0UhI9R;Ad z5h}#SF&{xdP`F>lv)uX#3hTIt^SZD1YWd1t_j0L(yFNo~IIg}1fDV?4krpl5V;>wL zb|SDm&7#8)u})kW+T*5Jy;yM&7ZG-(H#~VS-UL$#VERlI{`(A*ooeh&i2QOaoaigA zwibP}aCeXTP`qY=_>8YpYDKMRl=-L0dZoie$F;q(>PTb6d2ryj-$UcCe`06ttYiaH zFU*T_?ju9)o|tAF>I0#;HuO!`8cb+_OR!~(Ls+085f+ujAO7(I;dL z{FeUf{A54|bg(6KnroVCG|%=VIBVS^LYbZ+Pb=_ruP}w8Jp{dSNK>l|+%XBf+qGbv zDyIC(Yw_S!`ON;HL<|ALW51R>&#K2`KVqC>>w3ieLR;U~l}|4Dc)k31ub+db^CYq( z!^e}R7Kw2t)CuY$g*)w}4upU^+7>5Eo=C6in4D8a5W>awKyLm#e2*V|th1VS{6Sez zVt=Lya68t-7i{+Sec@Y)5d#+%7F={wEEmg|au-*<;&S?`$J5nH!p}6Ko@TQl$?~-_El+%u|XUhQ+c7)m1nP6J0nn)Zs>CykV6#8UFM!C~yj4E`J zXw@T1PkwUAZXiHUZ75s%>(ZM3xipfV{5(<&3?G&2M2UeIeUe&)5E;z6e>izB zah&A6RJtetli_M^=hD20Bp_Ap26wIE6W|g@wGwK7{Z7NnEV_-M)OLN}>#CG~?5_S- z`aWTS;&djmhr@tzVLaeg6<3^=e ztL{p{g(VVYcmz2+r$GhZsS0db^emVPJTEw9f%~3TbCJ!B6;6<57n~~r_s4B7`z?_k z8F{7_NcvQtr^0$6N*j#i<%y8}x>QQp-X;0p=bo%X;-hh&UN-c`eTt?hFxKZ>QTUa_ z>j74YZEPi@N0X5ODYM*whGiN6b$tw0Df#dJ0U~{=;AXYt90+YoS>XPzn5c&3_hHZ* zHQgJ8xDJv)r3TI0DPcieJBYrk&G~YDKxud@ct2pZq@b#AK zTZ9hvw#nQXiHfcJfgIF&*%Qy%ZZjWSF4*nR18zgFiTM?NTYSdT=<3Yq?cn|h|7#0_ zU*|9H35N$kUB&QG`9r<_SEA3$yjtwSR5&m3Jh7=`W;i-xxsi>z0!UFS^~?UEek% z#c`j6Y$j1>wJ%w9F3pLMI9iPdzxFpD)hwqPxb=0$%|!6N*VIbjMGVu{i3fzPUkgx? ztrL${R^*dlg1C5S(0<$Nc}E{}p9VWu#gv>B*XL~eqrjR|)PlKCQ6q@wM&qCR#*e?Z zi(&r7lVO_2~MY)Rbs%5^OdT^MSVaz3KExv!Hd0 zuItI}5kUTf9IFBu)*c@{Wg&$8R;faUciGes+Zi#_cGajz+q&t!xGm*pu_l4Bvj>t8=WTAHA1{iPqu6=xO5nl)GQ8DhPTV-jq5?D-WKm|+a&qRX0f(zAq zw?8BASM2lIXR`D&xW*M8v}&5nPF0NswwbA()$l7w1EyL%j{qjvJ8fJJ5ysRdEM&Pg z-Vc`7b3z|HzM!r}m~Kjp<}n!Xxd=s*kS67!A{pueY)=QN$?QTM`4*=%@ zQ&nGwNEf=s@XkTEkal}D48G8iD4NH%u`4;;1Uf%OiEck+{^NU$TTkQD2|lXkt8~vz7DftSD+QJhVJr{YJ!{7=}wd zcFPlXS|5GMUH&(#1vD>vEVO@?ob1{h%e~oo4d-hgiN}j>)+@hGG*^%u9maHRhAp&` zlKVZ?dc&l9^VTypGAw!?s!cGEg^dkj?vbHjMss(ObJSkAI$yEK-WGhr0+!rN6T9@OKDu6dU3`dD7m?) zPfMu14e$P1U!OCH^BPl^X+lNNf-{~MUF80-CqB;eEei`wPunPBqXI=LA(#Dr-6u8P z$93i`#H2UR-J}qB4UT>3?qW}LNS8d{@p%b@0DBLOOnoct^Qy{ns%+m#l=R_tkAdbC ze}~!Q^{dOx@p|VwIPAwM06oT1xw_xP!=vN-z`XuJ(s7_%Y`tgkQG}Q~A%IC1iHi-_ zc@Izz_cEhhHrg)B-1Td6#Iv9Ez5#~rbj@U>=C5~^w{Gv`dXw+~^|gmT*Hp8a=(3X) zbKadzK#dl-Q2%blSd6(v%6Sy0@z2%^0hH?gIA>s4M&X*^!d1q!x_5fqU_dmqJZcaf3~8u7+X3c}fEup0oJu&#@)H`7}V9B+4BHdIeL3x?(|;Fg`9qNDd% z7!`pP3-)xdND0Y#Qu7Drvq)x;h?lM%*jqQu)>!h(I0E9+JBmV~a2aQWOzt5$LHUmW(_D1;KkmTZKi(eD_02M(v?{83! zCTuQLM%Sh5C!A*mey6(39)%<8qIcdOP7%2gCg$&L?m?8la6Ci5d@X+|x4QjT5Q_gP z0vs<^CM4%)tp51OmN%lkTJ~7o=0kE2U!1Xf>=6(_pBpgj!%BRF5?)XtRuehLZ31Ke zU}ykKHAl&1%fewZj}LozGDf6%M64D3akuP6`__|+(_&l9PXVPIdB^ZT|Fz&975#^D ztZ@bDjJWLpA_!LmdSZAR zoZh{U4pq{jLSfTO?TuyKxKy?&zzLTsK@UeS4~&O;kK5{QO_@|294X5x#mK(8@d1mK&@^K^aB{WOsD-HTftb(;zm z{&wMBcGgmjMhU~rVgKXR$cCsoQMy-jO+29dV*E9Ey=k&xkM*GrP`^0j#XtS7_3jZd zEHpAnrf_T7wY#uC`Dd3B=>Ayw+uqQ?dxbQA$3Oitv^>dD*1%}AcY?rA1Bk9O))t1? z?MYj<{=tk3ptAo!KJXw>`TF&DT{KV!?^d#Z?SB`IH;oPjR_J2D{nK?(Pt(f)fx<}90 zpvK>FP1uF+Ih^2;rviM>^2!$4M_%MBivE1XnkIoY?(qyO9Lo-hez7YD4O_oZ;vlz# zVc_B=0{FANxUAV4CC`^FywXJhzEFMR((``m-I&vVr-r&Ae>XO^D!Lg`;O9y0NTOtRwrr?R=$3wSTgJJ%=kdE*2ggerR>rkf2V}edP^z9 zm)Wjxnx`C>V)eP3h?F4W#CS-vY(P(NjNl2yBUZSHik`Oncv?zmpJ4i-71%ue& z0o4LNL5uOOVi zt3X}=#IJ~~4Vq8*7vH4scbY8mTsYvskZ z0O$)Ixc#ZQ5OW;Gmb6C>{KQNHK%=p5VZO7=o_V7(SUnrfF&QBJ%LxILg4kz~O=Z82 zFaUX0#zpn(;{To|-RJ&eKCzX2B#xp8?VPn-Al;?vNW1Y2uHJ;u+%{78uQ=U^!u;)0a$*M1}IXmk|mJ}e|fXy#E)vPtV#jq7%UO%MzQgeCp_#uTS zGV4d864`#!HV|y}1PvHt6b0jipM^rh{`yT})B%5`&>7c?p;el)zU;b4 zAwR%!{J@4P%izghweL^@pkorZ=Ia0E+C55N4#ZN<a={j7vV0D)Y8!fF`<`czo)= znl6Zm>TvoEpGWzX6Ie?#PLm45pDBY2B7PHUv(N~;vqkZQuAyG$RS9t47uVr`IL209 zCmqWnMDd&%zQr%h0>JB-?lb224!OtQ5e>2O@+CJBhu)7#R!9L>`C-KiQK}k8061%# zf6vF6P)VtTiyYewc}!#gO7020rIpjK-D$w&t_#Kzy2D&R*XJt*;O}>B#1Aq6QGmbB zE6o8lPN9UK5vX|JX4j~GC`YMGr}eN~%}Er0JC8FIoU-JXj44kyo0i5J>NzO!$(b_JvH$50w z+6`u-1K@UND-ZgWG-!4PRM1bti#zxW5JQ!Zuu-9*6Wcb4$7mrG^jKNiPJWyA0;8P7 z(2a>B|3=HZd&hEHm|eLg#(y82@e}l$b-=7-tIy+-=yP*qGMTqxL7R>{9DZ7u-aFfjYL7a(3Cv|Kvnvd zPkikDR@nxd(YKgLc9a_SGVI1dYV4BsmyBJp3p1jS452J_nIlSv>;&=y*cs@){MVBcTy~Fz2F;@13|oPS%m3cJ zhW{$jM>m-R&~4m(L4H4~TQWDD^^&rPX?g*VgI=bN*5scqeIs=}$9%_}^eWAs+BidE zXAIM@&k~waNHfDpjUPR&^k!-F0%<)cRZ&#NWG72!l;DONysCM9#VXgRqvCo{y@9Q7 zy*fRg3l&6ixs9-HEu_6CUN`j}_K`xWs7PYpW3OhX0Pb&EQpt!fZnQw@xaq<3twdS5 zOMVT%PPtvg{UxP)Osp;{Qv()33ZR|lL5#J)IjsK>7IE7U<*^HKLJCy#O!yO#;hC?J-c<}L##7=y6x21? zO97!1tCWD{SxpRQqNlY$aX5N&sI(_R!DS2G7HO&sMvnv3b7G@;lEOZ#R^st|Y**$x z(NyOBicAa=Mx2rFIAr-!#&QnCNLjm_dPZ2WgFn5^OqRUq< zmGQ8n=Xyn5Fm{Da8gDL2co{odORMzc7$nP$ix{vJKXZRgQ3p%){e3`_M*G4Y00sFV zVAfC!e=$&)_rB+NmxjF~rI2?R0CYuPo0eXlxW6S6<2u=?40e!K7 zJFK@nDaX}VP<~LE7R)HYasK|*lkyu~nQ?Hr!N&yoq3+7d7^xUMVom#+5)Ri`3g71O z9YqxL0U%*D;a?M-hX4<5z@C28JYs!!1f>x_pTV)el)iGz6#d61JG9cZAJw$ClrBP- z25HCOI5)~e9eEA)G|hqg;XqmG-KnCePf#fAcXLukD*EqS^7_v>RybviI<%E2{0djy zpH|7n`otpNz9YvNAU`2&T@R>G)nMihoZoFpBWuT6%wPC_Jtyww#`R|V8PSM2T1ip; zBmcJu)7NPyg{bRb=H{W$a`vQxT6phq1NSqKifhov%&*k-D^?j^kAaiDgrBq$v+LQ{ z>g1^ASdQ z^21Syg?hb%Cl&CQmWiE8Jy4^cxZP3x5@HqEeuU}2q*;hJ6d(d5%eO6@IucWzL%-aM z)6^P5L_!lg*DxlKTRN#vV4U^M++E$LIY$LoJ7`=c!Qm#@e@nauyZc<}5e6fRwfPij9~S83JaoW_eqHOqskFAMFjYt8}hLmyf2 zQZWhK$?NgGi=FY=yFUH8QUhUS1X+C6Dc8O7S%!GNcnK#j7&{R~txXSo;b!0}cM~_o zn4Kcq=Nq%k=U5>!QF5GVq1#YUj3yHG9O{S;Z%X+E%V;!2Jrzd#5<&h-;Q7=W!izQ#pvSr9~*|9^#IFPaZb zE!B9G7@`pnZQD%VEaghOCJD#YU1x#VRv_VOE>leHd6seOw&qry`S3U`HR|D6NMnEf z4bF=*)P2!)ft38P`QfOXTj>Ma7yK?^Ue+_9=AaGJeX~pb=H;}$bMT7s=%+CsM1&mF zrS(`Sx!+0uvSA@NQJ5OMI7CdlHGzsft)dBJ)GHiSpsP&ebp1)x!^0tFu523DU3n^^ z;H7Kv_G1O!qAa6`ixt5R;Y2l}EFp|Q2tfxK272bN<{j(W!n<-4TtH+9w80!a@BX%b zKjYc}TXcL*N*AqTe|!p#mXG7mZt^BCZtomHlJflSxa@>Y6?*Y=VPz>)siEy3vtBq5 z95m_gzdc!3&|#vP&2VrnccA1HP^CZg46bt3@iR>8alK!H=_Kj@^olsIwA(uXD(Lhl z_}a(Nn7u*UP2+e81u+p#dWn(00kUrq&(0mf%IKJ|mp6?o%i{B;4=ob(fBGf25)nV& zLbqJ6?-dteH@*#v}V=nO(3_ zRWK@DhqT-oA{ORFWM~?$nsMY@nDr$fl&(U}P@C8FG{1Ap{X2^#$rc>79-yea?gj%P zA-O|%>vsySm|&gPB}L6W&cU_LD4&}dLL66egX?dDAK|6<0fXaC*zV$i5ATtpY400H z-Ki;1R#4c_s(f)@GH0S=P5ZTUW-2d7#~vZ-7KoY{GXR#;2q5%jBFJGpV`|(-W)ge; zZ^b~ggW>CO#GuRNs%nJxRl0pWil~B^V|C<`%efr>!o_C8W3+dsbWmQtVkc1YkeMSr zisF5}jE)PQ!XKw5?;Dk8I5b*L);DIkIw@#SS!tcvU!)SMt3pmP?~L4S&ark3HarOb zYQ3Uu9P*yx99fnk9YPjM!vMme+dz{567Hqr+U@mU7$9sEZs;gKl z@tm!Q!Q~v0SM_?@xdWdXXoL{#+z9YX(q~G`@^z}SeSfTllZ=z{UtAwrdhCokP*w(D zV}i(hK&V?ui+7Cv480O}sxL>n@x2bU_+{u^Di|4!y1?m$HjVyOjz&w2vcB0k#V_8& zH~(|WC>NQED^AksqPuw0vY0ZUqyUjeZn)^q3YPJJaE&n?Df~T*Vrxr1;rgaAC*L^K zPSW!@qK`be`thc6d?oaMF=!s(4 zKJXb?EBc>LqC6pJo~iUDYjQXGlS?tqQ-K?A%|CH7%lO2PsRGm#r1Neaxs~2wWNyn4 zO~e{RugiHMPykR3^ouyJ(pGRP;(C~8Fy7ReXBXYPd*3>2O#X$f1FZ9*28Zr9GyTzp50jDMEl1i zG-xT_eV&^uo(ixg|CSl{9}Qnjt?X`HauE@GTOQ9FG_usei}&1#?k1_Daq3!`0X@C^ zUAISncza1h_a%4_hUx&T$5g=T^xHOSN+yw-e3yi?tE3u6HwB-NOg*u8_0~L0oUVJ% zGuvFlc8Y7n6V6vT_18aJK4oen%d9B-<2a=c&-dH(tt0#j@4NFi=Jbu-=gH(k)gCY6!?tN>Iu%w!9S%p~#y;lz^ z(lz|>wj88A+#d7U0&K=B_9L{V4ksh1;Z!V6GF9QWH@6Njo0errWa|>8<<1GO-EdjX zy|LV{9CV*pAFwopk5xL+T0Ml#sj%)9{Z1ix$IN??CDD7yo5-&Dq-tW+Rndsgfwl*O zRQg6t{k9NASQfhPd|`(keY}zqNw~RHV&A`5(eEyTRrJ(U<{nqi>qzXODd;43*OuB* z6}BDZPcykvsB=JLDaP~Gn(X9!bj;Z_%{Q8uTV7{Xm#Yw3Mt#rKPgg}`?%DEp8o%O;~7e-!V+{?dL zQGq<;y$ccIg`#5AXB||Mg@c~Gf(9jvA_XW45r?b7qGfD{tCt0Sa|Uj!0yoR|W5)Z} z(vGEybXBmqnp5b&1x_~a0dAFPLbaW)`xZCcZY}!tG_5=nygT7@;V2A2`XxQX5l5jv z$mX5;FJevR)Xr)(kukn4$(vljjJswV+>qy;;iOE@*d>1lzEeqslRd0o@sknc#dfTT zNiE#w`H1}{`_OJi-BF2E+p?fnIs=)Bs3Wgja8OV3CwCR_Tz^?PRHe6PSiT@bHRo-d zM}I(A9EBkJ1#TZB{9GoGdMbaF9lh%qYb{BSV&o0?;h=3i9c2HP!w&8qnZLuSfP)H9 zT6wWdGBQ-{6<9>mldHaZa%Kr8wUK??y(|nkx_Wt(viSgCf3;tFIQO(+`eF?pCpY>v z@^}+$z+-m_RkMK227Dc~K(*|D@krq7&6SqxO$GXLlczd})zv=y;_j=vv{4ikK({T? z2KS%9WWS5VERr6*!hL4Pklml9;pOPI#4{BU$`?0*l4F(c)E8oZ?7s-*;rGWXpQUt| z%4(dnEl#bTt#+DhH34bt2#`J3nP5!}HEbxU87+3* z?DMyA=)88vW{Xh$Pm5z8o7W=Q&-+{&dwuu60gE;Zq&)H9SxSZm;c|0 zDtU6FafFB4JGjeiiZ=GcG3Zg;7_! zWRZ0kHNzKIpLiQ3Tu&qK2WN`ZyMFWHs|zO==uzNWHwiR89sdDYV-{_AHX-ByL6*_t zAIKWTAor8Ixx`wRw9aq6wp2()xp{3ioZK^KIOYj~mc7cKM#z>5L|v5WtM_yM+w8q^ zPS`XKeTLktFh75#sd1@m8cmXntJS1~NNfJ%i($URGv+XgPz0o|jeED3Z6<5bczc>m z@bTmb{JS!JhWy7hHnc^0JgVWE<;J%v$q7kTLd8p2DSFlG6hOW5KU=F8ip@LS_USSK z?MpKF?$>wwh*Lv&)btmin~p zwhFyD$zR4&odjIrO%W&ZFc%=f-fU)B*5_#o=k$M2n6Jv8PUsF~EDb{CN2mN1$KbN7 ziQ_$cgHH*4UOcxBeB)14t{}rY>sotb3aoHC31#uRY;rg9OxjF9lmI04-KcqCjm$1) zQHsicd{!h)#$s-{^ZXWJa}AI5;G(hJ`qP^+l?vqRdNSnlR6yiQc$a$84m-KMk108c z9Op(}_QN!BD-ZQbv3_Bcb5RslD; zOR-e_`uxYFpzYX)TZ9l7Kgr`3m+Nza@J6XJmQme`M(@x{*pcc(6z}3;5|V&(9v$Q# z`v>7xf>Luh?*f0q-otONdj&KUD}N5+73ML2Nt7t*ujRrblZtvUYkrS# z@~5FFrB9i%V@XUWxxGMSyXPf`yUnhW$9fcvs_S+_q<_HxHibAM;ot4@iClu?Agwb0 z{@q!4_0LK!Z*hF_;Ev_Ipdlw|-y(JLqI6a7TCiLA{2PY6i4FB!(oVVTrIOkyr{C9` z{)V^QH+O$m__%*ak@&Y);O|)-{PSOk&EUFIs+>?Y!3lWeT-;P4^7rZ7Wxbt!$*zJJ z1-G(H!DJ2PAB*2xhg~F$p`^sq|NGTpB)=ZJYnC*v6lKiL)d7<6|sU zov&sOVl&Ue{fktE!&dz>e^GJ``mg7<*4+p1I>+r}w!#@miQijQ$5L)tE4wg_*gq;3 z(q4uBMOV;LQ_Mf)BqEX)i}viQBU>H}FZB-kt}qo>X)9F8V+*fM4t9&ZtvEx#*eLTJ zgvu(&aHRZ7q}Yus&70V9M82PHlE269nJQFKI(6Vd2ZVxtbF@%VtgLv+k4*J18oqiz zu#0z&7Dac@y{!G%v&#!l+7gXgCU=>&6e~+E%1IrR9M$SJ+jmZ7DBSLoY~6k`c)i^O z_!L%UQlK8drLxE&rj=AoC zD`ZZzn?IkLv-Q>=U#r?{?jf^rvgO`KCEas#gRHd8+Z$tFzkRXmV)tEA{AFXO6R#^c z?p9o=G2vuj5%akj`N=fI?O-%u-1Cj%f=TGj;a;2gTdCm6n9D~xj^h-;XTg^xpH~ci z+IM}c3`kMqGV$)W4sKkDf75z~XhPb^xxwv!e}nVD{LtP<#XxF1P zfm~+uwdF>^kKGubdy0)KJ>;#oC4*=|(MRT+dLKvSgqE^2S#Dr3yxYLDSG z*pQ6n$Y@3{#Tqgb3nu~`nf6md)=n7o-rM&uA3hZASabiTsrSkoX+K|*6PPlF|^TsGXLH#(k2;Fjz^k_ zlGfIz7U{y$|93mIKPGhK7%K%hTf9$qi25d5DB-0DoW_D`q2&_181kO?b8w+0blXR> zyZWVfZ10bsDt9pbv7XKw|5MI=x>AyAg1j)!%jlIvh~2_839y;a`zqG72zU9GCUbFs zjc|af?OL<+tIKR+P8do~L@uIDrSEYQCg0181Y#C{b8Pr*Hek1^Wlkqz6<1w;oR>Kt z5hc&&n46AAH?7!;aa4=C?7?FgG^(fT5 zrWAV0*wXAfXusY9Nt|D=Gn?%te~tl;Qgv)ppn z-<;}bVd55RKY7xBVP|Z2qL724;pZ_Ro^k{kJNb(@PTsdeBoABZs7<~SJ8@ruH=&lK zzH^toEn3ExFloCV^kDxJMMGt!N8h+kPTmf0oWCgXt0c#U*w3hjO!Vyh0Ik&=V@A;4 ze4|9gPwLakKwO}rxa(Cx$~tdHp5Jn&k@%ctmhdxEwD>Q{zo~A5$AacWEFN5$y$O%( z#WP&TR_4olny~k-eqZU#rKxb{e*67&(pFopI(hD=IE|Y*-XnuT{7ME8Us$#mG@qLH zcKqRO%ds3Rnb#Tgv1`^yqIwUn96K>J|8B9VQ%~0i8J!JQIrza)s)nn$)IrUocmdMZ z6Gi+IYnMd}X6t00OU)DQ!YhlfUE>1r?i$u9RB-s-F` z9_1%2J3$U`2Dz+UzL(YTop59Z2B@2;BbBP zi=3empqYHa$3a|2ZzYXOlqczgs?V`Xk2ltqAnY|jjqgtf!5d>wqjulS`!Dx39;#2( z>P0!4R;T&LWXk@rfoBJ+FBMPeV*Fgfbl1&H$Iic={7OR$hG0}(W4t4Tycly3a$SgZ ztZOEy`89n0JJc{ce4@}~pel*~%$Ff|Mdsp-sj@>dmkvJ!4;Ey7V-!z}{&~uRjwy4G z<-2Bh3|kDhIo{uE@!o4(OA@R#JK<(OtVN7_gJB{-(WvAP_MUzXO_myLQl&=3Y18Yo zqKLzwGSPE`#v@@u!R6h9!Cqb;y2y}8=he`|=e?Nmj8{N=%a0C>dLI8bIsn4ALBqWg z1N~n`E9xs>hYSpUng`EiWxuPTIt-n>b=!E~IOouUJB9I}h!H(H1S4$OIQ?5Mj^L8sx(%5HRs7?kr0e2^hxdCu;S?Qn zoGy~@AyIHcmRS4rlzfJ@6i-uOwgUFVIhw;6G7%31U)4XS=hzuAGOo*drlj8vjc1Vb zc@32qTPKD6uNdQGdV4klQ}9}|miv{-uiO-E&Ot3Mu+ns6oZ(gH|D~*s+=bpxy<*rCs13TRQ?*(YI$Evj0`IE@C7xgQS>V+RXxp=B@hMgYd>_*a2cyeQ9w&U`RFyng#|2ZK0uiSDQ z@9DnJ9PY7HFPce5AtoXUWqVdi?V~$wp0J_Y2I*9On$!P;^f|Q90)aWW%y#;gTAp}& zFyZlJS<}JlL1pp>yEv^x1Am9H7fSBNZbf|+27Xg?dfyUAX1hB*@tsO;V_m8uy=7@1 zT#ivxExTwL1x)@Di$ov>r;syuEy;VzF21Yp-~@+D4VskO-GX-L>$W|C9+|wfYtzZ= z|D6;jbq4T#>vu}uEL`8hXGU^RjPX!obrdT$xY!x+l85{H+9lqI0%BFSmm>ZNhBC6G zt}p?oxY!Z1xG!-A&QA%K(UqzzsfTMzCXMDQ4bkjLOqb*VE#xdc3}*LM3Q`r+{CLz& zsa%fY$923i3EXuye6`l%rU>&3IMN}2sLJo;d2Zt5$E$um(lC-rmVkHVRl_S=Cj+>} z#p1ibX{>$J37DFEC38iY^57}DJMcYwSsnvEsB|Sgm8rZc<-mryqKY*B?B;ZkvM@Ys zDX;2(l^k>68r>ZllR_Tvwk@k_%c}8NvM({{nd8i@=1i>$H}tSqCw4|M1!mjcQ9J>O zZF&s=Ou2D~`KO1D3jp7X|D_hdJRx}S4`w{?`7D{MS{d4_^dK=KXanooF4iI_yMgT2VH0tqZ1F+i}D1+1-xs zOouT8;HnGEp94{HADuQm5DltY&E~MPHtkNfGonF;pmO71LRTyMW)ZO~MHpcINf}{z zz(1M^sEHnAO4#>`Q<*&IJ6&2}&=X(Da0K3jJ?u9e97}S-y-KEt$g}2zDOkF8tL5cE z{ve`GEMw*{B}-hx2PT(|9xx+-(sFTx_Q{V^ zYVTDD-SDX>^Prg7pu~4h)L0B^^d-#0Q&Jnw4xbbT?8`~PY*1vkyPo31I01o>6C~#F z1vEk4UX+#M=I-Z>WGCL z_BgrU*Ls96~uLoTjL36p&TiqXwe2Laru>BAP zP3d;a(zEy@&YLR4!3iEqrw;tt|D-~?x(xNACv3n{{tEF!8n%}0ux)lG(rLcq86|2| zW=OSSvF`q%jAhVn^;$(>iX9UuQ}{23^YTU^FVVg^#mQmzvT@y6v2-W`ooaMO)!OUg zphxY;LCfxrh_CPonDQ6(VXx;Z`u@_cGPu|K zMPdPy!T=TZ)w9g0bn_JWEQ-t>J!y3->%k$rc^!sNCwXzq?`1(B)a)6jLMF3+!g2D9 zj+^XB#Dl4aHCxhZP888cAs0V#@-w~@2X0$jhKZdPqZ78_^>Wj}aG+o8Hie&6aY&f| znZ2A#c=^ysYs(9=c;ZrjMnHQw@+J-N7;Clo9bo;n5Spxxcx?QUT)(r-;GVbFsLwh1 z6LV#-wdLxq;r@IXYG2zgU@A#s%U1F9zvqf_HVSeR@UqX}`44lxlM7mbWULYu(aWJT#UKr#-(#%i<|!|18`Ql5?aM}^g2I~cXblK-V_ zM~CtJx)&E@Tt9gQhiGUYG@O*gJz=JqBi6wr5!T#-cB_T8g@zWwCTZYX2z%_=h5+ z3Xjv#xA!<8l<<^(v)k~*KdlSB3@W$51ztH#U5%(@N}%++$noPiVL)hsFZl>i`hbxbsL!^WuKp$4d1Sa|(%JM;*u?Uc%R)aF5kUKS0KbRb&e9J0 z)XWch@26VcH%9XZMzq*gHX+6e7*-37j`{hgD2Rz=BJSNDjBIvQzh3w!nE@z08OAup zk1`N;0~+7sI(8}&#ZoQV2p*7q0pi=$l^qWTQuYyM0Lx?^3Tnw37?Mqwx~Ku4PTt-v zSXXk?!(^JdM`V|!LA)lwt~|Gk9RA_mn_OtfKbPtslc${8-JN7-cmD>z2S+70O{(6vrlYibttX2!~jarhFd5*&q(9=%1tO+y!2fCc(+u`77X@yX)eD1o*6&RjJ zQF}nXw$1SDwI|NGBdqS@dtPqlOAnrNh@t=}x}r$YTnCpLeY&V2Fy$%caJ{MZKvnl& zC5IISA#Am#v|~+1Bn6h2e8#3rp1O}27swVqtPl}ry3{(=~?I~d6{3==|uwN4!ah~62~9g5^354oDGf(#RKs? z54GO7<`LJn!%8#g)7ic3{@&gPv@wXbq$ zs=6Q4_Bl}>^wA}OcgoQniD!4OW&c%ym+rb)y{nhsQ_a$GZ`1ygxtJ^j)dt0tvZ2Jdyx8X&Abk(_U78s+byJ}3w{nEa>c zE3vFPC!>{^afsdJP7$r3$mZBm!RCZ@UR{GavB{hVC?VMM#Y-%6v40pB%GS&s@;CKq zP}T$gtwF<`j5I&y;|r?^p8nws%>O?%Xmp@vsovGDwCsTRaNDq=OiAvr%1Y4X5&0B9 zxn_7l8n2TJ^Ny8^gNFOm41(sDOLk7Hy$av)fC+S${l-j4ZhxZ86@43X7~R!JlG<%q@h?mvzPDZ5Gzd~T(C z4vy6zIhGAM#v{%D>{L`T=-q`yfLjOY*1U?TgQkC7kZfL|XO+wk#b{YKl4POHTw-YE~6VxoQz5-F9`tGM?L&ZM4KUP$68 z#J%r*H&%2Wdo!AXLh1QiS`nJucT&x+G14k^Zl@Z!vo6m`9CjfU$#J}jF#f4$t#~MfZUYMLOy;-8g8K)B1MSiut{l0o( zN7|k3;w4%@XT{c$zmm6-u*f)46*+8LBAF-AE4SC1C*Aw-$tWwctFfGMq+qt0u59wzs`0}L%S`h^Y2ym?lD-Sa1X R9}L5Q+MRp13vOBk|9}2Bhfe?i literal 0 HcmV?d00001 diff --git a/frontend/public/icon-maskable-512.png b/frontend/public/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..304847e2cc1086105d069f644409405e4f69c412 GIT binary patch literal 10252 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 Tof&fiz>kW8hJ5)$%i#Y37oLL` literal 0 HcmV?d00001 diff --git a/frontend/public/icon-maskable.svg b/frontend/public/icon-maskable.svg new file mode 100644 index 0000000..e8257e4 --- /dev/null +++ b/frontend/public/icon-maskable.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..f1d1229 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..e22cd3d --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,18 @@ +{ + "id": "/", + "name": "ThoughtSync", + "short_name": "ThoughtSync", + "description": "Capture a fleeting thought in a second — a self-hosted Keep-style board that grows into a lightweight second brain.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#ffffff", + "theme_color": "#F5C518", + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, + { "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" } + ] +} diff --git a/frontend/public/offline.html b/frontend/public/offline.html new file mode 100644 index 0000000..589ca4b --- /dev/null +++ b/frontend/public/offline.html @@ -0,0 +1,114 @@ + + + + + + + Offline · ThoughtSync + + + +
+ +

You're offline

+

ThoughtSync needs a connection to your server to load. Reconnect and try again.

+ +
+ + diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..32c0ad8 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,36 @@ +// ThoughtSync service worker — installable shell, deliberately NOT offline-first. +// +// This exists so the web app satisfies the PWA install criteria ("Add to Home +// Screen") and shows a friendly page when a navigation happens with no network. +// The real offline client is the future Android app (M5); to stay always-fresh +// and avoid deploy staleness, this SW does NOT cache the app shell, hashed build +// assets, or any /api response — everything but the offline fallback goes +// straight to the network. +const CACHE = "thoughtsync-shell-v1"; +const OFFLINE_URL = "/offline.html"; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches + .open(CACHE) + .then((cache) => cache.add(OFFLINE_URL)) + .then(() => self.skipWaiting()), + ); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) + .then(() => self.clients.claim()), + ); +}); + +// Only intercept page navigations: try the network, fall back to the cached +// offline page when the server is unreachable. All other requests (hashed +// assets, /api/*) are left untouched and hit the network normally. +self.addEventListener("fetch", (event) => { + if (event.request.mode !== "navigate") return; + event.respondWith(fetch(event.request).catch(() => caches.match(OFFLINE_URL))); +}); diff --git a/frontend/src/main.ts b/frontend/src/main.ts index c6ced36..ce512d1 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -8,3 +8,12 @@ const app = createApp(App); app.use(createPinia()); // before router: the nav guard reads the session store app.use(router); app.mount("#app"); + +// Register the service worker so the app is installable (PWA). It's a +// progressive enhancement — installability only, not offline-first (see +// public/sw.js) — so failures are non-fatal and intentionally ignored. +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + void navigator.serviceWorker.register("/sw.js").catch(() => {}); + }); +} diff --git a/src/thoughtsync/app.py b/src/thoughtsync/app.py index da5218d..ebf6a6b 100644 --- a/src/thoughtsync/app.py +++ b/src/thoughtsync/app.py @@ -1,5 +1,6 @@ from __future__ import annotations +import mimetypes import os import secrets from datetime import timedelta @@ -18,6 +19,10 @@ from .settings_api import bp as settings_bp STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") +# `.webmanifest` isn't in every base image's mime map; register it so the PWA +# manifest is served as application/manifest+json instead of octet-stream. +mimetypes.add_type("application/manifest+json", ".webmanifest") + def create_app() -> Quart: # static_folder=None: the SPA catch-all below owns static serving.