Scaffold the mobile member app: PWA shell, push, and app-shell tokens
New `mobile` Django app mounted at /app/ -- the installed PWA for Member mode (M1-M7, see design_handoff_rosterchief_platform/README.md). Coach mode (C1-C6) is a later phase and has no routes yet. Foundation pieces: - assets/mobile.css: Tailwind v4 theme reusing management.css's design tokens (same per-club --tenant-* theming pattern), plus the ice/coach accent and mobile's 14px card radius. - PushSubscription model + pywebpush-based sender (mobile/services/push.py), wired to notifications.Notification via a post_save signal so the existing notification system gains a push channel without knowing about PWAs itself. - Per-club manifest.webmanifest + service worker (served at /app/sw.js) + a server-rendered fallback home-screen icon (club initials on secondary_color) for clubs without an uploaded logo -- confirmed with the user as the fallback, never a generic RosterChief mark. - App shell (base.html): navy header, person switcher (every child a signed-in parent manages, plus "Me"), bottom tab bar, safe-area insets. Vendored htmx + Alpine for the screens built on top of it. - Placeholder views/routes for all seven M1-M7 screens so the shell is fully wired end-to-end before each screen is built out individually. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
48
mobile/services/push.py
Normal file
48
mobile/services/push.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Web Push delivery -- the push channel for notifications.Notification.
|
||||
|
||||
Wired in from mobile.signals (a post_save on Notification), deliberately kept
|
||||
out of the notifications app itself, which stays channel-agnostic (see that
|
||||
app's models.py docstring: "the whole point is reusing this for other kinds
|
||||
of activity later"). A backward dependency the other way -- notifications
|
||||
importing mobile -- would be the wrong direction: notifications has no
|
||||
reason to know a PWA exists.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from pywebpush import WebPushException, webpush
|
||||
|
||||
from .. import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_push_to_member(member, *, title: str, body: str, url: str = "/app/") -> None:
|
||||
if not settings.VAPID_PRIVATE_KEY:
|
||||
# Dev/no-config default -- see the settings.py comment next to VAPID_PRIVATE_KEY.
|
||||
return
|
||||
|
||||
payload = json.dumps({"title": title, "body": body, "url": url})
|
||||
for subscription in models.PushSubscription.objects.filter(member=member):
|
||||
try:
|
||||
webpush(
|
||||
subscription_info=subscription.as_subscription_info(),
|
||||
data=payload,
|
||||
vapid_private_key=settings.VAPID_PRIVATE_KEY,
|
||||
vapid_claims={"sub": f"mailto:{settings.VAPID_ADMIN_EMAIL}"},
|
||||
)
|
||||
except WebPushException as exc:
|
||||
status = exc.response.status_code if exc.response is not None else None
|
||||
if status in (404, 410):
|
||||
# The browser's push service says this registration is gone for good --
|
||||
# not a transient failure, so keeping it around would only mean retrying
|
||||
# a subscription that will never accept a push again.
|
||||
subscription.delete()
|
||||
else:
|
||||
logger.warning("Push send failed for %s: %s", member, exc)
|
||||
except OSError as exc:
|
||||
# Never fatal -- same reasoning as every other branded send in this app
|
||||
# (see e.g. notifications.services._send_email).
|
||||
logger.warning("Push send failed for %s: %s", member, exc)
|
||||
Reference in New Issue
Block a user