From 30c61351ded6d7738f187caaeec6ef706874f907 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Fri, 4 Sep 2026 22:03:12 -0600 Subject: [PATCH] Make the web app installable as a PWA (Tier 1: app shell + offline fallback) Adds a manifest, service worker, and branded icons so the site can be installed to a home screen/desktop, plus an offline fallback page so a dropped connection shows something friendlier than the browser's default error. Icons are rasterized from the existing favicon.svg mark via rsvg-convert. The manifest and service worker are served through small Django views (not raw static files) so their asset URLs pick up WhiteNoise's content-hashed filenames in prod/selfhosted, and the service worker is served from the site root so its scope covers the whole app. Does not include true offline task data or Web Push notifications -- those are tracked separately as larger follow-up projects. Co-Authored-By: Claude Sonnet 5 --- config/urls.py | 8 +++- config/views.py | 17 ++++++++ static/favicons/apple-touch-icon-180.png | Bin 0 -> 2333 bytes static/favicons/icon-192.png | Bin 0 -> 2394 bytes static/favicons/icon-512-maskable.png | Bin 0 -> 3085 bytes static/favicons/icon-512.png | Bin 0 -> 6998 bytes static/js/app.js | 11 +++++ templates/base.html | 7 ++++ templates/manifest.webmanifest | 26 ++++++++++++ templates/offline.html | 19 +++++++++ templates/sw.js | 49 +++++++++++++++++++++++ 11 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 static/favicons/apple-touch-icon-180.png create mode 100644 static/favicons/icon-192.png create mode 100644 static/favicons/icon-512-maskable.png create mode 100644 static/favicons/icon-512.png create mode 100644 templates/manifest.webmanifest create mode 100644 templates/offline.html create mode 100644 templates/sw.js diff --git a/config/urls.py b/config/urls.py index d61560b..574afd1 100644 --- a/config/urls.py +++ b/config/urls.py @@ -6,18 +6,24 @@ from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.views.generic import TemplateView from users.urls import api_urlpatterns as users_api_urls, web_urlpatterns as users_web_urls from tasks.urls import api_urlpatterns as tasks_api_urls, web_urlpatterns as tasks_web_urls from sync.urls import api_urlpatterns as sync_api_urls from notifications.urls import api_urlpatterns as notifications_api_urls from tasks.views_debug import debug_user_agent -from .views import api_root +from .views import api_root, manifest_webmanifest, service_worker urlpatterns = [ # Admin path('admin/', admin.site.urls), + # PWA + path('manifest.webmanifest', manifest_webmanifest, name='manifest'), + path('sw.js', service_worker, name='service-worker'), + path('offline/', TemplateView.as_view(template_name='offline.html'), name='offline'), + # Debug endpoint (remove in production) path('debug/user-agent/', debug_user_agent, name='debug-user-agent'), diff --git a/config/views.py b/config/views.py index 47c2a8b..5be4937 100644 --- a/config/views.py +++ b/config/views.py @@ -3,6 +3,23 @@ API root views for KeepItGoing. """ from django.http import JsonResponse +from django.shortcuts import render + + +def manifest_webmanifest(request): + """Serve the PWA web app manifest, rendered so icon URLs pick up static hashing.""" + return render(request, 'manifest.webmanifest', content_type='application/manifest+json') + + +def service_worker(request): + """ + Serve the service worker script from the site root so its default scope + covers the whole app (a service worker can only control paths at or + below where it's served from). + """ + response = render(request, 'sw.js', content_type='application/javascript') + response['Cache-Control'] = 'no-cache' + return response def api_root(request): diff --git a/static/favicons/apple-touch-icon-180.png b/static/favicons/apple-touch-icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..18482ba94e2cd40590dbabefce149b3087cacf5e GIT binary patch literal 2333 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 HaMFJPxG-ic literal 0 HcmV?d00001 diff --git a/static/favicons/icon-512-maskable.png b/static/favicons/icon-512-maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..ac8aef9f17bf5473596bd33001d80c24279cd15f GIT binary patch literal 3085 zcmeHJ`&Uy}7T!0#q@YGpP%8MKiZ+Z7@YzsU5}-WfVUlSZ2qF+c1xF$2AcH_5R~>My zTBb#L6f{tmjfD$=1&V__VlCL#DGB0)sx`qf3xO+&=q-5hCUdg<8~V#E*Ztw_b@txh z{`PmyJ!>DAiX(V)JmvrZFLGPxhX5R8a=_8PP3fEI2_bVMlF>|af%wiICQcafpN zA0MioxWD~GVh(oWX;$XDo3>vn^*dks`B{8jg4H!CiN>!_ z$7K6vI#`G`H17&O^q|**@3;S25w>lkk2q2=P&qr({cvph#iJ?xgYF+*6g+wVZ~#r` zm#l`VnD#(``+Ojz9Nq zQd`;*ZA_oZ(A}MD-8mRhYr!FWkMe4vYComWT6^E_V;|*v=r?mI&)ttyLcS1sq6KjWfc{M2jQLGoPlF>7aFiMVBinixC3FpwN%(v=wtz zM?f!K*J|bQhWs!hct;Am*h8Pif$J^L?a9TU*OOO}w+7{wt+40fz#X3IVb~DLGV|O1 z($0yLxKtGm#kDWW+6-k0Hd~-S^Hk}no}9dR+rfPaMqu7EBieq_CbB&v(as8t=xVW& z$010U*p<%4m!E2MhUzf{sp36h8$>(_7(X&;vRz@+i;~R6A@2q=Fdrido2{+kP!mCl zGF)JEE+x5%T-TU^K#X`Cul8RK#toDt4u|&L?ASjcD2)`|M{&ipHmhsie1GH113EU^ zr18n+5SQcCc_C0EGij`RNH3waC<`?{wiqMcldJQ#fntIbu`8h|Nw-7IM1kT|Qm5SW zm6=G2whExh4duFmw}IJ&9@CzP;8hjCq-V%p^0Fl>7xlI%@=&AD5ukQ z<1muKx_e^89a40DC9FS6r;#{((`-Fifr5q80?`Z;KB!GMTobeP0VYlVoc7B2{!8qS zz83SZUDEE>cy>f0X%FnXyY$g*=gf?Wj0AOfG*sL~A^A#WX7a`Qy7v6xPi3b%ur~wf z2NMm34peZHj;NS8l}cHzB-z$$)Vx88DsiH;t96JG7T*I(LI#_gNkh3#{Me?VzB@u$7X5bh9ZIrM z0-x?aLo4S|@9P;?G0Yg_Vv}sWQV#_i3>WL_+AoX#rEbwP!pCMmTOXYy9QrAf?V<(e zKq`~bC2G;^GmFNpW2mHS!19n7NiYXyu*Y*AkGH9B(e9(>Kg-ysN{Q}~ILWf+_sXGq zO~CZ?i_Fb#X3GilOZ0Q-kGY^q5B#tpui;GnnMBIjmk&wnP28P#8l08!>O+S?b!*1M zNOs%kB3GRF=;XJU$+K?-RA(;XHCfjef$E!Dp`N*ZkfYyxJ%gZ+p|0JFe*wz*86Wm8 zjzv2BI}#m^y`f!&9DN64LB6?fft&FmSFVZ5YVI)gG$s zK}pWxRZb8+{sBTELqML?hH5sgz!a+zx1f}9oRD3z!-YA5L+c%6TyTUvtpmt#aDpE4 zDx&(63q`)|`q-D_)bz6K0&ef#)DJsn@Bggq%xxUn;<>dk$-MS&TPSCl zY~gGL*RVA=mVKU6Vw)V)I_UPXwSJgl-MOW*>B8hISeRUWNDT7wWp>t|aP%H@9jYVd zgQ;GC=uM)nyV+*M0liUojk^%_>M%%;wEK`;#&czhkrMt`jjX}mLkuVA-i~ynTdpH4 zmG6Hg*J$roujC3!`^+wG2u}DcJb;42oM7piTL?Rv5AAI!D0*!Kj22z6L+;}QC9myA zzIs07?XIA%@+{gsJ9?*{5A^m-Ug#OS42)~3Lkuv*!LCp ztw%KJk{UDk7^ WXY5|5DC;Vk?;$cw9NHM7IPyPX-RwXB literal 0 HcmV?d00001 diff --git a/static/favicons/icon-512.png b/static/favicons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..689f9b7160831dfa85a1b0404c01bceb9827afbe GIT binary patch literal 6998 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 literal 0 HcmV?d00001 diff --git a/static/js/app.js b/static/js/app.js index b05984a..7d13d2d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -7,8 +7,19 @@ document.addEventListener('DOMContentLoaded', function() { initTheme(); initTaskSelection(); initTimerDisplays(); + registerServiceWorker(); }); +/* ============================================ + Service Worker + ============================================ */ + +function registerServiceWorker() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js'); + } +} + /* ============================================ Theme Toggle ============================================ */ diff --git a/templates/base.html b/templates/base.html index e999d8b..7a7c838 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,13 @@ + + + + + + + {% block extra_css %}{% endblock %} diff --git a/templates/manifest.webmanifest b/templates/manifest.webmanifest new file mode 100644 index 0000000..ad5da74 --- /dev/null +++ b/templates/manifest.webmanifest @@ -0,0 +1,26 @@ +{% load static %}{ + "name": "KeepItGoing", + "short_name": "KeepItGoing", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#3B82F6", + "icons": [ + { + "src": "{% static 'favicons/icon-192.png' %}", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "{% static 'favicons/icon-512.png' %}", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "{% static 'favicons/icon-512-maskable.png' %}", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/templates/offline.html b/templates/offline.html new file mode 100644 index 0000000..b248d11 --- /dev/null +++ b/templates/offline.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block title %}You're Offline - KeepItGoing{% endblock %} + +{% block content %} +
+

You're offline

+

This page isn't available without a connection. Reconnect and try again.

+ +
+{% endblock %} + +{% block auth_content %} +
+

You're offline

+

This page isn't available without a connection. Reconnect and try again.

+ +
+{% endblock %} diff --git a/templates/sw.js b/templates/sw.js new file mode 100644 index 0000000..9e2bf9a --- /dev/null +++ b/templates/sw.js @@ -0,0 +1,49 @@ +{% load static %}const CACHE_NAME = 'keepitgoing-shell-v1'; +const OFFLINE_URL = '{% url "offline" %}'; + +const PRECACHE_URLS = [ + "{% static 'css/app.css' %}", + "{% static 'js/app.js' %}", + "{% static 'favicons/favicon.svg' %}", + "{% static 'favicons/icon-192.png' %}", + "{% static 'favicons/icon-512.png' %}", + "{% static 'favicons/icon-512-maskable.png' %}", + "{% static 'favicons/apple-touch-icon-180.png' %}", + OFFLINE_URL, +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + + // Navigations: try the network first, fall back to the offline page. + if (request.mode === 'navigate') { + event.respondWith( + fetch(request).catch(() => caches.match(OFFLINE_URL)) + ); + return; + } + + // Precached shell assets: serve from cache first. + const path = new URL(request.url).pathname; + if (PRECACHE_URLS.includes(path)) { + event.respondWith( + caches.match(request).then((cached) => cached || fetch(request)) + ); + } +});