Files
FabledSteward/plugins/prometheus/templates/prometheus/series.html
T
bvandeusenandClaude Opus 5.5 bd762307e0
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m16s
CI / unit (push) Failing after 1m29s
CI / publish (push) Skipped
feat(prometheus): the widget template library: list, edit with live preview, paste in, upload, export, duplicate (#5692)
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>
2026-10-14 09:22:31 -04:00

69 lines
3.7 KiB
HTML

{# plugins/prometheus/templates/prometheus/series.html — one stored series
of a target (milestone 506): its newest value and a chart over a chosen
range. A *_timestamp_seconds series charts as moments, not numbers. #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs, page_header %}
{% block title %}{{ metric }} — {{ target.name }} — Steward{% endblock %}
{% from "prometheus/_tabs.html" import prometheus_tabs %}
{% block section_tabs %}{{ prometheus_tabs("targets") }}{% endblock %}
{% block breadcrumb %}{{ crumbs([("Prometheus", "/plugins/prometheus/"), (target.name, "/plugins/prometheus/targets/" ~ target.id), (metric, "")]) }}{% endblock %}
{% block content %}
{% call page_header(metric, labels or none) %}
{% for h in ranges %}
<a href="?resource={{ resource|urlencode }}&metric={{ metric|urlencode }}&hours={{ h }}" class="range-btn{% if hours == h %} active{% endif %}"{% if hours == h %} aria-current="true"{% endif %}>{% if h < 168 %}{{ h }}h{% else %}7d{% endif %}</a>
{% endfor %}
{% endcall %}
<p class="page-verdict">Now: <strong>{% if now.moment %}{{ now.moment|ago }}{% else %}{{ now.text }}{% endif %}</strong></p>
<section class="card" aria-label="{{ metric }} over the last {{ hours }} hours">
<div style="position:relative;height:260px;">
<canvas id="prom-chart" role="img" aria-label="{{ metric }} over the last {{ hours }} hours"></canvas>
</div>
</section>
<p class="field-hint">To be told when it crosses a line, add an alert rule on <strong>Prometheus targets</strong> → <span class="mono">{{ resource }}</span> → <span class="mono">{{ metric }}</span>.
<a href="/alerts/rules/new">New alert rule</a></p>
<script>
(function () {
var points = {{ points_json | safe }};
var hours = {{ hours }};
var moments = {{ "true" if stamp_axis else "false" }};
var el = document.getElementById("prom-chart");
if (points.length < 2) {
el.parentElement.innerHTML = '<p class="field-hint" style="margin:0;">Not enough samples yet for a chart.</p>';
return;
}
function stamp(iso) {
var d = new Date(iso);
return hours > 24
? d.toLocaleDateString([], { weekday: "short", day: "numeric" }) + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false })
: d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
}
function moment(v) {
return new Date(v * 1000).toLocaleString([], { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hour12: false });
}
new Chart(el.getContext("2d"), {
type: "line",
data: { labels: points.map(function (p) { return p[0]; }),
datasets: [{ data: points.map(function (p) { return p[1]; }), borderColor: stewardColor("--blue"),
backgroundColor: "transparent", borderWidth: 1.5, pointRadius: 0, tension: 0.2 }] },
options: {
responsive: true, maintainAspectRatio: false, animation: false,
interaction: { mode: "index", intersect: false },
plugins: { legend: { display: false },
tooltip: { callbacks: {
title: function (items) { return items.length ? new Date(points[items[0].dataIndex][0]).toLocaleString([], { hour12: false }) : ""; },
label: function (item) { return moments ? moment(item.parsed.y) : String(item.parsed.y); } } } },
scales: {
x: { ticks: { maxTicksLimit: 8, maxRotation: 0, font: { size: 10 },
callback: function (v) { return stamp(points[v][0]); } }, grid: { display: false } },
y: { ticks: { font: { size: 10 }, maxTicksLimit: 5,
callback: function (v) { return moments ? moment(v) : v; } } }
}
}
});
})();
</script>
{% endblock %}