{# The polled fragment: engine health, the exposure strip, the readings, and the chart data. The `hx-*` attributes ride on this element rather than on a wrapper because the swap replaces this element — a trigger declared on a parent would keep firing at whatever the parent used to point at, and one declared on a child would be swapped away with it. The chart *shells* deliberately live outside this fragment, in `dashboard.html`. Their uPlot instances survive every poll and are handed new data by `board.js`; re-rendering them here would tear down and rebuild a canvas every 30 seconds and lose the reader's place in the process. #} {# `data-window` is read by the heatmap island, which lives outside this fragment and has no other way to learn that the range changed. It belongs HERE rather than on the island because this element is what the swap replaces: an attribute on the island would have to be kept in step by hand, and the copy that is never updated is the one that silently disagrees. One element owns the range; everything else reads it. `data-cohort` follows the same rule and has no reader yet, deliberately. The heatmap island does NOT read it: `heatmap_minute` stores a packed grid and no staff dimension, so the overlays cannot follow the cohort at all — see the caveat below. The attribute is here so that anything which ever can follow it reads the one authoritative copy instead of starting a second one. `data-timezone` is the same pattern for the third reader: uPlot draws its axes in the browser, so the site's zone has to travel as data. The exposure tick beside it is converted server-side, and both come from `site.timezone` — one page, one clock (P3.9). #}

engine {{ health.status.value }}

{# Inside the fragment so the current-range highlight swaps with the data it describes. Real `href`s, so the range still changes with scripting off — htmx intercepts the click when it is on. Every URL on this fragment carries BOTH parameters. One that names only its own is a control that silently resets the other, and the range chips and the cohort chips each render two URLs. #} {# Offered only where there is a staff zone to split on. Without one every `staff_value` is `0.0` or `None` (ADR-0021), so all three chips would answer the same zero and the control would read as broken rather than as inapplicable. #} {% if staff_configured %} {% endif %}
{# Per-camera freshness. The tiles below say what was counted; this says whether anyone is still counting. A camera that wedges keeps its tiles — a stall is a gap, not a scope that stopped existing — so without this row a dead camera and a quiet shop render identically. An age rather than a clock time: it answers "is this current" without picking a timezone, and which clock this page prints times in is still open (P3.9). #} {# The exposure strip. Every other panel on this page answers "what happened"; this one answers the question underneath it — has the box been seeing anything at all, and for how long. A quiet zone and a dead camera draw the same flat chart, and only this tells them apart. #}
Exposure {{ exposure.covered }}/{{ exposure.cells | length }} intervals with data

{{ since_label }} now

{# Two columns because there are two kinds of number here and reading one as the other is the mistake worth designing out: a level is what the floor is like at this moment, a count is what the window accumulated. Summing occupancy would report a quiet afternoon as a crowd; showing the newest bucket of footfall would report a busy day as one person. #}
{% set levels = tiles | rejectattr("total") | list %} {% set counts = tiles | selectattr("total") | list %} {% if levels %}

Now

    {# A reading with no value renders an em dash, never a zero. Zero is a measurement — it says nobody walked past. No data says nothing was reported, and printing `0` for a camera that has been dead all morning tells the operator their shop is empty. A cohort-exempt reading is marked rather than blanked. Conversion's denominator is still total footfall, so it has no staff portion to subtract — and an em dash there would read as a metric that stopped working under the filter. #} {% for tile in levels %}
  • {{ tile.label }} {{ tile.metric.value.replace("_", " ") }} {% set unaffected = tile.cohort_exempt and cohort.value != 'all' %} {%- if tile.value is none %}—{% else %}{{ tile.value | round(1) }}{% endif -%}
  • {% endfor %}
{% endif %} {% if counts %}

This {{ window.value }}

    {% for tile in counts %}
  • {{ tile.label }} {{ tile.metric.value.replace("_", " ") }} {% set unaffected = tile.cohort_exempt and cohort.value != 'all' %} {%- if tile.value is none %}—{% else %}{{ tile.value | round | int }}{% endif -%}
  • {% endfor %}
{% endif %}
{# The heatmaps cannot follow this control, and saying so is the point. `heatmap_minute` stores a packed grid and no staff dimension, so the overlays below are everyone's traffic whatever the readings above are filtered to. This caveat lives INSIDE the fragment on purpose. Rendered once in the page shell — up with the `.floors` section it describes — it would still be claiming `all` after the first chip click, which is exactly the staleness PR #51 was about. #} {% if heatmaps and cohort.value != 'all' %}

Readings show {{ cohort.value }}; heatmaps show everyone.

{% endif %}

Engine

Uptime
{{ uptime }}
Unsynced
{{ health.sync.unsynced_rows }}
Disk free
{{ health.disk.free_pct }}%
{# One request feeds the readings and the charts, so the two halves of the page can never disagree about which window they are showing. #}