{% extends "base.html" %} {% comment %} A broadcast's detail page (SPEC §13.2: "live counters from stats json"). Everything that moves lives in _counters.html, polled every three seconds through apps/common/polling.py's conditional GET. The two htmx traps that module documents are handled in the script block at the bottom, and both are load bearing: htmx does not send If-None-Match on its own, and htmx 2's default responseHandling matches "304" with its [23].. rule, so without the prepended rule an unchanged poll would swap an empty body over the counters and blank them. {% endcomment %} {% block title %}{{ broadcast.name }} · Broadcasts · DemandTrail Messaging{% endblock %} {% block page_header %}

{{ broadcast.name }}

{% comment %} The status chip is deliberately NOT here. It changes while somebody is looking at the page — scheduled → sending → sent — and this block is outside the polled region, so a copy here would sit at whatever it was when the page loaded while the counters beside it moved. It lives in _counters.html instead, which is the fragment that refreshes. {% endcomment %}

{{ broadcast.channel_connection.display_name }}

All broadcasts
{% csrf_token %}
{% endblock %} {% block content %}
{% include "broadcasts/_counters.html" %}
Recipients Who was skipped, and why.
{% comment %} Issue #26's deliverability summary for the channel this broadcast went out on. Deliberately outside the polled region and about the *connection* rather than about this send: the counters above already answer "how did this go", and this answers the different question beside it — is the channel healthy enough to schedule the next one. It does not move while somebody watches, so it costs one query on page load and nothing thereafter. {% endcomment %} {% comment %} Clicks on the links inside the message (issue #26). Outside the polled fragment above, and deliberately: a click is a thing a recipient did rather than a state a recipient is in, so it takes no part in `queued = sent + failed + cancelled + skipped`, it does not move while somebody watches one send, and including it in _counters.html would have put a node_stat_daily aggregate on every three-second poll — including the ones that answer 304. It comes from the same counter the flow builder's overlay shows. {% endcomment %}
Clicks Links pressed in this message. Refresh to update.
{{ clicked }}
Clicked
{% if deliverability %}
{{ deliverability.connection.display_name }} deliverability Last 30 days, every message on this channel.
{{ deliverability.total }}
Messages
{{ deliverability.sent }}
Reached provider
{{ deliverability.failed }}
Failed
{# Blank rather than 0%: a rate with no denominator is not a rate. #}
{% if deliverability.delivery_rate is not None %}{{ deliverability.delivery_rate }}%{% else %}N/A{% endif %}
Delivered
{% endif %} {% endblock %} {% block alpine_components %} {% endblock %}