{# poll_url and poll_interval come from status_view.with_poll, which builds the URL with status_view.poll_url; this template composes no URL. The poll carries the token of what this rendering shows, so an unchanged poll is answered 204 and focus inside the area stays put. tabindex="-1" is on every rendering because htmx restores focus by id only to a focusable element. autofocus is only on an action's response: on a poll it would pull focus out of the Title box every second. No line is aria-busy, because a screen reader may hold back a busy subtree inside a live region and "Scanning..." would never be heard; class="busy-line" draws the spinner instead. #}
{{ busy_line }}
{% elif job.state == JobState.AWAITING_FLIP %} {# The controls are emitted only for the owner, or for anyone when the job has no owner token; never hide them with CSS instead. The title gate is the other way round: a job with no owner token is answerable by anyone but shows its title to no one. There is deliberately no cancel control or second timer here: the flip timeout already covers a closed tab. non_owner_line and the waiting lines below end in a full stop, not an ellipsis, on purpose: nothing is happening on this viewer's behalf. #} {% if flip_answer %}{{ flip_answer | flip_answer_label }}
{% elif is_owner %} {% include "partials/flip.html" %} {% else %}{{ non_owner_line }}
{% endif %} {% elif job.state in pass_wait_states %} {# The same owner gate as the flip prompt. Before the worker publishes the question the owner sees busy_line, the state's progress wording, not non_owner_line. Neither waiting line is a busy-line: a spinner makes a scan that waits for a person look hung. #} {% if pass_answer %}{{ pass_answer | pass_answer_label }}
{% elif is_owner and pass_copy %} {% include "partials/pages_prompt.html" %} {% elif not is_owner %}{{ non_owner_line }}
{% else %}{{ busy_line }}
{% endif %} {% elif job.state == JobState.DONE %} {# Guard on counts being None, never on a count's truthiness: error, cancel, a refused submit and rows older than the count columns leave them NULL, while a measured 0 renders as 0. A warned upload reads amber and is not an alert: it is a degraded success. #} {% if job.warning %}⚠ {{ job.state | outcome_line(job.warning, job.title) }}
{{ job.warning }}
{% else %}✓ {{ job.state | outcome_line(job.warning, job.title) }}
{% endif %} {% set counts = job | page_counts %} {% if counts %}{{ counts }}
{% endif %} {% set removed = job | removed_pages %} {% if removed %}{{ removed }}
{% endif %} {% if terminal_reload %}{% include "partials/terminal_reload.html" %}{% endif %} {% elif job.state == JobState.FALLBACK %}→ {{ job.state | outcome_line(job.warning, job.title) }}
{% if job.warning %}{{ job.warning }}
{% endif %} {% set counts = job | page_counts %} {% if counts %}{{ counts }}
{% endif %} {% set removed = job | removed_pages %} {% if removed %}{{ removed }}
{% endif %} {% if terminal_reload %}{% include "partials/terminal_reload.html" %}{% endif %} {% elif job.state == JobState.CANCELLED %} {# A cancel is a deliberate stop, not a failure, so it is not announced as an alert. #}⊘ Cancelled: {{ job.title }}
{% if terminal_reload %}{% include "partials/terminal_reload.html" %}{% endif %} {% elif job.state == JobState.ERROR %} {# The alert wraps the next step as well as the sentence, so a screen reader hears it;⚠ {{ job.error_category | error_message }}
{{ job.error_category | error_next_step }}
{% else %}✗ {{ job.error_category | error_message }}
{{ job.error_category | error_next_step }}
{{ job.error }}
Category: {{ job.error_category }}
Job: {{ job.id }}
✗ Error: {{ job.error }}
{% endif %} {% if terminal_reload %}{% include "partials/terminal_reload.html" %}{% endif %} {% endif %} {% if job.thumbnail %}{{ idle_line }}
{% if last_job %}{{ last_scan_line }}
{% if last_scan_detail %}{{ last_scan_detail }}
{% endif %} {% if last_job.state == JobState.ERROR and last_job.error_category %}{{ last_job.error }}
Category: {{ last_job.error_category }}
Job: {{ last_job.id }}