{# ---- Heroicons (outline, MIT) as inline SVG - no build step needed ---- #} {# The sidebar's class list - ONE definition shared by the page shell and the OOB refresh, so a live update can't morph the drawer styling out from under an open drawer. Off-canvas below md; the open state lives on . #} {% macro sidebar_classes() -%} w-72 shrink-0 border-r border-line overflow-y-auto p-3 max-md:fixed max-md:inset-y-0 max-md:left-0 max-md:z-40 max-md:bg-ink max-md:-translate-x-full max-md:transition-transform max-md:duration-200 [.sidebar-open_&]:max-md:translate-x-0 {%- endmacro %} {# One badge style everywhere (paused, draining, state chips, counts) - the hand-rolled copies had drifted between px-1.5 and px-2. Gap rules: a badge annotating text sits gap-2 from it (inside the same group); sibling badges in a cluster sit gap-1.5 apart; icon-to-text inside a badge is gap-1. #} {% macro badge(tone, extra='') -%} text-xs px-1.5 py-0.5 rounded-md bg-{{ tone }}/10 text-{{ tone }} {{ extra }} {%- endmacro %} {% macro icon(name, cls="w-4 h-4") -%} {%- endmacro %} {# The shared empty/absent state: a centred icon + ONE line, so "no jobs", "no schedulers", "no workers" and "no results" all read the same. #} {% macro empty_state(icon_name='inbox', message='Nothing here', bordered=false) -%}
{{ icon(icon_name, 'w-7 h-7') }} {{ message }}
{%- endmacro %} {# The jobs-table header row: one cell per grid track, aligned by the subgrid. Rendered for EVERY state that lists rows; only the select-all checkbox is conditional (it needs the bulk bar to act on). #} {% macro jobs_head(bulk=true) -%}
{% if bulk %} {% else %}{% endif %} id job data {# right-aligned like the numeric values they head #} when ×
{%- endmacro %} {# One job as a native
accordion row. `state` keys the per-row actions (use the job's own state for search results that span states); `bulk` says whether a bulk bar exists to act on the row checkbox. #} {% macro job_row(name, j, state, page=1, bulk=true) -%}
{# One cell per grid track - check · chevron · id · name · data · progress · time · attempts · actions, in exactly this order; the subgrid does the aligning (.jobs-table in input.css). #} {% if bulk %} {% else %}{% endif %} {{ icon('chevron', 'w-4 h-4') }} #{{ j.id }} {# The name is what operators scan; the full name is one hover away. #} {{ j.name }} {% if j.id.startswith('repeat:') %} {# A scheduler's materialized next run. It's a real delayed job (so it counts and can be inspected/promoted); the badge explains why it also shows under Schedulers and names the schedule it came from. #} {{ glyph('clock', "Scheduled occurrence from scheduler '" ~ j.id.split(':')[1] ~ "'") }} {% endif %} {% if j.children_count %} {# A flow parent: quiet branch glyph + how wide the fan-out is. #} {{ icon('flow', 'w-3 h-3') }} {{ j.children_count }} children in this flow {% elif j.parent_id %} {# A flow child: its own turn-arrow glyph (belongs under a parent), so parents and children read differently at a glance. The parent link lives in the detail; the row just hints. #} {{ glyph('flow-child', 'Part of flow #' ~ j.parent_id) }} {% endif %} {% if state == 'failed' and j.failed_reason %} {# Failed rows lead with WHY - the payload is one expand away, the reason is what an operator scans for. #} {{ j.failed_reason }} {% else %} {{ j.data|tojson }} {% endif %} {# progress is an active-job concept: a finished row's 100% bar (or a failed row's stuck one) is noise next to its real outcome #} {% if j.progress is number and state == 'active' %} {% elif j.children_count and j.children_done is defined %} {# A flow parent in any tab: fan-in progress (done green, failed red, pending empty) - the same language as the detail's segmented bar. `children_done is defined` guards any path that didn't enrich the row. #} {% set total = j.children_count %} {% endif %} {# The state-relevant moment: finished / started / due / queued. Relative for scanning; the absolute moment lives on the hover title. #} {% if state == 'delayed' %} {{ (j.timestamp + (j.delay or 0))|due }} {% else %} {# a parked flow root sits under active but hasn't started - show when it was enqueued, not its (empty) processed time #} {% set ts = j.finished_on if state in ('completed', 'failed') else (j.timestamp if j.state == 'waiting-children' else (j.processed_on if state == 'active' else j.timestamp)) %} {{ ts|ago }} {% endif %} {{ j.attempts_made }}× {% if state == 'delayed' %} {% endif %} {% if state == 'failed' %} {# A failed flow parent re-drives its whole failed subtree, not just itself, so the label says so rather than undersell it as "this job". #} {% set retry_label = 'Retry flow - re-drives its failed children' if j.children_count else 'Retry this job' %} {% endif %}
{%- endmacro %} {# An icon-only metadata glyph: a muted pill, words live in the tooltip. Gap rules for badges are documented on badge() above. #} {% macro glyph(icon_name, tip) -%} {{ icon(icon_name, 'w-3 h-3') }}{{ tip }} {%- endmacro %} {# state -> display label. `waiting-children` (a flow parent parked on its kids) reads as "parked"; it has no tab of its own - such a root shows under active. #} {% macro state_label(s) -%} {%- if s == 'waiting-children' -%}parked{%- else -%}{{ s }}{%- endif -%} {%- endmacro %} {# state -> semantic status token (one token per state; see styles/input.css) #} {% macro state_token(s) -%} {%- if s == 'active' -%}info {%- elif s == 'completed' -%}success {%- elif s == 'failed' -%}danger {%- elif s == 'delayed' -%}warning {%- else -%}muted {%- endif -%} {%- endmacro %} {# state -> solid progress-bar color #} {% macro bar_color(s) -%}bg-{{ state_token(s) }}{%- endmacro %} {# state -> pill colors (one token, three fixed intensities; both themes) #} {% macro state_color(s) -%} {%- set c = state_token(s) -%}text-{{ c }} bg-{{ c }}/10 ring-{{ c }}/30 {%- endmacro %} {# A clickable job-id chip → the standalone job page. Import `with context` (url_for). #} {% macro job_chip(queue, jid) -%} {% set href = url_for('job_page', name=queue, job_id=jid).path %} #{{ jid }} {%- endmacro %} {# One node of a flow tree (recursive): id chip · status pill · name · duration · failure reason. The status sits in a fixed-width column so names stay flush within a level - the same state pills the rest of the UI uses, spelled out (no color-decoding); the active pill pulses like a draining worker. The current job's row is gently lifted so "you are here" needs no legend. Import `with context` (job_chip needs url_for). #} {# Takes a LIST of sibling nodes and emits the