From 3d77c7cd5a213361c7045a0271d370e37414ac8d Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 29 Mar 2026 19:04:33 -0400 Subject: [PATCH] feat: PWA support with hockey puck icon Adds full PWA compliance: web app manifest, service worker with cache-first static / network-first scoreboard strategy, and a generated hockey puck icon (512, 192, 180, 32px) on the app's dark navy background. Includes all required meta tags for iOS standalone mode and a /favicon.ico route. Co-Authored-By: Claude Sonnet 4.6 --- app/routes.py | 22 +++++++++++++++- app/static/icon-180x180.png | Bin 0 -> 1538 bytes app/static/icon-192x192.png | Bin 0 -> 1695 bytes app/static/icon-32x32.png | Bin 0 -> 281 bytes app/static/icon-512x512.png | Bin 0 -> 5473 bytes app/static/manifest.json | 24 ++++++++++++++++++ app/static/script.js | 5 ++++ app/static/sw.js | 49 ++++++++++++++++++++++++++++++++++++ app/templates/index.html | 7 ++++++ 9 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 app/static/icon-180x180.png create mode 100644 app/static/icon-192x192.png create mode 100644 app/static/icon-32x32.png create mode 100644 app/static/icon-512x512.png create mode 100644 app/static/manifest.json create mode 100644 app/static/sw.js diff --git a/app/routes.py b/app/routes.py index e37856d..cbaca4f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,12 +1,32 @@ import json -from flask import render_template, jsonify +from flask import render_template, jsonify, send_from_directory from app import app from app.config import SCOREBOARD_DATA_FILE from app.games import parse_games +@app.route("/manifest.json") +def manifest(): + return send_from_directory(app.static_folder, "manifest.json") + + +@app.route("/sw.js") +def service_worker(): + response = send_from_directory(app.static_folder, "sw.js") + response.headers["Service-Worker-Allowed"] = "/" + response.headers["Cache-Control"] = "no-cache" + return response + + +@app.route("/favicon.ico") +def favicon(): + return send_from_directory( + app.static_folder, "icon-32x32.png", mimetype="image/png" + ) + + @app.route("/") def index(): return render_template("index.html") diff --git a/app/static/icon-180x180.png b/app/static/icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..2510949c0a40db9101d18a67a4dc5a213c1d5e52 GIT binary patch literal 1538 zcmbVM{Xf$Q9ABAvxHJzZEbJ7kWOiz`BHWiTw2`Ddgd(%6aO;}HX0N^~xzjurEf3$q zMu@W^t0V~Wyh%f(s=~sOe6c;l?S~ef zd!QAC$5|W2W3Lr<Ey)lOybDC z_=L4bhdPtiv}9^x4*xfN(jI9_{fmXu*-~TJqsRooiVPDhA*}mtj zt;2dEgL2pN%h@(5KZfj({3rOGw{qVRX+VlYROpEvKH z8sx+X@AI`dIRVW&ComHhKj1H>^;k-N%>oQWwttpcCFAIvJHDOjv@X_Gcf29duD^Ig z-641YyMD1_#(jspttU*#{r+VXJ8odbL#0|MCl{C#6GiAt9Q$32@WpI1)=9V$lT>>o zT?}EJ&Rosd25hV(3OUN1FfK52V{i^yL)l6MIcWg0t~3HOhugotRV60bO~|M}a6TnX%1` zcto8UtbFkVhD5VUHJVlO_=eB`gY)};tAQG)@qOgi0+Nkfk&KEi1;;I%_H3E7aI*kJ zF>mtA>NZ=dh)WnRq_knLGV z^VmTO7t3M50~!=*TM_r;9YufXW$4n%!g5NlaJI>LWqw>*&{|}|-MzeEow4rzvih|u z3xma6-h-Ai0dRqjCM6^HGCTNvS^7RZOU8vf355o4;-xkIEJNbMd%7P)3cHiSkSov# z$yBemaXtqpKk;w(wIkGyV1aK;a{^mmG|3@Jh_59BtapKt(U%~tL2bZ5YzX2Q+mQt} z_HxA%1zxKWiLz0a-IIIo zJA>}lAE7{p8voQ36sx%foc7w+cePF}*iGD4oSAuBR-8Fd%=9i#=@-SRPCoM(q))f- z%UGLnc;C+ioL_^2wHJ@1D>m_0^sHts9jY9K%lO_=b-u9t{!haTeyMfhuJBZSG~>ihf;pGUozcmY1b)xA07U-;oh!^ zeU5UAWkkf?0S*n#uSnsL!whpY6OJv+krH2c=D^Mkv>J-br`4PiR*@F2H+3r2%CV{jhKJ8WD3?@#P5sczw&vz7$b(pJsWJjHY zcLeAZL9O>O2MbnRFQd?Y~dH!Tr2L!*vN f=@Kjd2iPd`d%`2Xx<@KgM<6`iyvcW6j)DIGWTDr* literal 0 HcmV?d00001 diff --git a/app/static/icon-192x192.png b/app/static/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..412217b33d77046fe95583e486320cebb9723d3d GIT binary patch literal 1695 zcmb7FYgChm8GgU;k$fgd5{VKK2n{M$MY&8t8be4E604vvC$>rmbTBHY;aFG#LZUzb zL2#DhWeB0vNn~=FD6HWUM1^S?qQPQ|C9!nS05zaU3Q1VHvvbz{**QD!<$2!Ad4E1{ z(Y{E*a-tg%0G96(@}lji{W&naJx7nE5&-Z?yLe&yrIpjqcb3v(Xv+8ueF+rV?X9)E zyuNdMj^0B`s&L3Z!OPW zY)?Pl?=&>uDKouxn=n=_Fkk}}KJbQ?a7aE2kK7Q|HXJ7fRXoMu#{V7&Lais$AIDYO z-uY_d`Qv8Okq*+69f0O{y(h8NlmY(exGzbA;wElk-HZ|wR#us0MJiz zlUIYi_I0SU#d;j~=uFwa9q{s0%sBrEZ2!xqicInK>uqbwiHiEpt|OYTA_$%wC{sN+ zc8(3DtdNyVp$EidcT5RBYeD=hX}>M?N(3i4>wRzGN(j<4C${g1bkSM`TN_4WI|R5l zBK7#y{cu|~AnN2Z-)CYB=&6oTVqAjOH14|d^dp3oWBb5{e?iY851l9v0A*KcA2rWO zVQBO@f*NpQ^W+(NSRJ(eaAWJ&5os5JZeug~l=V=`&gO*>^D%|trO$ilECesSUTN5{ zmdvrP=j`4oe{64XvT5fXwSxrG_ff)C5F_7Qu2HO26zmoK&DEoMcu&hRPRDSenh%Gq z&Ed_YJ!DQtU*V09?Pc19XBw>zbj8K)@8PvWj6?k+j(>7giT000Z-fThaLUsye(PW^ zfq88OVzT5MK63v=_;Uw+{OvYfiBF`4pc3)MDnfv*audkp;?@*!mrKg${rk4T+>07z zIU=!iZ>Z|^YK35%m^Sd?xkF&&zw4{U=?miM{gVX=TM&t@H(4}~{DRcElD1vN6t6ZRC0`_3BulT_`}Plnz}`Dpu&hc;*b%73JJvsf_4 z@~R{ z$mjmE6FpId5IxhraFaygNULJx83+y=$TGVczuEWx)vQ0;BjVG{r*t#(dlMqP&obw&Z+n&<(~bqD_o8zFmS`{h-_xy!u&+I#>IG8wB%U;MMDzMC= z080%{zdRD>&HN1UJSGt=P$wE^}?}h z4}Q~SZ)N1BU5}kRgo@jOg4yPkjSEL{2TiA*FUsVcbeb>D3&mFhcBNEe%?vYQuI@Zhm%9 zdF2!%7u5;f literal 0 HcmV?d00001 diff --git a/app/static/icon-32x32.png b/app/static/icon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..4772f8d2a26d413f6ec5e67d85e93b197bae5dfa GIT binary patch literal 281 zcmV+!0p|XRP)7W?TSy3l)Kbj>AYTz{F*+tDMDRKw zAUf-ROYa1p+X1g~^8degaRFlxn<@?<#V0V!VVK-> zFt{8r;88H(iEzNP;eZ!JgQ6%N@XBaVRL3AZpbZij9?&KkHL@nfjZ$(8DXKQhv4zNh fbWCW4M;!nFLzQ^(dyMoa00000NkvXXu0mjf?qF%d literal 0 HcmV?d00001 diff --git a/app/static/icon-512x512.png b/app/static/icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..72cff757e59b676a17cd314942b9a18f0e9189b8 GIT binary patch literal 5473 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 { autoRefresh(); setInterval(tickClocks, 1000); + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(err => { + console.warn('Service worker registration failed:', err); + }); + } }); diff --git a/app/static/sw.js b/app/static/sw.js new file mode 100644 index 0000000..9b4034a --- /dev/null +++ b/app/static/sw.js @@ -0,0 +1,49 @@ +const CACHE = 'nhl-scoreboard-v1'; +const PRECACHE = [ + '/', + '/static/styles.css', + '/static/script.js', + '/static/icon-192x192.png', + '/static/icon-512x512.png', + '/manifest.json', +]; + +self.addEventListener('install', event => { + event.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE))); + self.skipWaiting(); +}); + +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => + Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', event => { + const { pathname } = new URL(event.request.url); + + // Network-first for the live scoreboard API — stale data is useless + if (pathname === '/scoreboard') { + event.respondWith( + fetch(event.request).catch(() => caches.match(event.request)) + ); + return; + } + + // Cache-first for everything else (static assets, shell) + event.respondWith( + caches.match(event.request).then(cached => { + if (cached) return cached; + return fetch(event.request).then(response => { + if (response.ok) { + const clone = response.clone(); + caches.open(CACHE).then(c => c.put(event.request, clone)); + } + return response; + }); + }) + ); +}); diff --git a/app/templates/index.html b/app/templates/index.html index 815a3e3..9d31530 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,6 +3,13 @@ NHL Scoreboard + + + + + + +