{% extends "controlpanel/base.html" %} {% load static lucide %} {% block panel_title %}Platform health{% endblock panel_title %} {% block breadcrumb %} platform | {{ totals.clubs }} club{{ totals.clubs|pluralize }} live | {{ totals.members }} member{{ totals.members|pluralize }} {% endblock breadcrumb %} {% block strip_right %} {% if failed_jobs %} {{ failed_jobs|length }} job failure{{ failed_jobs|length|pluralize }} · 24h {% endif %} {% endblock strip_right %} {% block actions %} {% lucide "plus" size=14 %} Create club {% endblock actions %} {% block panel %} {# --- KPI row ------------------------------------------------------- #}
clubs live
{{ totals.clubs }}
{{ totals.archived_clubs }} archived
members
{{ totals.members }}
{{ totals.admins }} club admin{{ totals.admins|pluralize }}
dues owed
€{{ attention.dues_owed|floatformat:0 }}
{{ attention.dues_in_grace }} in grace · {{ attention.dues_overdue }} overdue
no season
{{ attention.clubs_without_season }}
can't take signups
dormant clubs
{{ attention.dormant_clubs }}
no events 30d
failed jobs
{{ failed_jobs|length }}
last 24h · jobs
{# --- left column ------------------------------------------------ #}
{# No hand-written colour key here: Chart.js renders its own real, correctly-swatched legend below the chart -- a second, uncoloured text hint duplicating it just reads as broken. #} Signups · {{ charts.signups|length }} months
Club health sorted by risk
{% for club in clubs %} {% empty %} {% endfor %}
Club Active members Events 30d Plan Risk
{{ club.name }} {{ club.active_members }} {{ club.upcoming_events }} {{ club.plan_name|default:"—" }} {% if club.risk == "high" %} high {% elif club.risk == "watch" %} watch {% else %} ok {% endif %}
No clubs yet.
Onboarding
{% for step in funnel %}
{% lucide step.icon size=12 %} {{ step.label }}
{{ step.count }}
{% endfor %}
{# --- right column ------------------------------------------------ #}
Alerts
{% if attention.renewals_pending %}
{{ attention.renewals_pending }} subscription{{ attention.renewals_pending|pluralize }} awaiting renewal
renew_subscriptions may be stalled
{% endif %} {% if attention.dues_overdue %}
{{ attention.dues_overdue }} club{{ attention.dues_overdue|pluralize }} overdue on platform fees
past grace — see billing
{% endif %} {% for run in job_log %} {% if run.status == "failure" %}
{{ run.name }} failed
{{ run.started_at|date:"H:i" }} · {{ run.error|truncatechars:60|default:"see the Jobs tab" }}
{% endif %} {% endfor %} {% if attention.admins_pending_mfa %}
{{ attention.admins_pending_mfa }} admin{{ attention.admins_pending_mfa|pluralize }} without MFA
locked out until they enrol
{% endif %} {% if attention.clubs_unbilled %}
{{ attention.clubs_unbilled }} active club{{ attention.clubs_unbilled|pluralize }} on no plan
using the platform for free
{% endif %} {% if not attention.renewals_pending and not attention.dues_overdue and not attention.admins_pending_mfa and not attention.clubs_unbilled and not failed_jobs %}
Nothing needs attention.
{% endif %}
Feature adoption
{% for flag in flags %}
{{ flag.name }} {% if flag.overridden %}{{ flag.everyone|yesno:"everyone,off" }}{% else %}{{ flag.clubs }}/{{ totals.clubs }}{% endif %}
{% empty %}
No feature flags yet.
{% endfor %}
Job log
{% for run in job_log %}
{{ run.started_at|date:"H:i" }} {% if run.status == "success" %} ok {% elif run.status == "failure" %} err {% else %} ··· {% endif %} {{ run.name }}{% if run.detail %} · {{ run.detail|truncatechars:40 }}{% endif %}
{% empty %}
No job runs recorded yet.
{% endfor %}
View all jobs →
{% endblock panel %} {% block extra_body %} {{ charts|json_script:"chart-data" }} {% endblock extra_body %}