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>
Ubuntu becomes the default sans and JetBrains Mono the default mono (via
--font-sans / --font-mono, which Tailwind wires to the body and <code>
defaults). Roboto and Tourney are opt-in utilities: `font-roboto`, and
`font-tourney` for display numerals like jersey numbers.
Self-hosted rather than linked from Google's CDN: a CDN <link> sends every
visitor's IP to a third party on each page load, which is an avoidable GDPR
liability for an EU club platform, and it puts someone else's uptime in our
render path. Files come from the @fontsource packages.
Two subsets each, with unicode-range: plain `latin` cannot render the Polish,
Czech or Turkish letters that turn up in member names, and the range means
latin-ext is only fetched by pages that actually contain those glyphs.
Tourney is variable on two axes, so font-stretch is declared as a range next to
the weight range -- declare only the weight and the browser clamps the width
axis to its default, making `font-stretch: 125%` (a wide shirt number) silently
do nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
daisyUI is an npm plugin, so the standalone Tailwind CLI can't load it; this
adds a real build. Themes are configured as `light --default, dark
--prefersdark`, so dark follows the OS with no JavaScript, and an explicit
data-theme (set by the toggle) overrides it.
The built CSS is committed, so `uv run manage.py runserver` remains all a
Python dev needs; `npm run watch` is only for those touching styles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>