feat show percentage variation in chart title

Display the percentage change between the first and last data points of the selected timeframe period, inline with the probe label. Positive changes show green with +, negative show red with −, and zero variation shows muted text. Recomputed on every chart refresh.

Commit
162d8f7205f4fdba33c69fc7b8ab1d623b5f2f58
Author
GPT-5 medium <codex@openai.com>
Author date
Committer
GPT-5 medium <codex@openai.com>
Committer date
Changed files
roles/dashboard/public/js/dashboard/charts.js
index 8904ffa9..65b9e38f 100644..100644
@@ -113,6 +113,32 @@
113 113 }));
114 114 }
115 115
116 Added: function renderVariation(probe, trend) {
117 Added: const element = document.querySelector(`[data-variation="${probe}"]`);
118 Added: if (!element) return;
119 Added:
120 Added: const values = trend.filter(Number.isFinite);
121 Added: if (values.length < 2) {
122 Added: element.textContent = "";
123 Added: return;
124 Added: }
125 Added:
126 Added: const first = values[0];
127 Added: const last = values[values.length - 1];
128 Added:
129 Added: if (first === 0) {
130 Added: element.textContent = "";
131 Added: return;
132 Added: }
133 Added:
134 Added: const percent = ((last - first) / Math.abs(first)) * 100;
135 Added: const sign = percent >= 0 ? "+" : "";
136 Added: const color = percent > 0 ? "text-success" : percent < 0 ? "text-danger" : "text-muted";
137 Added:
138 Added: element.className = `small fw-normal ${color}`;
139 Added: element.textContent = `${sign}${percent.toFixed(1)}%`;
140 Added: }
141 Added:
116 142 function registerTopLeftTooltipPositioner() {
117 143 if (Chart.Tooltip.positioners.dashboardTopLeft) return;
118 144
@@ -263,6 +289,7 @@
263 289 ? `Latest: ${latest.value} ${latest.unit || ""} at ${formatDateTime(latest.timestamp)} (${formatAge(ageMs(latest.timestamp))})`
264 290 : "No readings found for this probe yet.";
265 291 }
292 Added: renderVariation(probe, series.trend);
266 293 renderMessageTable(probe, data.readings);
267 294 } catch (error) {
268 295 if (error.name === "AbortError") return;
roles/dashboard/templates/dashboard/probe-card.html.ep
index 781ca458..2161c417 100644..100644
@@ -5,6 +5,7 @@
5 5 <div class="probe-card-title min-w-0">
6 6 <h1 class="h5 mb-0">
7 7 <a class="text-body text-decoration-none" href="/probes/<%= $probe->{key} %>"><%= $probe->{label} %></a>
8 Added: <span class="small fw-normal text-muted" data-variation="<%= $probe->{key} %>"></span>
8 9 </h1>
9 10 <p class="text-muted small mb-0 mt-1">
10 11 Unit: <strong><%= $probe->{unit} %></strong>