Prometheus gains a "Widget templates" tab beside Targets. It lists the built-in templates and an admin's own, any that no longer pass the checker first. A template opens as its YAML beside a live preview drawn over a chosen target exactly as the dashboard widget draws it (routes.draw, now shared by both). An admin writes or pastes a new one, reads a .yaml upload into the editor to look over, duplicates any template under a free name, edits and deletes their own; anyone can export the YAML. Built-in templates are read-only. Custom templates are rows in prometheus_widget_templates (migration prometheus_002_templates), kept in backups, keyed "custom:<id>" so a rename keeps its widgets. Saving needs a template that passes the checker and a name no other template has. Create, update and delete are audited and link to the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
3.6 KiB
HTML
62 lines
3.6 KiB
HTML
{# plugins/prometheus/templates/prometheus/index.html — every scrape target,
|
|
those not answering first, each saying what its newest scrape found: how
|
|
many series it stored, or why it got nothing (milestone 506). #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import page_header, status_badge, stat_tile, empty_state %}
|
|
{% block title %}Prometheus — Steward{% endblock %}
|
|
{% from "prometheus/_tabs.html" import prometheus_tabs %}
|
|
{% block section_tabs %}{{ prometheus_tabs("targets") }}{% endblock %}
|
|
{% block content %}
|
|
{% set admin = session.user_role == 'admin' %}
|
|
{% call page_header("Prometheus targets", (targets|length ~ " target" ~ ("s" if targets|length != 1 else "")) if targets else none) %}
|
|
{% if admin %}<a class="btn btn-sm" href="/plugins/prometheus/targets/new">Add target</a>{% endif %}
|
|
{% endcall %}
|
|
|
|
{% if not targets %}
|
|
<div class="card">{{ empty_state("No targets yet. Add a service's /metrics address, and Steward scrapes it on a schedule so its numbers can be charted and alerted on.",
|
|
"Add target" if admin else none, "/plugins/prometheus/targets/new") }}</div>
|
|
{% endif %}
|
|
|
|
{% for t in targets %}
|
|
<section class="card" aria-labelledby="prom-{{ t.id }}">
|
|
<div class="host-health-head">
|
|
{% if t.reach == 'up' %}{{ status_badge('up', 'Answering') }}
|
|
{% elif t.reach == 'down' %}{{ status_badge('down', 'Not answering') }}
|
|
{% elif t.reach == 'stale' %}{{ status_badge('stale', 'Not scraped') }}
|
|
{% elif t.reach == 'off' %}{{ status_badge('stale', 'Turned off') }}
|
|
{% else %}{{ status_badge('pending', 'Not scraped yet') }}{% endif %}
|
|
<h2 id="prom-{{ t.id }}" class="section-title" style="margin:0;"><a href="/plugins/prometheus/targets/{{ t.id }}">{{ t.name }}</a></h2>
|
|
<span class="field-hint mono" style="min-width:0;max-width:28rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="{{ t.url }}">{{ t.url }}</span>
|
|
<span class="page-actions" style="margin-left:auto;">
|
|
<a href="/plugins/prometheus/targets/{{ t.id }}" class="btn btn-sm btn-ghost">Series</a>
|
|
{% if admin %}<a href="/plugins/prometheus/targets/{{ t.id }}/edit" class="btn btn-sm btn-ghost">Edit</a>{% endif %}
|
|
</span>
|
|
</div>
|
|
|
|
{% if t.reach == 'down' %}
|
|
<p class="tone-crit" style="margin:0 0 0.4rem;">{{ t.says }}</p>
|
|
<p class="field-hint" style="margin:0 0 0.6rem;">
|
|
{%- if t.ok_at %}Last answered {{ t.ok_at|ago }}.{% else %}It has never answered.{% endif %}
|
|
Tried {{ t.scraped_at|ago }}.</p>
|
|
{% elif t.reach in ('stale', 'off') %}
|
|
<p class="field-hint" style="margin:0 0 0.6rem;">{{ t.says }}{% if t.scraped_at %} Last scraped {{ t.scraped_at|ago }}.{% endif %}</p>
|
|
{% elif t.reach == 'pending' %}
|
|
<p class="field-hint" style="margin:0 0 0.6rem;">Not scraped yet: the first scrape runs within seconds of saving.</p>
|
|
{% endif %}
|
|
|
|
{% if t.reach == 'up' %}
|
|
<div class="stat-strip" style="margin-bottom:0;">
|
|
{{ stat_tile("Series stored", t.kept) }}
|
|
{{ stat_tile("Over the cap", t.capped, hint=("cap is " ~ t.cap) if t.capped else none) }}
|
|
{{ stat_tile("Left out by filters", t.dropped) }}
|
|
{{ stat_tile("Scrape time", (t.scrape_ms|round|int ~ " ms") if t.scrape_ms is not none else "—") }}
|
|
{{ stat_tile("Every", t.interval|duration) }}
|
|
</div>
|
|
{% if t.capped %}
|
|
<p class="tone-warn" style="margin:0.6rem 0 0;">{{ t.capped }} series weren't stored: past the cap of {{ t.cap }}, or a name too long to store. {% if admin %}<a href="/plugins/prometheus/targets/{{ t.id }}/edit">Narrow what's kept or raise the cap.</a>{% endif %}</p>
|
|
{% endif %}
|
|
{% endif %}
|
|
</section>
|
|
{% endfor %}
|
|
{% endblock %}
|