New mobile:calendar_feed (/app/calendar/<token>.ics) -- a standard
webcal-style subscription feed calendar apps can poll, so a family's
schedule shows up in Apple/Google/Outlook Calendar alongside everything
else. Token-authenticated rather than session-authenticated (calendar
apps can't do interactive login); CalendarFeedToken is deliberately not
club-scoped, since the same token works under whichever club subdomain
the request is fetched from -- an account active in more than one club
needs only the one link.
Combined across every managed person (their name goes in the event
title when there's more than one), every event they're invited to
regardless of RSVP status, not capped to Calendar's own 2-week window --
a synced calendar app is exactly where someone wants the whole season
visible. A cancelled event stays in the feed as STATUS:CANCELLED rather
than disappearing, so it's removed properly on the subscriber's next
refresh instead of just vanishing.
New mobile:calendar_feed_settings (linked from Me) shows the webcal://
and https:// links plus a "reset my calendar link" action that
immediately invalidates the old one. Uses the icalendar package for
RFC 5545 generation rather than hand-rolling escaping/line-folding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
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
New `notifications` app: Notification (club-scoped, keyed to the
member it's about, generic `source` via a ContentType/object_id pair
so future activities can reuse this without a new model each time)
plus notify_members(), which resolves each member's own email (if
they hold a login) and every parent/guardian's, always -- a child
with their own account doesn't opt their parents out -- and emails
the club-branded template to whichever addresses that resolves to.
Delivery is email-only for now (no in-app feed exists yet); the row
is created either way, ready for one later.
news.tasks.notify_news_published resolves the audience (a team-scoped
item's current rosters, or every active member if it's club-wide) and
calls notify_members with the item's title/plain-text body.
NewsPublishForm gained a "Notify linked members" checkbox (opt-in,
default off); when checked, NewsPublishView schedules the task with
Celery's `eta` set to the item's own published_at -- a scheduled
item's notification arrives when it actually goes live, and an
immediate publish (eta in the past) just runs right away, no separate
branch needed.
Registered the new notification email on the Club identity page's
Email tab alongside the others.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Large uncommitted body of work accumulated across sessions on this branch --
committing as a checkpoint so it's tracked and future worktree-isolated agents
see the real codebase instead of a stale ancestor commit. Covers the
management app's dedicated Tailwind theme and templates, the club onboarding
requirement/signup workflow (club/services/onboarding.py, requirement/status
models, sign-up dashboard), fee/status auto-activation decoupling, referee
management, and the new events calendar grid service layer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Club staff author body as Markdown in the control panel (help_text
now explains the syntax); the public API renders it to sanitized
HTML on the way out via news/services.py -- markdown for the
conversion, nh3 (Rust/ammonia) to strip anything staff's raw
Markdown source might smuggle through (script tags, event handler
attributes, javascript: URLs) before it reaches someone else's
public website. The control panel's own preview is untouched and
still shows the raw source.
Excerpt is now derived from the rendered HTML's plain text rather
than the raw Markdown source, so syntax like ** or [text](url)
doesn't leak into what's meant to be a short teaser.
A large batch of club-management features built up over one session:
- Club dashboard banner warning admins 1 month before billing ends
- Full Events/EventSeries CRUD (recurrence builder, occurrence lifecycle,
per-team permissions), with match->game rename and game-specific fields
(score, competition, live status, external game ID)
- Django-admin competition dropdown, gated per-club by feature flag
- Auto-import of RBIHF fixtures (scrape -> diff -> preview -> confirm),
with location/opponent dropdowns suggested from existing club data
- Feature-flag-gated Shop/Forms nav sections, reusing the same flag
machinery for the RBIHF import button
- Team roster now scoped to members active this season or next, sorted and
grouped by position
- Club sport type (ice hockey / other), shown in the control panel's club
subtitle
- Per-season team photo upload from the team page
- New public read-only API (Django Ninja) at /api/v1/: news, team rosters,
upcoming/live/per-team games, and sponsors -- auto-documented via Swagger
UI, CORS-enabled for a club's own external website
- Club sponsors: admin-only CRUD (logo, URL, active date window) plus a
date-windowed, optionally randomized API endpoint
- Assorted fixes: NullBooleanField dropdown rendering, cross-club event
validation timing, searchable-select chip placement, btn-neutral ->
default button style sweep, calendar-month chart windows
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
Team managers/coaches now only see their own teams, can't create teams,
and can view (but not edit) positions -- admins keep full rights.
Locations and Opponents move from read-only stubs to full CRUD, gated
to admins and management-position staff, with a country dropdown
(django-countries) instead of free text. Also: the team list shows
player/staff counts, and deleting a news item's main photo promotes
another one instead of leaving the item without one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R1gj3J1QPfP38XWpnpbFpy
Gives clubs a self-service /manage/ area for members, families, teams,
roles, and season memberships, alongside real fee-payment tracking
(FeePayment, record_payment/mark_as_paid/remaining_balance) so a
membership's paid status reflects actual money received instead of a
single manually-set flag.
Three things would have broken a deploy, all invisible until it happened:
- psycopg was missing. dj-database-url parses postgres:// happily, so the app
would have started and died on its first query.
- No CACHES, so Django used LocMemCache -- private to one process. waffle caches
each flag's targeting there, so under several gunicorn workers a toggle flipped
in the control panel flushes ONE worker and the others keep serving the stale
flag. That is a feature that "sometimes doesn't turn on", and it makes Redis a
requirement of the first multi-worker deploy, not of the second server.
- Uploads (club logos) sat on local disk. Fine on one box; on two, a logo
uploaded to node A 404s on node B. Storage now switches to S3 the moment a
bucket is configured, so adding a server stays a config change.
HTTPS behind a proxy: SECURE_PROXY_SSL_HEADER is not optional once Caddy
terminates TLS -- without it Django thinks every request is plain HTTP,
request.is_secure() is false, WebAuthn disagrees with the browser about the
origin, and SECURE_SSL_REDIRECT turns into a loop. HSTS covers subdomains,
because every club is one.
The SSL/cookie flags default to off and are switched on by the production
environment on purpose: defaulting them to `not DEBUG` would redirect every test
request to https and break the suite wherever DEBUG is unset. `check --deploy` is
what catches a deploy that forgot them.
Static files are served by WhiteNoise from the app itself, so a second app server
needs no shared volume or CDN. Manifest storage is production-only: it demands a
collectstatic manifest that no test run has.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RosterChief charging the clubs, which is a different domain from `shop` (a club
charging its members). Nothing here is club-scoped: these rows reference a Club,
they are not owned by one, and no club user ever sees them.
- Tier + TierPrice. Prices are dated, not keyed by year: a rate change is one row
with a future active_from, and price_on(day) answers "what was in force then".
A tier with no price yet returns None, which callers must treat as "cannot
bill" -- never as free.
- Due: one rolling-year period per club, with a 45-day grace tail. The tier and
the amount are SNAPSHOTS taken when the period opens. Raise the price and last
year's period must still say what was actually charged; reading it back through
the tier would silently rewrite financial history.
- DuePayment: partial payments accumulate. amount_paid is re-summed from the
payments on every change, never incremented -- an increment drifts the moment a
payment is deleted, and the drift still looks like money.
- Invoice: PDF via WeasyPrint, rendered on demand from the frozen snapshot. Only
the number is stored, in one platform-wide series (unlike the shop's per-club
order numbers), and re-issuing returns the existing one rather than burning a
number -- a gap in an invoice series is a question you don't want to answer.
WeasyPrint is imported lazily: it binds to native pango/cairo, and the app, the
tests and every other page must still run on a machine without them.
- archive_overdue_clubs reports by default and archives only with --commit. That
asymmetry is deliberate: this switches off paying customers, so a bad clock or a
cron misconfiguration should cost an email, not a morning of angry clubs. A club
with auto_archive off is spared entirely.
Renewal continues from the last period end, not from the payment date: a club that
pays two months late has still used those two months.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds django-browser-reload: runserver already restarts on Python changes, but
the browser had to be refreshed by hand for every template or CSS edit. It also
watches static/, so a Tailwind rebuild now refreshes the page on its own.
Mounted only under DEBUG -- it injects a script into every HTML response and
serves an open event stream, neither of which belongs in production; a test
holds that line. Its endpoint is exempt from RequireMFAMiddleware, otherwise a
not-yet-enrolled staff user has the stream redirected away and live reload dies
on the MFA enrolment page, which is exactly a page we are restyling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a `features` app with a swappable waffle Flag (WAFFLE_FLAG_MODEL) that
gains a m2m to Club, so a feature can be rolled out club by club.
Two things worth calling out:
- `everyone` keeps waffle's contract of overriding *all* other targeting, so
club targeting is only consulted when `everyone is None`. This keeps
`everyone = False` usable as a hard kill-switch.
- m2m edits don't call save(), so waffle's per-flag cache would go stale when
clubs are added or removed. A m2m_changed receiver flushes it from both
directions, and get_flush_keys() drops the club-set key alongside waffle's own.
Note for future flag tests: waffle's cache is not rolled back with the test
transaction, so tests touching flags must clear it (see features/tests.py).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The clubmanager.app domain was taken, so the platform is now RosterChief
(rosterchief.app). Renames the Django project package clubmanager/ ->
rosterchief/ (git tracks it as a move, so history follows), every
`from rosterchief.base import ...`, the settings/wsgi/asgi module paths,
env vars (ROSTERCHIEF_BASE_DOMAIN / ROSTERCHIEF_RP_NAME), the MFA adapter
(RosterChiefMFAAdapter), brand text, and the docs.
Two things were deliberately NOT swept:
- club.models.ClubManager stays: it is the Django manager *for Club*, not the
brand. A blind rename would have silently broken it.
- Migrations are untouched (history is not rewritten). The only reference was a
cosmetic help_text, so a normal AlterField migration carries the new domain.
Note the WebAuthn RP ID is the base domain, so moving to rosterchief.app
cryptographically invalidates any passkey enrolled under the old one; they
cannot be migrated and must be re-enrolled. Nothing is in production, so the
real cost is zero.
Add django-lucide (from bsiebens/lucide) for icons: the theme toggle now swaps
sun/moon against the effective theme, and the control panel gets icons on its
tabs, actions and stat groups. Its classifiers stop at Django 5.0, but that is
stale metadata — verified rendering on Django 6 / Python 3.14.
Also add formbuilder, shop and controlpanel to ruff's known-first-party list,
which had drifted behind the apps that landed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt django-allauth with allauth.mfa, giving TOTP, WebAuthn passkeys and
recovery codes — and the signup/password-reset flows we'll need next. There was
no login UI at all before this (only /admin/), so this brings the auth stack.
The critical piece is authentication/adapters.py. A passkey is bound to a
WebAuthn Relying Party ID (a domain), and allauth derives that from the request
host — which under our subdomain tenancy would bind a passkey to a *single* club
(ajax-united.clubmanager.app) and silently fail at every other one. The adapter
pins the RP ID to CLUBMANAGER_BASE_DOMAIN so one passkey works across all clubs.
Note this cuts both ways: changing that base domain invalidates every existing
passkey.
RequireMFAMiddleware makes a second factor mandatory for anyone who can change
other people's data — Django staff/superusers and holders of an elevated
ClubRole (ADMIN/EDITOR), via the access service — while leaving it optional for
regular members. /admin/login/ is routed through allauth, since Django's own
admin login knows nothing about second factors.
allauth is installed WITHOUT django.contrib.sites (optional since allauth 65),
so ARCHITECTURE.md's rejection of the Sites framework stands and no Club.site
bridge is needed. Sessions are shared across club subdomains, matching the
one-passkey-everywhere model; tenancy still scopes what you can see.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add EventSeries (club-scoped): an RFC-5545 rrule + dtstart + duration and a
template (kind/title/location/opponent + audience M2M). Concrete Event rows
are materialised occurrences carrying a series FK plus detached/cancelled
flags; the series tracks excluded_dates (EXDATEs) and a generated_until
horizon watermark.
Recurrence service:
- occurrence_datetimes expands the rrule (via python-dateutil) up to a
horizon, minus EXDATEs.
- generate_occurrences materialises missing rows, copies the template +
audience (so attendance syncs through the existing signals), and is
idempotent.
- cancel_occurrence adds an EXDATE and deletes (or soft-cancels) one
occurrence so it isn't regenerated; detach_occurrence marks an occurrence
as independently edited; propagate_series re-applies the template to
non-detached future occurrences.
- extend_event_series management command rolls the horizon forward.
Register EventSeries in the admin and surface series/detached/cancelled on
the Event admin. Add python-dateutil. Full suite at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework an event's audience: replace the single team FK with a teams M2M
plus invited_members and excluded_members, and add a season FK (derived
from the start date when blank) so team rosters resolve correctly. A data
migration copies existing team -> teams.
Add an attendance sync service: the effective audience is the union of the
teams' rosters for the event's season plus invited, minus excluded;
sync_event_attendances reconciles Attendance rows for future events only,
adding NO_RESPONSE rows for new members and hard-deleting rows for members
no longer invited. Signals drive it: editing an event or its audience
re-syncs that event, and adding/removing a team-roster member re-syncs
that team's future events.
Register all events models in the admin (with an attendance inline) and
add events to the admin registration smoke test. Add Season.covering()
and pillow (Opponent.logo ImageField). Full suite at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add coverage 7.15 to the dev group for test-coverage reporting. Remove
the erroneous contextvars>=2.4 runtime dependency: contextvars is part of
the standard library on Python 3.14, and the PyPI backport (with its
immutables dependency) would shadow it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Introduce a comprehensive architecture document outlining the app's domain model, design principles, and shared conventions.
- Establish the foundation for multi-tenancy and tenant scoping via `Club` as the tenant root and `ClubScopedModel`.
- Detail the decomposition of the app into planned sub-apps and their responsibilities.
- Define model structure, relationships, and access control mechanisms (RBAC) through `ClubRole` and service-layer authorization.
- Provide a roadmap for tenant-aware features: seasons, rosters, events, shop, and dynamic forms.
Introduce the foundational accounts app:
- Custom email-as-username User (AbstractBaseUser + PermissionsMixin) set
as AUTH_USER_MODEL, decoupled from membership so children can be members
without a login.
- Member model holding personal/roster data (names, contact email, phone +
emergency phone via django-phonenumber-field, license number, DOB) with an
optional link to a User.
- Family household grouping and directional Guardianship (guardian -> child)
with uniqueness and no-self-guardian constraints.
- Custom UserAdmin plus Member/Family admin with inlines and autocomplete.
- Settings: register apps, AUTH_USER_MODEL, phonenumber defaults (BE/E164).
- Add CLAUDE.md and a tracked static/ directory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>