Skip to content

Commit 2340242

Browse files
MartinCastroAlvarezmartin-castro-laminr-aiclaude
authored
feat(web): register service worker + link manifest — installable PWA (#86 frontend) (#219)
The PWA backend (manifest + hand-rolled SW serving) shipped in #200, but nothing linked the manifest or registered the SW, so the browser never offered "Install" and the SW never activated. This wires the frontend half. - `index.html` template: `<link rel="manifest" href="{{ mount_point }}web.manifest">` + `theme-color` / `apple-mobile-web-app-*` meta so the browser's install heuristics fire. - `main.tsx`: register `<mount>sw.js` scoped to the mount on `load`. Best-effort (offline/install is progressive enhancement); the SW itself honors `no-store` so authenticated reads are never cached, and the mount scope is permitted by the backend's `Service-Worker-Allowed: <mount>` header. Install-prompt affordance + logout cache-purge (`dar:purge`) are the remaining follow-ups on #86. Test: `test_shell_links_pwa_manifest` asserts the shell links the manifest + theme-color. Typecheck green (7 pkgs); build ok; prettier + ruff + black clean; 15 spa-index tests pass. Tier 4 (web) + the template (Tier 3-ish, no logic). Self-merging under the repo-owner's full-tier authorization. Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb1b015 commit 2340242

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

django_admin_react/templates/admin_react/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
{% else %}
1313
<link rel="icon" href="data:," />
1414
{% endif %}
15+
{# PWA (#86): the manifest + SW are served by the package at the
16+
mount (pwa.py); link the manifest + PWA meta so the browser
17+
offers "Install". The SW is registered from main.tsx. #}
18+
<link rel="manifest" href="{{ mount_point }}web.manifest" />
19+
<meta name="theme-color" content="#ffffff" />
20+
<meta name="apple-mobile-web-app-capable" content="yes" />
21+
<meta name="apple-mobile-web-app-title" content="{{ brand_title }}" />
1522
{% csrf_token %}
1623
{% if bundle %}
1724
{% for css_path in bundle.css %}

frontend/apps/web/src/main.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ function detectMount(): string {
3434
const mount = detectMount();
3535
const client = new ApiClient({ mount });
3636

37+
// PWA (#86): register the hand-rolled service worker the package serves
38+
// at `<mount>sw.js`, scoped to the mount so it never claims sibling
39+
// Django views. Best-effort — the app works fully without it, and the
40+
// SW itself honors `Cache-Control: no-store` so authenticated reads are
41+
// never cached. The `Service-Worker-Allowed: <mount>` header (set by
42+
// the backend ServiceWorkerView) is what permits the mount scope.
43+
function registerServiceWorker(mountPath: string): void {
44+
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return;
45+
window.addEventListener('load', () => {
46+
void navigator.serviceWorker.register(`${mountPath}sw.js`, { scope: mountPath }).catch(() => {
47+
/* registration is best-effort; offline/install is a progressive
48+
enhancement, not a requirement. */
49+
});
50+
});
51+
}
52+
registerServiceWorker(mount);
53+
3754
const rootEl = document.getElementById('root');
3855
if (!rootEl) throw new Error('#root element not found');
3956

tests/test_spa_index.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ def test_spa_shell_is_not_cacheable(superuser_client: Client) -> None:
103103
"""
104104
response = superuser_client.get(ROOT_URL)
105105
cache_control = response.headers.get("Cache-Control", "")
106-
assert "no-cache" in cache_control or "no-store" in cache_control, (
107-
f"SPA shell must send no-cache/no-store; got {cache_control!r}"
108-
)
106+
assert (
107+
"no-cache" in cache_control or "no-store" in cache_control
108+
), f"SPA shell must send no-cache/no-store; got {cache_control!r}"
109109

110110

111111
# --------------------------------------------------------------------------- #
@@ -270,3 +270,13 @@ def test_react_login_on_does_not_change_staff_path(superuser_client: Client) ->
270270
assert response.status_code == 200
271271
finally:
272272
_reload_conf()
273+
274+
275+
@pytest.mark.django_db
276+
def test_shell_links_pwa_manifest(superuser_client: Client) -> None:
277+
"""The SPA shell links the package-served manifest so the browser
278+
offers "Install" (#86 frontend). Mount-relative href."""
279+
body = superuser_client.get(ROOT_URL).content.decode("utf-8")
280+
assert 'rel="manifest"' in body
281+
assert 'href="/admin-react/web.manifest"' in body
282+
assert 'name="theme-color"' in body

0 commit comments

Comments
 (0)