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:
38
mobile/models.py
Normal file
38
mobile/models.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from members.models import Member
|
||||
from rosterchief.base import ClubScopedModel, validate_club_scope
|
||||
|
||||
|
||||
class PushSubscription(ClubScopedModel):
|
||||
"""One browser/device's Web Push registration for one member -- created by
|
||||
the subscribe flow in mobile/static/mobile/app.js, sent to whenever
|
||||
mobile.signals pushes a new notifications.Notification for that member.
|
||||
|
||||
`endpoint` (the URL the browser's own push service gave it) is globally
|
||||
unique by construction -- a browser only ever has one push registration
|
||||
per origin -- so it doubles as the natural "already subscribed on this
|
||||
device" key: re-subscribing replaces the row rather than creating a
|
||||
second one for the same browser (see mobile.views.PushSubscribeView).
|
||||
"""
|
||||
|
||||
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="push_subscriptions", verbose_name=_("member"))
|
||||
endpoint = models.URLField(_("endpoint"), max_length=500, unique=True)
|
||||
p256dh = models.CharField(_("p256dh key"), max_length=255)
|
||||
auth = models.CharField(_("auth key"), max_length=255)
|
||||
user_agent = models.CharField(_("user agent"), max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("push subscription")
|
||||
verbose_name_plural = _("push subscriptions")
|
||||
ordering = ["-created"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.member} — {self.user_agent or self.endpoint[:40]}"
|
||||
|
||||
def clean(self):
|
||||
validate_club_scope(self, self.club_id, member_fields=("member",))
|
||||
|
||||
def as_subscription_info(self) -> dict:
|
||||
return {"endpoint": self.endpoint, "keys": {"p256dh": self.p256dh, "auth": self.auth}}
|
||||
Reference in New Issue
Block a user