KPIs and the chart now share a single flex row (chart card wider, flex-[3])
instead of a full KPI row followed by a tall standalone chart card -- the
per-referee fee breakdown moves into the chart's own tooltip instead of a
separate list underneath, so this stays compact and the actual
games-needing-a-referee list isn't pushed below the fold.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The season-at-a-glance numbers become their own full-width KPI row (same
treatment as the existing "games in view" row above), with the chart and
per-referee breakdown as a separate full-width card below, instead of
squeezed into a narrow sidebar next to the chart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Games refereed per referee this season, as a chart.js bar chart plus a
small table with fees paid per referee, alongside season-at-a-glance KPIs
(active referees, total games refereed, average per referee) -- separate
from the existing "games in view" KPIs, which track the upcoming range
picked by the filter strip rather than season-to-date workload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Creating (or re-teaming) a home game for a club-managed team now auto-
invites every eligible referee (teams.RefereeProfile) via a new
RefereeSignup model, notifying them the same way news/events already do.
They see it as its own distinct row on the mobile Calendar (own accent
colour) and can accept or decline right there -- accepting routes through
the existing capacity-checked assign_referee (assigned_by=None marks it
self-service), so it lands as a real EventReferee row with no separate sync
step. The desktop referee-management screen and event detail page both
surface pending invites and flag self-signed-up referees distinctly from
admin assignments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Coach mode's line-up screen no longer has lines/slots -- just a yes/no
toggle per available roster player, grouped by their roster position, both
in coach mode and on the published member-side view. Replaces LineupUnit/
LineupSlot with a single LineupSelection model.
Notifications now show their full body text (no more truncatechars) and the
unread colour bar spans the full row height via self-stretch, matching the
calendar row's own marker, so it still reads correctly once a body wraps to
several lines.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
A family with several kids on a news item's audience now gets one email/one
Notification, not one per child -- deduped on resolved recipient emails, so
overlapping (not just identical) guardian sets still collapse correctly.
Event notifications are untouched: each child still needs their own reply.
Home's "Club news" teaser is also decoupled from the person-scope switcher --
a parent with no team of their own still sees their kids' team news when
they've picked their own "Me" chip, not just when "All" is selected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Once a game's line-up is published, "Your answers" switches to a read-only
status pill and a new Line-up card shows every unit/slot; a SELECTED member
can still report they can no longer make it, which flips them to Absent and
notifies the team's managers immediately (the closed-deadline guard doesn't
apply here, since a published line-up is usually well past it). Also shows
the full date for the "Meet" time, not just the hour.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
flex-1 + min-w-0 still wasn't producing equal-width In/Out (and In/Maybe/
Out) buttons in practice -- a longer label's own content could still win
a wider share. Switched every such row to CSS grid (grid-cols-2/3): each
track is a content-independent minmax(0, 1fr), so equal width is
guaranteed by the grid itself rather than relying on flex-basis/min-width
interactions. Applied to the hero In/Out row, its Cancel/Confirm pair,
event_detail's 3-way In/Maybe/Out row, and its own Cancel/Confirm pair.
Also brightened the hero's face-off/meet/location meta line (was
text-on-dark, a dim grey) to plain white, matching the readability pass
already done on the "are you in?" subtext and the button labels.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The "are you in?" subtext and the In/Out button labels were both dim
grey (text-on-dark-dim/text-on-dark) against a photo backdrop -- bumped
to white, and gave both buttons a shared border (border-white/30) for
definition against the image, same colour on both so they read as a
matched pair.
The Out reason step (label, textarea, Cancel/Confirm) used to be loose
floating controls directly on the photo -- now wrapped in one bordered/
backed panel (same border colour as the In/Out pair) so it reads as one
cohesive control instead of scattered text and buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The In/Out hero buttons (and Out's reason confirm) stopped working: Alpine
owns the toggle between a row's two sibling forms (buttons vs. the reason
prompt), and hx-boost="true" on <body> had htmx *also* intercepting the
same submit -- both ended up fighting over it. Fixed by marking every
write-action <form> across the mobile app hx-boost="false" (link
navigation, where the smooth-navigation feature actually matters, is
untouched). The one exception worth calling out: coach/lineup.html's form
uses three submit buttons sharing one <form> via formaction overrides --
htmx's boost reads the form's own action rather than the submitter's
formaction override, so a boosted click there would always have posted to
the wrong endpoint regardless of the Alpine conflict.
Also:
- A reason for Out is now mandatory, not just captured -- empty and
punctuation-only "answers" (a bare ".", "-", "??") are rejected
server-side (the authoritative check) with textarea required/minlength
as a client-side nudge on top.
- Maybe can now carry an optional reason too, visible to the same audience
as Out's (this member/family, and Coach mode's bench attendance) -- one
shared reason form in event_detail.html's per-person row, its hidden
status input following whichever of Maybe/Out was tapped.
- The 3-way In/Maybe/Out row (and the 2-way hero In/Out) now use min-w-0 on
every button so flex-1 actually splits the row evenly -- a longer
label's own intrinsic width was winning it a bigger share otherwise.
- HomeView's "Needs your answer" list now excludes events whose
registration deadline has already passed -- replying is no longer
possible there (same rule EventDetailView.post already enforces), so it
doesn't belong in a "still needs a reply" list. hero_attendance is
unaffected -- it always shows the true next event, falling back to a
read-only pill once its own deadline closes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Attendance.note already existed but nothing wrote to it. Picking "Out"
anywhere (Home's hero, Coach Today's "Also yours", event_detail's per-
person answers) now opens an optional reason field before submitting,
via an Alpine two-step within the same form rather than a separate
confirmation screen. EventDetailView.post stores it only when the status
is actually "absent", and clears it the moment someone flips back to
In/Maybe -- a stale "sick" note under a since-changed answer would just
be confusing.
Private by construction, not by a permission check: nothing anywhere
renders another member's own note -- the squad-response view stays
counts-only like it already was. The only two places that read it back
are this member/family's own "Your answers" card and Coach mode's bench
attendance screen, both already scoped to people the viewer has a real
claim on.
Also, on the shared hero In/Out buttons (Home + Coach Today): dropped the
green "In" styling -- nobody's answered yet at that point, so a green
default misleadingly read as an already-recorded answer -- and added
min-w-0 to both buttons so flex-1 actually splits the row evenly; a
longer label's own intrinsic width was winning it a bigger share
otherwise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
A bright/light event photo (e.g. a well-lit rink) let too much through at
the top and middle of the gradient, making the "Next up" eyebrow and the
face-off/location meta line hard to read against it. Increases the
gradient stops on both Home's hero card and Event detail's header --
darkening the backdrop rather than lightening the text, which was already
white/near-max.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The previous turn's all-ice-blue header wasn't what the design doc actually
called for -- its own text is explicit: "the role switcher with Coach
active (bg-ice)", i.e. the accent belongs on the switcher's active segment,
not the header background. Reverted .coach-header to dark ink (matching
Member mode's own navy) and added .role-switcher-item-active-ice (bg-ice/
ice-ink) for just the Manager pill, leaving Member mode's own white/ink
active state untouched. All the header_extra text this touched (Attendance,
Line-up) reverts to white/on-dark to match.
Fixed the switcher jumping position between modes: both shells' top info
row now carries the same min-h-11 (previously only implicit in the member
header, via its bell button's own height), and the switcher sits directly
below that row in both -- the coach header's team-picker pill row (multi-
team accounts) now renders *after* the switcher instead of before it, so
its conditional presence can't shift the switcher down anymore.
Tab bar order swapped per feedback -- Today / Squad / Schedule / [+],
not Today / Squad / [+] / Schedule -- the "+" reads better as the last,
most prominent item than sandwiched in the middle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Reworks the coach shell per feedback that the previous dark-ink header
didn't read as visually distinct from Member mode's own navy header, and
that the rounded "sheet overlaps header" treatment wasn't landing:
- .coach-header is now solid ice-blue (--color-ice, never club-themed --
Coach mode's own signature colour) with --color-ice-ink foreground text,
instead of dark ink with white text. Every header_extra block across the
coach templates (attendance, lineup) is recoloured to match.
- .coach-sheet drops the 20px rounded-top/negative-margin overlap -- flush
edge-to-edge below the header now, same join the member shell already uses.
- The header's "Head coach" label was hardcoded regardless of the account's
actual role -- CoachScopeMixin now resolves active_team_role from the
person's own current-season StaffAssignment.position on the active team,
so a team manager or physio sees their real title, not someone else's.
Tab bar is now Today / Squad / [+] / Schedule -- no Me tab (the account's
own settings already live in Member mode via the role switcher, no need
for a second one). Squad (CoachSquadView) is a new roster+staff screen for
the active team; Schedule (CoachScheduleView) is a new full upcoming-events
list for the team, each row routed straight to the coach-relevant action
(Attendance for a practice, Line-up for a game) rather than the Member-
shell RSVP page. The "+" is a raised ice-blue circle opening a small popup
with New event/New post/Add player -- three genuinely different actions,
so the button opens a menu rather than committing to one destination.
Today's own inline New event/New post/Add player buttons are gone now that
the tab bar covers the same ground.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Every icon in mobile/templates/mobile/ was a hand-rolled inline <svg> with
raw path data -- management and controlpanel have used django-lucide
throughout instead (confirmed already 100% consistent there, audited as
part of this change). django-lucide is a pure server-side Python tag with
no JS/CDN runtime dependency, so this is a drop-in swap producing the same
kind of literal <svg><path .../></svg> markup mobile already wrote by hand,
just pulled from the shared vendored icon set instead of duplicated per
call site. Mapped each shape to its nearest Lucide name: bell, house
("home" doesn't exist as a lucide name -- confirmed by hand), calendar,
newspaper, user, users, chevron-left, chevron-right, check, x. Hardcoded
hex strokes (#0b1220, #fff) became stroke="currentColor" + a text-ink/
text-white class, matching how every other icon in this app already gets
its color.
Also caught and fixed a hx-boost gap while touching coach/base.html: the
coach tab bar's own "Me" link (-> mobile:me, a Member-shell view) was
missing the hx-boost="false" every other cross-shell link already got.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
hx-boost="true" on both shells' <body> turns every same-shell link/form
into an AJAX navigation (swapping <body>'s children, pushing the URL)
instead of a full browser reload -- htmx.js was already loaded on every
page but had zero actual usage anywhere in the codebase until now.
Only <body>'s children swap under a boost, never the tag itself, so
anything that crosses between the two differently-styled shells (Member's
bg-paper vs Coach's bg-ink, different data attributes) is marked
hx-boost="false" to force a real navigation instead: both role-switcher
links, the Me page's "Teams I coach/manage" row (leads into Coach mode),
and Coach Today's "Also yours" quick-RSVP forms (post to the Member-shell
event_detail view). The calendar-sync "Add to calendar" webcal:// link is
also excluded -- it's meant to hand off to the OS calendar app, not be
treated as in-app navigation.
Also, on the Me page:
- Renamed the mode switcher and the "Teams I coach" card to "Manager"/
"Teams I coach/manage" -- a team manager assigned there isn't
necessarily a coach.
- Fixed the team badge, which was slicing short_name to 2 characters
("U16" showing as "U1") -- replaced with a plain icon instead of text,
so there's nothing left to truncate (the full team name is still the
row's own label).
- Styled the whole card dark (m-card-dark), matching the design mock's
own dark "Coach mode" promo card treatment for this part of M5.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New beyond the design mock: a "Teams I coach" card listing every
current-season StaffAssignment self.me holds (team + role), each row
linking straight into Coach mode for that team via ?team=<pk>, which
CoachScopeMixin already resolves and persists to the session. Shown for
any staffed team, not just ones self.me manages -- Coach mode's own
screens already render read-only for a non-management position, so
there's nothing to hide on this summary row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New models (events/models.py): Lineup (one per event), LineupUnit (Line 1,
Defence pair 1, ... -- coach-entered labels, no fixed sport structure since
neither Club nor Team carries one), LineupSlot (one position, optionally
filled by a member). events/services/lineup.py's publish_lineup is the
reason Attendance.AttendanceStatus.SELECTED/NOT_SELECTED existed at all --
publishing flips every slotted member to SELECTED and every other available
(non-out, non-silent) roster member to NOT_SELECTED, then notifies only the
selected players.
Placement is a native <select> per slot, batch-saved with one "Save
line-up" submit, not the design mock's drag-and-drop -- this codebase has
no established htmx interaction pattern yet (htmx.js is loaded but nothing
uses it) to build a live per-tap version on, and a reliable plain form beats
a first, unproven real-time interaction for an already-large screen.
place_member handles "swap" semantics for it: placing a member vacates any
other slot of theirs in the same lineup, bumping whoever was already in the
target slot back to the available pool.
Wires the missing pieces from C1's own docstring: the "needs you" line-up-
not-published row and the tonight card's "Line-up" button, both deferred
in that stage specifically because this model didn't exist yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
management.forms.TeamMembershipForm is shaped for one member at a time
(per-row jersey number/position/captain flags), which doesn't fit a "tap a
few names, add them" flow -- the design mock itself shows plain checkboxes,
no inline position picker. Reuses the same two eligibility rules the form
applies internally (teams.services.eligible_roster_members, minus whoever's
already on this team+season) directly instead, and lets a coach fill in
jersey number/position afterward on the desktop -- the model's own
help_text already documents a blank position as a normal, expected state.
"Suggested" (on this team last season) is real, computed data via
club.models.Season.before. "Age eligible" from the mock isn't built --
neither Club nor Team carries an age-group field to compare a birth date
against, so faking that filter would just mean it silently matched nothing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
management.forms.NewsForm defaults its teams field to every club team --
fine for an editor/admin, but a real gap for a coach, who should only ever
post as their own team, never "on behalf of" one they don't run. Re-scoped
to teams_managed_by via self.managed_teams, and made required (a coach's
post is always team-scoped, never empty/club-wide -- that stays an
editor/admin claim).
Gated with club.services.access.can_add_news, which already includes
is_coach_manager. On submit the post goes straight to
News.submit_for_review() plus the same notify_editors_of_pending_review
call the desktop's own NewsSubmitForReviewView makes, landing in an
editor's queue instead of a silent draft. The button reads "Send" rather
than the mock's "Publish" -- can_publish_news stays editor/admin-only, so
that's the honest description of what actually happens.
visibility is left at the model's own INTERNAL default rather than
building the mock's "also on club website" toggle -- an editor reviewing
the pending post can widen it before publishing if a public-site
placement is actually warranted; that's a real gate, not a decorative
row, so it isn't reproduced as one here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
management.forms.EventForm already scopes its teams field to
teams_managed_by(user, club) via EventAudienceFormMixin -- exactly the
restriction a coach needs, so it's reused as-is rather than duplicated.
Only a subset of the desktop form's fields is rendered (title, kind, teams,
location, start, answers-close deadline); the rest stays unset, all of it
optional on the model except max_referees, which is dropped from the form
entirely so its own default (2) applies instead of demanding a value this
screen has no field for.
After a successful save, replicates management.views.EventCreateView's own
notify_new_event.delay(...) call by hand, since this isn't a CreateView --
attendance sync itself is automatic via events/signals.py.
"Repeat weekly" from the design mock isn't wired up this stage -- the
recurring-series machinery is a separate form with its own fields; adding
it is later work, not something to fake with an inert toggle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
First slice of the coach-mode build (design_handoff_rosterchief_platform/
README.md's "Coach mode" section, C1-C6). Ships the foundation together with
the two screens the design doc calls out as the reason coaches install
anything at all, rather than landing a Today screen with a dead "Check
attendance" button:
- mobile/coach_mixins.py's CoachScopeMixin -- the dark-mode mirror of
PersonScopeMixin, scoped by active team (via club.services.access'
teams_staffed_by/teams_managed_by) instead of managed people.
- A standalone dark ink/ice shell (mobile/templates/mobile/coach/base.html)
with the mode's signature 20px-radius overlapping sheet, reusing the
--color-ice/--color-ink tokens that already existed in assets/mobile.css
but were unconsumed until now.
- The Coach/Member role switcher is re-added to the member shell, gated on
a real staff assignment (has_coach_access), replacing the "deliberately
not rendered yet" placeholder.
- C1 Today: stat tiles, a tonight's-session card, a silent-players "needs
you" row, and an "Also yours" card reusing HomeView's own hero-RSVP
pattern scoped to the coach's own member record.
- C2 Bench attendance: writes through events.services.attendance.
record_check_in, which existed for exactly this and had no caller yet.
Read-only for staff on a team without a management position.
Line-up (C3), create event (C4), post news (C5), and add-to-roster (C6)
follow in later stages -- see the coach-mode plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
divide-y was stacking two borders: the custom .divide-y > * + * rule (top,
--color-rule) plus Tailwind's own generated divide-y utility (bottom width/
style with no color, so it fell back to a dark currentColor). Give each
staff row its own border-b border-rule instead, the same single-border
approach the roster table already uses via .table tbody tr.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Home already had the M1 dues card; Me's own "Payments & dues" row was
explicitly stubbed out with nowhere to lead. Give it a real destination:
a Payments screen (open balances for everyone managed, reusing Home's
dues-card layout via a shared _dues_row.html partial) and a "N OPEN"
pill on the Me row itself, only shown once something is actually owed.
club.services.fees.open_dues_rows is factored out so Home and Payments
can never drift apart on what counts as "still open".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The agenda used to hard-cut at a ~14-day lookahead with no way to see
anything past it. Drop the cutoff and bucket whatever falls beyond
"Next week" into per-month groups (each its own sticky header), so the
screen stays a full upcoming agenda instead of losing events that were
still worth seeing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Fixed at h-9.5 it matched the old two-line layout, but fell short next
to a three-line row (title + meta + location). self-stretch keeps it
matching whatever the content column actually renders.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Games previously got a 4px left border while training/other events used a
3px bar next to the date — move games onto the same bar for one consistent
marker position. Also break the location name out of the crowded meta line
into its own row so it doesn't get truncated away.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Legal address (street/zip/city) split out of the "Club" card into its
own "Legal address" card. "Colours" moved to the second column, above
"Live preview" -- the card it actually drives, and now sits next to
instead of scrolled away from it.
The colour inputs are physically outside <form id="club-settings-form">
now, so each gets an explicit form="club-settings-form" attribute to
keep submitting with the rest of the identity form -- HTML supports this
natively, no JS or structural change needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New Club.event_background, uploaded from the identity page. Individual
events have no photo of their own (an established, deliberate scope
decision for this build), so this is the one club-wide stand-in for the
photo the design canvas's own hero mockups call for -- shown under the
same dark gradient the hero already used, filtered to grayscale so it
never fights the club's own brand colours. Falls back to the existing
plain dark background when nothing's uploaded.
Applied to both event hero treatments: M2's own event-detail screen (the
"big black thing" this was reported against) and Home's "next up" card.
The image and gradient are separate absolutely-positioned layers behind
a z-10 content wrapper, not a filter on the card itself, so the
grayscale treatment never touches the text/buttons drawn on top of it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New Club.legal_address/legal_zip_code/legal_city, editable from the
identity page (management:club_settings). Official document headers
(the dues invoice, the referee payment form) previously borrowed the
club's home Location for this -- conflating "where we play" with "our
registered address", which aren't always the same place. New
club.services.invoicing.resolve_document_address(club) picks the club's
own legal address when set, falling back to the home Location exactly as
before when it isn't, so nothing breaks for a club that hasn't set one
yet. Location.is_home now means only what it always should have: telling
a home game from an away one.
Renamed the shared "home_location" template/context variable to
"document_address" on both PDFs to match -- it was never accurate once a
legal address could win instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New test: publishing a news item with the "notify linked members" toggle
checked creates a Notification, sends a push (mocked webpush call
asserted), and the row actually shows up -- unread -- in the member's
own mobile inbox. Confirms mobile.signals' post_save hook on
Notification (built generically, not news-specific) correctly catches
notifications created from the management app's own publish flow, not
just the ones mobile's own views create. No bugs found; this closes the
loop the request asked to verify.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The header bell's unread indicator was an 8px dot -- now a proper count
badge (capped at "9+"), matching the size/legibility of similar badges
elsewhere in the app. Notifications also gains "Clear all" next to
"Mark all read" -- a hard delete of the list (the same clear-all gesture
a phone's own notification centre uses), not another read-state flip.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
EventDetailView.post now rejects a POST once event.deadline has passed
(400), matching the read-only treatment the UI already showed -- a
disabled button is only a hint, not a guarantee against a direct POST.
M2's own "Your answers" card now also shows a read-only status pill
instead of the In/Maybe/Out buttons once closed, the same way Home's
hero card already did.
Also fixed Calendar's window: it was pinned to "through next calendar
week's Sunday", which meant the effective lookahead shrank to as little
as 8-9 days whenever today fell late in the week (e.g. a Friday), so an
event 10 days out could silently not appear even though it's well within
what "the next two weeks" should mean. The window is now always >= 14
days from today regardless of which weekday today is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New events.tasks.send_deadline_reminders (daily beat schedule, registered
in features.jobs.JOB_REGISTRY for the control panel's Jobs tab), the gap
notify_new_event's own docstring flagged: a recurring series' occurrences
never go through that on-creation path (they're bulk-generated by
extend_event_series, and notifying per-occurrence there would flood
everyone), so they never got any "you need to answer this" nudge at all.
This sweeps every upcoming event once, one week before whichever cutoff
matters -- the event's own answer deadline, or its start when none is
set -- and notifies whoever's still NO_RESPONSE, using the same
notify_members() -> Notification -> push chain notify_new_event already
uses. New Event.deadline_reminder_sent_at makes it idempotent: each
event's window opens once, is marked processed regardless of whether
anyone needed notifying, and a missed run still catches anything whose
window hasn't fully closed by the time the job next runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
New events.tasks.notify_new_event, scheduled from
management.views.EventCreateView.form_valid -- the deliberate "a staff
member planned one new event" action, not every Event row that happens
to get created. A recurring series' rolling-horizon extension
(extend_event_series) and bulk fixture imports are NOT wired to this on
purpose: either would flood everyone with one push per occurrence
instead of the single, deliberate action this is meant to catch.
Notifies whoever is still NO_RESPONSE right after creation -- exactly
"everyone who needs to respond", using the existing notify_members() ->
Notification -> mobile.signals push chain already in place, so this is
in-app (visible in the M7 inbox, mark read/mark-all-read) and a push
notification both, with no new plumbing needed there.
Generalized mobile.views.NotificationsView's News-only "tap to open the
source" handling (previously isinstance(source, News)) into
_notification_source_link, covering Event sources too -- tapping an
"new event" notification now marks it read and opens the event's answer
screen, the same way a news notification already opened the article.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Calendar gains a real "All/Games/Practices" filter (?kind=), a working
version of the design mock's own "Games only" pill -- its List/Month
toggle isn't reproduced, that's a whole second view mode, not a filter.
base.html gains a header_extra block so a screen can add its own title
into the *same* navy app-header instead of stacking a second,
differently-coloured block underneath it. Me's avatar/name/subtitle row
moved there from its own bg-ink block (a visible colour seam that didn't
match the design canvas's own M5 markup, which keeps it all in one navy
area), and Calendar now shows a "Calendar" title the same way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Previously the whole "Club news" card just vanished when there was
nothing to show, dropping the "All news" link along with it. Now it
always renders -- either the real teaser or a "No news yet" card, still
linking through to mobile:news_list either way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Face-off/Meet/Where each sat at the top of their row instead of centered
against the value -- fine for a single-line value, but Where's two-line
address made the label visibly misaligned. Added items-center to each
row's flex container.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
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
Home's news card now shows up to 3 recent items (lead item with its photo
placeholder, the next two as compact rows) instead of just the latest
one, plus an "All news" link -- the design canvas's own M1 markup already
had that link, just unbuilt until now. Both also now filter by
visibility (internal or both, never external-only -- that's the public
website's own audience), which the teaser never actually enforced before.
New mobile:news_list page is the full archive: every published,
member-visible item for the club, newest first, not narrowed to any
particular team the way Home's own teaser is. Paginated at 20/page. The
bottom tab bar's News tab now links here instead of a dead #news anchor
on Home.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
- Home: the person switcher (chip row) now lives in the white content area
instead of the navy header, matching the design canvas's own M1 markup,
and renders smaller. Home is now the only screen that shows it at all.
- Calendar: dropped the person switcher and the "My schedule"/"All members"
toggle entirely -- it always shows every event self.managed_people is
invited to, full stop. Rows now run edge-to-edge (-mx-4) instead of
living in a rounded, inset card, matching the M3 design canvas's own
full-bleed layout. (The design mock's List/Month/"Games only" controls
aren't reproduced -- no real functionality behind them yet.)
- Home's "Needs your answer" card caps at 5 items with a "+N more in
Calendar" link, so it can't crowd the dues/news cards below it off the
first screenful.
- Home gains a sponsors strip at the very bottom (horizontally scrolling,
scrollbar hidden) -- active sponsors only, reshuffled on every request.
club.services.sponsors.active_sponsors factors the "what counts as
active" query out of club/api.py's public sponsors endpoint so both it
and Home share one definition.
- Also removed mobile/views.py's now-fully-dead _PlaceholderScreen and its
template -- every M1-M7 screen has had a real implementation for a while
and nothing subclassed it anymore.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
active_tab/screen_title were set as class attributes on every screen view,
but only ever threaded into the template context by _PlaceholderScreen's
own get_context_data -- which no real M1-M7 screen has inherited from
since each one got its own concrete implementation. Every screen's own
get_context_data override skipped them, so `active_tab` was always
undefined in the template and the tab bar never lit up. Defaulted both
from the class attributes in PersonScopeMixin (every screen's shared
base) via kwargs.setdefault, so a screen with its own dynamic
screen_title (EventDetailView's event.title) still wins.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
Still scrolls the same way, just without the visible scrollbar chrome --
a new .scrollbar-hide utility (scrollbar-width/-ms-overflow-style plus
the WebKit pseudo-element) rather than anything scroll-behaviour-affecting.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The shell cache was cache-first: once a browser had static/css/mobile.css
cached, nothing in the fetch handler ever re-requested it, so every CSS/JS
fix shipped since install stayed invisible (exactly what happened with
the crest becoming round -- an already-loaded app kept showing the old
hexagon). Switched to network-first with a cache fallback for offline use
only, and bumped the cache name so existing installs drop their stale
v1 cache on next activation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The header crest was management.css's hexagon clip-path mark, and its
no-logo fallback rendered an empty coloured hexagon with no initials at
all. Both now render as a plain circle, and the fallback shows the
club's initials on its own colour -- same source (Club.logo/Club.initials)
the PWA install icon already uses, just inline in the header instead of
a generated PNG.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
DEFAULT_DAY_START_HOUR/DEFAULT_DAY_END_HOUR only widened the grid's
default 08:00-22:00 window for an event outside it, on whichever end
that event fell on -- so a single early-morning event (e.g. one starting
at midnight) widened the start but left the end clipped at 22:00 rather
than covering the full day. Default the window to the full day (0-24)
instead; the same expansion logic still guards a pathological
almost-midnight-ending event, it just never needs to trigger anymore.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
A lone member (or a parent of exactly one child) still lands straight on
their own record and never sees the switcher, same as before. The moment
there's more than one managed person, an "All" chip appears first and is
selected by default -- Home's cards (hero, needs-your-answer, dues) now
aggregate across everyone in scope instead of just one person, and
Calendar's own "My schedule" default does the same for its agenda.
PersonScopeMixin gains ``scope_everyone`` (bool) and ``people_in_scope``
(the effective list to filter by, regardless of which mode is active) --
kept separate from Calendar's own, unrelated ``?scope=all`` toggle
("every club event" vs. this "every person I manage"). Every RSVP form
now names its target member explicitly (``member_id``) rather than
relying on the old implicit "whoever is currently scoped" fallback, which
stops working the moment "All" -- not one person -- is the default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
mobile/templates/mobile/_avatar.html replaces four copies of the same
rounded-full/font-display/initials-slicing markup across event_detail.html,
me.html (the account's own header avatar and each "People I manage" row),
and news_detail.html's byline -- size/text/background stay parameterized
per call site so nothing changes visually.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
The last of the seven Member-mode screens. A real editable form for the
fields Member actually has (first/last name, date of birth, email,
phone, emergency phone) -- the design mock's national-register-number,
address, allergies/notes and consent-toggle rows have no backing field
and are omitted rather than added as new schema for a screen-building
pass. Two mock rows do have real data and are shown read-only instead:
every guardian via Member.guardians ("Emergency contact"), and any open
onboarding requirement for the person's current-season membership (via
club.services.onboarding.checklist_for) as a banner -- no upload/complete
action, that stays staff-only elsewhere in the platform.
Authorization: the target Member (from the URL) must be one of the
signed-in account's managed_people, checked on both GET and POST -- an
unmanaged id 404s, same as another club's Event/News already does
elsewhere in this app.
This completes M1-M7 (Home, Event detail, Calendar, News article, Me &
my people, Edit personal info, Notifications). Coach mode (C1-C6) is a
separate, later phase per the design doc and is intentionally not
started here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9