Competition rows (events.services.competitions' per-club data-source gate) could previously only be managed through the Django admin. Adds a card alongside Flags/Switches with create/edit/delete, mirroring the Flags card's own modal pattern -- no cascading effects to weigh on delete since Event.competition matches by name, not a foreign key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
240 lines
14 KiB
HTML
240 lines
14 KiB
HTML
{% extends "controlpanel/base.html" %}
|
|
{% load lucide ui %}
|
|
|
|
{% block panel_title %}Features{% endblock panel_title %}
|
|
|
|
{% block breadcrumb %}
|
|
<span class="text-ink">features</span>
|
|
<span class="text-edge">|</span>
|
|
<span>{{ flags|length }} flag{{ flags|length|pluralize }}</span>
|
|
<span class="text-edge">|</span>
|
|
<span>{{ switches|length }} switch{{ switches|length|pluralize }}</span>
|
|
<span class="text-edge">|</span>
|
|
<span>{{ competitions|length }} competition{{ competitions|length|pluralize }}</span>
|
|
{% endblock breadcrumb %}
|
|
|
|
{% block actions %}
|
|
<button class="btn btn-primary gap-2" type="button" onclick="document.getElementById('flag_create_modal').showModal()">{% lucide "plus" size=16 %} New feature</button>
|
|
<button class="btn btn-outline gap-2" type="button" onclick="document.getElementById('competition_create_modal').showModal()">{% lucide "plus" size=16 %} New competition</button>
|
|
{% endblock actions %}
|
|
|
|
{% block panel %}
|
|
{% url 'controlpanel:flag_create' as flag_create_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id="flag_create_modal" title="New feature" form=flag_form action_url=flag_create_url submit_label="Create" submit_icon="plus" %}
|
|
{% url 'controlpanel:competition_create' as competition_create_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id="competition_create_modal" title="New competition" form=competition_form action_url=competition_create_url submit_label="Create" submit_icon="plus" %}
|
|
|
|
{% comment %}
|
|
The lock-down. Clubs get a maintenance page, the scheduled jobs stand down, and the
|
|
control panel and the auth screens stay open — otherwise you could not sign in to
|
|
turn it back off. This is the single most safety-critical control in the panel, so
|
|
the active state gets the full club/club-dark treatment, not just a badge.
|
|
{% endcomment %}
|
|
<div class="card overflow-hidden {% if maintenance.is_active %}border-2 border-club{% endif %}">
|
|
{% if maintenance.is_active %}
|
|
<div class="flex flex-wrap items-center gap-2.5 border-b border-club bg-club px-4 py-3">
|
|
{% lucide "lock" size=18 class="shrink-0 text-white" %}
|
|
<span class="font-display text-sm font-extrabold tracking-[.1em] text-white uppercase">Maintenance mode — platform closed</span>
|
|
<span class="flex-1"></span>
|
|
<span class="font-mono text-[11px] text-white/80">since {{ maintenance.started_at|date:"j M Y, H:i" }}{% if maintenance.started_by %} · {{ maintenance.started_by.email }}{% endif %}</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="flex items-center gap-2.5 border-b border-line bg-subhead px-4 py-3">
|
|
{% lucide "wrench" size=18 class="shrink-0 text-muted" %}
|
|
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">Maintenance mode</span>
|
|
<span class="flex-1"></span>
|
|
<span class="flex items-center gap-1.5 font-mono text-[11px] text-ok-text">
|
|
<span class="h-[7px] w-[7px] rounded-full bg-ok"></span>
|
|
platform open
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card-body">
|
|
{% if maintenance.is_active %}
|
|
{% if maintenance.message %}
|
|
<div>
|
|
<div class="mb-1.5 font-mono text-[10px] tracking-[.08em] text-muted uppercase">Message shown to clubs</div>
|
|
<div class="border border-line bg-subhead px-3 py-2.5 font-mono text-sm text-ink">{{ maintenance.message }}</div>
|
|
</div>
|
|
{% endif %}
|
|
<p class="text-sm text-muted">Every club subdomain is serving a maintenance page and the scheduled jobs have stood down. The control panel and the sign-in screens stay open.</p>
|
|
|
|
<form class="mt-1" method="post" action="{% url 'controlpanel:maintenance' %}">
|
|
{% csrf_token %}
|
|
<button class="btn btn-success gap-2" type="submit">{% lucide "lock-open" size=16 %} Reopen the platform</button>
|
|
</form>
|
|
{% else %}
|
|
<p class="text-sm text-muted">Closes every club subdomain and stands the scheduled jobs down. The control panel and the sign-in screens stay open.</p>
|
|
|
|
<form class="flex flex-col gap-3" method="post" action="{% url 'controlpanel:maintenance' %}">
|
|
{% csrf_token %}
|
|
<div class="form-control w-full">
|
|
<label class="label-text mb-1.5 block" for="{{ maintenance_form.message.id_for_label }}">{{ maintenance_form.message.label }}</label>
|
|
{{ maintenance_form.message|daisy }}
|
|
<span class="label-text-alt mt-1 block">{{ maintenance_form.message.help_text }}</span>
|
|
</div>
|
|
<button class="btn btn-error gap-2 self-start" type="submit">{% lucide "lock" size=16 %} Close the platform</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="flex items-center gap-2.5 border-b border-line px-4 py-3">
|
|
{% lucide "flag" size=16 class="shrink-0 text-muted" %}
|
|
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">Flags</span>
|
|
<span class="flex-1"></span>
|
|
<span class="font-mono text-[11px] text-muted">{{ flags|length }} flag{{ flags|length|pluralize }}</span>
|
|
</div>
|
|
<p class="px-4 pt-3 pb-3 text-sm text-muted">
|
|
Flags are turned on per club. Setting <span class="font-semibold text-ink">Everyone</span> to Yes or No overrides club targeting entirely.
|
|
</p>
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Everyone</th>
|
|
<th>Clubs</th>
|
|
<th>Note</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for flag in flags %}
|
|
<tr>
|
|
<td class="font-mono">{{ flag.name }}</td>
|
|
<td>
|
|
{% if flag.everyone is True %}
|
|
<span class="badge badge-success">On for all</span>
|
|
{% elif flag.everyone is False %}
|
|
<span class="badge badge-error">Off everywhere</span>
|
|
{% else %}
|
|
<span class="badge badge-info">Per club</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="font-mono tabular-nums text-muted">{{ flag.clubs.count }}</td>
|
|
<td class="max-w-xs truncate text-muted">{{ flag.note|default:"—" }}</td>
|
|
<td class="text-right">
|
|
<button class="btn btn-outline btn-sm gap-1" type="button" onclick="document.getElementById('{{ flag.pk|dom_id:"flag_edit_modal" }}').showModal()">{% lucide "pencil" size=14 %} Edit</button>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="5" class="text-center text-muted">No features yet.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% comment %} Dialogs live outside the table: <tbody> may only contain <tr> elements. {% endcomment %}
|
|
{% for flag in flags %}
|
|
{% url 'controlpanel:flag_update' flag.pk as flag_update_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id=flag.pk|dom_id:"flag_edit_modal" title="Edit "|add:flag.name form=flag.edit_form action_url=flag_update_url submit_label="Save" submit_icon="check" %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="flex items-center gap-2.5 border-b border-line px-4 py-3">
|
|
{% lucide "power" size=16 class="shrink-0 text-muted" %}
|
|
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">Switches</span>
|
|
<span class="flex-1"></span>
|
|
<span class="font-mono text-[11px] text-muted">{{ switches|length }} switch{{ switches|length|pluralize }}</span>
|
|
</div>
|
|
<p class="px-4 pt-3 pb-3 text-sm text-muted">Global on/off for the whole platform — kill-switches, maintenance, infra rollouts.</p>
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Note</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for switch in switches %}
|
|
<tr>
|
|
<td class="font-mono">{{ switch.name }}</td>
|
|
<td class="text-muted">{{ switch.note|default:"—" }}</td>
|
|
<td class="text-right">
|
|
<form method="post" action="{% url 'controlpanel:switch_toggle' switch.pk %}">
|
|
{% csrf_token %}
|
|
<button class="relative inline-block h-[22px] w-10 shrink-0 rounded-full transition-colors {% if switch.active %}bg-ok{% else %}bg-edge{% endif %}" type="submit" aria-pressed="{% if switch.active %}true{% else %}false{% endif %}" aria-label="Toggle {{ switch.name }}">
|
|
<span class="absolute top-[3px] h-4 w-4 rounded-full bg-white transition-all {% if switch.active %}right-[3px]{% else %}left-[3px]{% endif %}"></span>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="3" class="text-center text-muted">No switches yet — add one in the Django admin.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="flex items-center gap-2.5 border-b border-line px-4 py-3">
|
|
{% lucide "trophy" size=16 class="shrink-0 text-muted" %}
|
|
<span class="font-display text-sm font-extrabold tracking-[.1em] text-ink uppercase">Competitions</span>
|
|
<span class="flex-1"></span>
|
|
<span class="font-mono text-[11px] text-muted">{{ competitions|length }} competition{{ competitions|length|pluralize }}</span>
|
|
</div>
|
|
<p class="px-4 pt-3 pb-3 text-sm text-muted">
|
|
Data sources a game's score/status can be fetched from — see events.services.competitions. A club only sees one in its event form once its feature flag is on for them.
|
|
</p>
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Sport</th>
|
|
<th>Module</th>
|
|
<th>Flag</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for competition in competitions %}
|
|
<tr>
|
|
<td class="font-mono">{{ competition.name }}</td>
|
|
<td>{{ competition.get_sport_type_display }}</td>
|
|
<td class="max-w-xs truncate text-muted">{{ competition.module }}</td>
|
|
<td>
|
|
{% if competition.flag %}
|
|
<span class="badge badge-info">{{ competition.flag.name }}</span>
|
|
{% else %}
|
|
<span class="badge badge-neutral">No flag — hidden everywhere</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="flex justify-end gap-1">
|
|
<button class="btn btn-outline btn-sm gap-1" type="button" onclick="document.getElementById('{{ competition.pk|dom_id:"competition_edit_modal" }}').showModal()">{% lucide "pencil" size=14 %} Edit</button>
|
|
<button class="btn btn-outline btn-error btn-sm gap-1" type="button" onclick="document.getElementById('{{ competition.pk|dom_id:"competition_delete_modal" }}').showModal()">{% lucide "trash-2" size=14 %} Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="5" class="text-center text-muted">No competitions yet.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% comment %} Dialogs live outside the table: <tbody> may only contain <tr> elements. {% endcomment %}
|
|
{% for competition in competitions %}
|
|
{% url 'controlpanel:competition_update' competition.pk as competition_update_url %}
|
|
{% include "controlpanel/_modal_form.html" with modal_id=competition.pk|dom_id:"competition_edit_modal" title="Edit "|add:competition.name form=competition.edit_form action_url=competition_update_url submit_label="Save" submit_icon="check" %}
|
|
|
|
{% url 'controlpanel:competition_delete' competition.pk as competition_delete_url %}
|
|
{% include "controlpanel/_confirm_modal.html" with modal_id=competition.pk|dom_id:"competition_delete_modal" title="Delete "|add:competition.name body="No club-visible data is lost — Event.competition matches by name, not a foreign key, so existing games simply stop offering a live-score fetch." action_url=competition_delete_url submit_label="Delete" %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endblock panel %}
|