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
Replace the mobile nav's horizontal scroll strip (which wrapped into
an unreadable jumble because daisyUI's .menu sets flex-wrap: wrap and
.menu-horizontal never resets it) with a hamburger button that opens
a proper left-side drawer, mirrored in controlpanel for the same bug.
Move the theme toggle, "Management", and "Django admin" controls into
that drawer below `lg`, leaving only the account icon in the navbar,
so the club name has room to render in full instead of truncating to
initials.
Add a reusable `.table-cards` CSS pattern (app.css) that turns a list
table into a stack of labelled cards below `md`, and apply it to every
list/detail table in the management app -- members, families, teams,
memberships, events, groups, locations, opponents, positions, referee
levels/list, roles, sponsors, news, parent claims, team roster/staff,
and the shared family-members table. Tables revert to normal desktop
layout at `md` and up.
Member list gets extra passes: search submits icon-only below `sm` so
it fits next to the input, the Members/Guardians/Both tabs go
full-width, and last/first name merge into one column. Action-button
rows across every page stack full-width on mobile instead of wrapping
mid-button (shared fix in base.html). Home dashboard's upcoming-events
and news mini-tables become simple lists instead of overflowing
tables. Also: brand truncates instead of overflowing on a long club
name, a stray invalid xmlns attribute removed, a wrapping UUID badge
hidden below `sm` on team detail.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The old page gave every eligible member a table row, which a club with a
hundred-plus members can't use, and its search was a GET round-trip that
discarded anything already ticked. Now you add rows: pick a person from a
searchable select, pick a position, fill in the details.
The position decides the role. Position.staff_position already distinguishes
them, so a staff position creates a StaffAssignment and anything else a
TeamMembership -- no separate "player or staff?" control that could disagree
with the position picked. Someone joining as both is two rows. Jersey number
and captaincy exist only on TeamMembership, so a staff row rejects them and the
row script greys them out, keyed off the data-staff marker PositionSelect
stamps on staff options.
All-or-nothing on submit: one bad row re-renders the page with every row still
filled in and the offending field flagged, rather than saving the good rows and
losing the rest -- a partial save costs far more when the rows were typed by
hand. Cross-row checks no single row can see (the same person twice, two rows
claiming one jersey) live on the formset's clean(); per-row checks live on the
row form. Eligibility is still never trusted from the POST.
Captain and alternate captain on one row is refused as self-contradictory, but
how many captains a team may have is deliberately left alone: neither the model
nor the single-add form constrains it, and inventing the rule in one entry path
only would be bypassable by adding players one at a time. A test pins that
absence so it reads as a decision rather than an oversight.
Two implementation notes worth keeping: rows are cloned from the template's
parsed content, not its innerHTML, because assigning "<tr>...</tr>" to a
detached <div> silently drops it; and these tables deliberately skip the usual
overflow-x-auto wrapper, which would make a scroll container that clips the
picker's dropdown for every row but the first couple. Removing a row leaves
TOTAL_FORMS alone -- Django reads a form whose fields are absent from the POST
as an unchanged extra and skips it, which is safe, unlike re-indexing live
inputs.
management/tests.py also carries the setUpTestData rationalisation from the
previous commit, which couldn't be split cleanly from the new bulk-add tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
Extracts the "Grant role" member picker's typeahead combobox into a
reusable static/js/searchable-select.js (opt in via data-searchable on
the widget), and reuses it for the news form's teams field -- a proper
multiselect with removable pills and filter-as-you-type, replacing the
plain checkbox list. Needed forwarding widget attrs through the shared
select template, which never passed them to the rendered <select>.
One server now, the same image and env vars for many later: point
DJANGO_DATABASE_URL / DJANGO_REDIS_URL at central services, set a bucket, drop the
db and redis services, run several web containers behind a load balancer. No code
changes.
The wildcard certificate is what shapes this. Subdomain tenancy needs
*.rosterchief.app, and Let's Encrypt will not issue a wildcard over HTTP-01 -- only
DNS-01 -- so Caddy is built with a DNS provider plugin and needs an API token. That
single constraint is why the proxy is Caddy rather than the usual nginx+certbot.
The image apt-installs libpango and friends, which is what WeasyPrint binds to. The
PDF invoices that cannot render on a Mac without Homebrew work in the container by
construction.
Migrations are NOT run by the entrypoint: with more than one web container they
would race, and a starting gunicorn worker is a bad place to discover a failed
migration. Deploy runs them once, explicitly.
Two things the local build check caught, either of which would have failed the
image build at collectstatic (manifest storage treats a missing referenced file as
fatal):
- chart.js ended with a sourceMappingURL pointing at a .map we never vendored.
Stripped, with an npm script so re-vendoring cannot bring it back.
- The Tailwind INPUT file lived at static/src/app.css, inside the served static
tree, so collectstatic collected it and then choked on its @import "tailwindcss".
It belongs outside: it is a build input, not an asset. Now assets/app.css.
Verified locally under gunicorn + WhiteNoise + manifest storage: pages serve and
the CSS comes back hashed. The image itself is unverified -- there is no container
runtime on this machine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard leads with the numbers that are supposed to be zero, because a
dashboard of healthy counts is one nobody opens:
- Clubs with no season covering today. Seasons scope memberships, rosters and
events, so such a club cannot take a signup or schedule a match -- and it fails
silently, nothing errors, it is just inert.
- Dormant clubs: nothing on the calendar for 30 days. Churn signal.
- Admins pending MFA. RequireMFAMiddleware redirects them to enrolment, so they
are locked out of their own club until they act: a support queue, not a stat.
- Outstanding money across every club.
Then the shape of the business: an onboarding funnel (clubs → with members → with
a team → with events, which separates working clubs from shells), feature-flag
adoption per club, and two charts -- signups and revenue per month.
Charts use chart.js, self-hosted rather than pulled from a CDN, for the same
reason as the fonts: no third-party in the render path. Two things the browser
taught me: the canvas needs a height-bounded wrapper (with maintainAspectRatio
off it sizes to its parent, and a parent with no height grew it to 3489px), and
chart.js cannot read daisyUI's CSS variables, so the charts re-render on a
data-theme change or keep the light palette in dark mode.
The month series is zero-filled: a chart that skips empty months draws a smooth
line straight over a month in which nothing happened.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>