{# traefik/rows.html — HTMX fragment for the Traefik page: the verdict (swapped into #traefik-verdict above the request chart, #5710), routers that need attention (routes.router_state), their metric anomalies, the proxy's own figures, the healthy routers folded into one table (one over a cutoff but within its normal says so, #5741), then certificates by soonest expiry. #} {% from "_macros.html" import stat_tile, status_badge %} {# ── Verdict: what needs a look and since when, then the counts ─────────── #} {% set shown = 3 %}

{%- if problems %}{{ problems|length }} router{{ "s" if problems|length != 1 }} need{{ "s" if problems|length == 1 }} a look {%- for r in problems[:shown] %}{{ ":" if loop.first else ";" }} {{ r.name }}, {{ r.why }} {%- if r.since_before %}, all of the last {{ current_range }}{% elif r.since %}, started {{ r.since|ago }}{% endif %} {%- endfor %} {%- if problems|length > shown %}; {{ problems|length - shown }} more below{% endif %}. {%- if healthy %} {{ healthy|length }} healthy.{% endif %} {%- elif healthy %}All {{ healthy|length }} router{{ "s" if healthy|length != 1 }} healthy {%- else %}No routers reported yet. {%- endif %} {%- if gone %} {{ gone|length }} no longer reported.{% endif %}

{# ── The proxy's own figures: shown even before any router reports ───────── #} {% macro proxy_figures() %} {% if global_stat and (global_stat.config_last_reload_at or global_stat.open_conns_total is not none or global_stat.process_memory_bytes is not none) %}
{% if global_stat.config_last_reload_at %} {{ stat_tile("Config reloaded", global_stat.config_last_reload_at|ago, hint=(global_stat.config_reloads_total|int ~ " reloads") if global_stat.config_reloads_total is not none else none) }} {% endif %} {% if global_stat.open_conns_total is not none %}{{ stat_tile("Open connections", global_stat.open_conns_total|int) }}{% endif %} {% if global_stat.process_memory_bytes is not none %}{{ stat_tile("Memory", global_stat.process_memory_bytes|bytes) }}{% endif %}
{% endif %} {% endmacro %} {# A router that has answered nothing since Steward started has no latency yet. #} {% macro ms(v) %}{% if v is none %}—{% else %}{{ "%.0f"|format(v) }} ms{% endif %}{% endmacro %} {% macro latency_cell(v) %}{{ ms(v) }}{% endmacro %} {% macro router_card(r) %}
{{ status_badge(r.state, "Failing" if r.state == 'crit' else "Warning") }}

{{ r.name }}

{% if r.meaning %}{{ r.meaning.label }}: {% endif %}{{ r.why }} {%- if r.since_before %}, all of the last {{ current_range }}{% elif r.since %}, started {{ r.since|ago }}{% endif %}

{% if r.codes %}

Last 15 min: {% for code, n in r.codes[:5] %}{{ code }} ×{{ "{:,}".format(n) }}{{ ", " if not loop.last }}{% endfor %} {%- if r.codes|length > 5 %}, {{ r.codes|length - 5 }} more code{{ "s" if r.codes|length - 5 != 1 }}{% endif %}

{% elif codes_known %}

No 4xx or 5xx responses in the last 15 min.

{% else %}

Status codes not recorded yet: Steward keeps them from this version on.

{% endif %} {% if r.meaning %} {# What it means and the one next step (#5712; plugins/traefik/meaning.py). #}

{{ r.meaning.meaning }} {{ r.meaning.likely }}

Next: {%- if r.meaning.action == 'act' %} {%- if r.service %} {{ r.service }}: its tasks and logs → {%- else %} look at the app's container and logs under Docker → Swarm; Steward can't tell which service serves this router. {%- endif %} {%- else %} if this is normal for this router, raise the 4xx cutoff in Settings → Thresholds. It is one cutoff for every router {%- if not r.normed %}; once Steward has learned this router's normal for the hour, it flags only departures from it{% endif %}. {%- endif %}

{% endif %} {{ latency_cell(r.latest.latency_p95_ms) }}
Req/s{{ "%.2f"|format(r.latest.request_rate) }}{{ r.sparkline_req|safe }}
Bandwidth{{ r.latest.response_bytes_rate|rate }}{{ r.sparkline_bytes|safe }}
p95{{ r.sparkline_p95|safe }}
p99{{ ms(r.latest.latency_p99_ms) }}
4xx{{ "%.1f"|format(r.latest.error_rate_4xx_pct) }}%{{ r.sparkline_4xx|safe }}
5xx{{ "%.1f"|format(r.latest.error_rate_5xx_pct) }}%{{ r.sparkline_5xx|safe }}
Last scraped {{ r.latest.scraped_at|ago }}
{% endmacro %} {% if problems or healthy or gone or certs or old_certs %} {# ── Routers that need attention ─────────────────────────────────────────── #} {% if problems %} {# A grid, not CSS columns: a column box drops its last card's bottom margin, which left the cards touching the section below (#5214). #}
{% for r in problems %}{{ router_card(r) }}{% endfor %}
{% endif %} {# A router's 4xx share or p95 out of its own normal (#5741), and the last 30 days' anomalies, to review here. #} {% include "_anomaly_card.html" %} {{ proxy_figures() }} {# ── Healthy routers, folded ─────────────────────────────────────────────── #} {% if healthy %}
{{ healthy|length }} healthy router{{ 's' if healthy|length != 1 }}
{% for r in healthy %} {{ latency_cell(r.latest.latency_p95_ms) }} {% endfor %}
RouterReq/sBandwidthp954xx5xx
{{ r.name }}{% if r.why %}
{{ r.why }}
{% endif %}
{{ "%.2f"|format(r.latest.request_rate) }} {{ r.latest.response_bytes_rate|rate }}{{ "%.1f"|format(r.latest.error_rate_4xx_pct) }}% {{ "%.1f"|format(r.latest.error_rate_5xx_pct) }}%
{% endif %} {# ── Routers Traefik no longer reports: their last numbers are history, not a state, so they are listed by when they were last seen and nothing else. ── #} {% if gone %}
{{ gone|length }} router{{ 's' if gone|length != 1 }} no longer reported

Traefik stopped reporting these, usually because the service behind them was removed.

{% endif %} {% if healthy %} {% endif %} {# ── Certificates Traefik still serves, soonest expiry first; then the ones it no longer reports (renewed or retired), kept here but off the dashboard (#5254). #} {% if certs or old_certs %}
TLS certificates
{% if certs %} {% for cert in certs %} {% endfor %}
NameRoutersExpiresLeft
{{ cert.cn }}{% if cert.sans %}
also {{ cert.sans|join(", ") }}
{% endif %}
{% if cert.routers is none %}—{% elif cert.routers %}{{ cert.routers|join(", ") }}{% else %}not in use{% endif %} {{ cert.not_after.strftime("%Y-%m-%d") }} {{ cert_left(cert.days_remaining) }}
{% endif %} {% if old_certs %}
No longer served ({{ old_certs|length }}) — renewed or retired; Traefik stopped reporting them {% for cert in old_certs %} {% endfor %}
NameExpires
{{ cert.cn }} {{ cert.not_after.strftime("%Y-%m-%d") }}
{% endif %}
{% endif %} {% else %} {{ proxy_figures() }}

No Traefik metrics yet. Configure metrics_url in config.yaml under plugins.traefik.

{% endif %}