Moving the figures below the problem cards put them inside the routers block, so with no routers yet the config-reload, connection and memory tiles vanished (CI 9100, test_the_config_tile_says_when_it_last_reloaded). They are a macro now, drawn in both branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
8.8 KiB
HTML
164 lines
8.8 KiB
HTML
{# 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), the proxy's own figures, the healthy
|
|
routers folded into one table, 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 %}
|
|
<p class="page-verdict" id="traefik-verdict" hx-swap-oob="true">
|
|
{%- if problems %}<strong class="tone-{{ problems[0].state }}">{{ problems|length }} router{{ "s" if problems|length != 1 }} need{{ "s" if problems|length == 1 }} a look</strong>
|
|
{%- for r in problems[:shown] %}{{ ":" if loop.first else ";" }} <span style="word-break:break-all;">{{ r.name }}</span>, {{ 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 %}<strong class="tone-ok">All {{ healthy|length }} router{{ "s" if healthy|length != 1 }} healthy</strong>
|
|
{%- else %}<span class="tone-muted">No routers reported yet.</span>
|
|
{%- endif %}
|
|
{%- if gone %} <span class="tone-muted">{{ gone|length }} no longer reported.</span>{% endif %}
|
|
</p>
|
|
|
|
{# ── 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) %}
|
|
<div class="stat-strip">
|
|
{% 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 %}
|
|
</div>
|
|
{% endif %}
|
|
{% endmacro %}
|
|
|
|
{% macro router_card(r) %}
|
|
<div class="card">
|
|
<div class="host-health-head">
|
|
{{ status_badge(r.state, "Failing" if r.state == 'crit' else "Warning") }}
|
|
<h3 class="section-title" style="margin:0;word-break:break-all;">{{ r.name }}</h3>
|
|
</div>
|
|
<p class="problem-detail" style="margin-bottom:0.5rem;">{{ r.why }}
|
|
{%- if r.since_before %}, all of the last {{ current_range }}{% elif r.since %}, started {{ r.since|ago }}{% endif %}</p>
|
|
<table class="table" style="font-size:0.85rem;">
|
|
<tr><td class="field-hint">Req/s</td><td>{{ "%.2f"|format(r.latest.request_rate) }}</td><td>{{ r.sparkline_req|safe }}</td></tr>
|
|
<tr><td class="field-hint">Bandwidth</td><td>{{ r.latest.response_bytes_rate|rate }}</td><td>{{ r.sparkline_bytes|safe }}</td></tr>
|
|
<tr><td class="field-hint">p95</td><td class="tone-{{ status_info(status_class(r.latest.latency_p95_ms, 'latency'))[0] }}">{{ "%.0f"|format(r.latest.latency_p95_ms) }} ms</td><td>{{ r.sparkline_p95|safe }}</td></tr>
|
|
<tr><td class="field-hint">p99</td><td>{{ "%.0f"|format(r.latest.latency_p99_ms) }} ms</td><td></td></tr>
|
|
<tr><td class="field-hint">4xx</td><td class="{{ 'tone-warn' if r.latest.error_rate_4xx_pct > error_4xx else '' }}">{{ "%.1f"|format(r.latest.error_rate_4xx_pct) }}%</td><td>{{ r.sparkline_4xx|safe }}</td></tr>
|
|
<tr><td class="field-hint">5xx</td><td class="{{ 'tone-crit' if r.latest.error_rate_5xx_pct > error_5xx else '' }}">{{ "%.1f"|format(r.latest.error_rate_5xx_pct) }}%</td><td>{{ r.sparkline_5xx|safe }}</td></tr>
|
|
</table>
|
|
<div class="field-hint" style="margin-top:0.5rem;">Last scraped {{ r.latest.scraped_at|ago }}</div>
|
|
</div>
|
|
{% 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). #}
|
|
<div class="node-grid" style="margin-bottom:1rem;">
|
|
{% for r in problems %}{{ router_card(r) }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{{ proxy_figures() }}
|
|
|
|
{# ── Healthy routers, folded ─────────────────────────────────────────────── #}
|
|
{% if healthy %}
|
|
<details class="card" id="traefik-healthy" style="margin-bottom:1rem;">
|
|
<summary class="section-title" style="cursor:pointer;">{{ healthy|length }} healthy router{{ 's' if healthy|length != 1 }}</summary>
|
|
<div class="table-wrap" style="margin-top:0.75rem;">
|
|
<table class="table" style="font-size:0.85rem;">
|
|
<thead><tr><th>Router</th><th>Req/s</th><th>Bandwidth</th><th>p95</th><th>4xx</th><th>5xx</th></tr></thead>
|
|
<tbody>
|
|
{% for r in healthy %}
|
|
<tr>
|
|
<td style="word-break:break-all;">{{ r.name }}</td>
|
|
<td>{{ "%.2f"|format(r.latest.request_rate) }}</td>
|
|
<td>{{ r.latest.response_bytes_rate|rate }}</td>
|
|
<td class="tone-{{ status_info(status_class(r.latest.latency_p95_ms, 'latency'))[0] }}">{{ "%.0f"|format(r.latest.latency_p95_ms) }} ms</td>
|
|
<td>{{ "%.1f"|format(r.latest.error_rate_4xx_pct) }}%</td>
|
|
<td>{{ "%.1f"|format(r.latest.error_rate_5xx_pct) }}%</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</details>
|
|
{% 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 %}
|
|
<details class="card" style="margin-bottom:1rem;">
|
|
<summary class="section-title" style="cursor:pointer;">{{ gone|length }} router{{ 's' if gone|length != 1 }} no longer reported</summary>
|
|
<p class="field-hint" style="margin:0.5rem 0;">Traefik stopped reporting these, usually because the service behind them was removed.</p>
|
|
<ul class="problem-list">
|
|
{% for r in gone %}
|
|
<li><span style="word-break:break-all;">{{ r.name }}</span> <span class="problem-detail">last seen {{ r.latest.scraped_at|ago }}</span></li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
{% endif %}
|
|
{% if healthy %}
|
|
<script>
|
|
// The fragment is re-polled; keep the healthy list as the reader left it.
|
|
(function () {
|
|
var el = document.getElementById("traefik-healthy");
|
|
if (!el) return;
|
|
el.open = !!window.__traefikHealthyOpen;
|
|
el.addEventListener("toggle", function () { window.__traefikHealthyOpen = el.open; });
|
|
})();
|
|
</script>
|
|
{% 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 %}
|
|
<div class="card">
|
|
<div class="section-title" style="margin-bottom:0.75rem;">TLS certificates</div>
|
|
{% if certs %}
|
|
<table class="table" style="font-size:0.85rem;">
|
|
<thead><tr><th>Name</th><th>Routers</th><th>Expires</th><th style="text-align:right;">Left</th></tr></thead>
|
|
<tbody>
|
|
{% for cert in certs %}
|
|
<tr>
|
|
<td style="word-break:break-all;">{{ cert.cn }}{% if cert.sans %}<div class="field-hint">also {{ cert.sans|join(", ") }}</div>{% endif %}</td>
|
|
<td class="field-hint">{% if cert.routers is none %}—{% elif cert.routers %}{{ cert.routers|join(", ") }}{% else %}not in use{% endif %}</td>
|
|
<td class="field-hint">{{ cert.not_after.strftime("%Y-%m-%d") }}</td>
|
|
<td style="text-align:right;white-space:nowrap;" class="tone-{{ status_info(status_class(cert.days_remaining, 'tls_days'))[0] }}">{{ cert_left(cert.days_remaining) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% endif %}
|
|
{% if old_certs %}
|
|
<details style="margin-top:0.75rem;">
|
|
<summary class="field-hint">No longer served ({{ old_certs|length }}) — renewed or retired; Traefik stopped reporting them</summary>
|
|
<table class="table" style="font-size:0.85rem;margin-top:0.5rem;">
|
|
<thead><tr><th>Name</th><th>Expires</th></tr></thead>
|
|
<tbody>
|
|
{% for cert in old_certs %}
|
|
<tr style="color:var(--text-muted);">
|
|
<td style="word-break:break-all;">{{ cert.cn }}</td>
|
|
<td class="field-hint">{{ cert.not_after.strftime("%Y-%m-%d") }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</details>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
{{ proxy_figures() }}
|
|
<div class="card">
|
|
<p class="field-hint">No Traefik metrics yet. Configure <code>metrics_url</code> in <code>config.yaml</code> under <code>plugins.traefik</code>.</p>
|
|
</div>
|
|
{% endif %}
|