refactor consolidate frontend data helpers

Centralize JSON transport and endpoint-specific weather and insights requests. Share timeframe conversion and tooltip defaults across chart modules. Make backend unreachable status authoritative through a pure state mapper.

Commit
c6ec804ab9ab105f58f70250ba811af49c86d992
Author
gpt-5.6-sol high <agent@openai.com>
Author date
Committer
gpt-5.6-sol high <agent@openai.com>
Committer date
Changed files
roles/dashboard/public/js/dashboard/api.js
index 435636c7..c162bb23 100644..100644
@@ -1,4 +1,4 @@
1 Removed: async function fetchJson(url, options = {}) {
1 Added: export async function fetchJson(url, options = {}) {
2 2 const response = await fetch(url, options);
3 3
4 4 if (!response.ok) {
@@ -47,4 +47,33 @@
47 47 const options = signal ? { signal } : {};
48 48 const payload = await fetchJson(`/api/correlation/${probeX}/${probeY}?${params}`, options);
49 49 return payload.points || [];
50 Added: }
51 Added:
52 Added: export async function fetchWeatherHourly(hours, signal = null) {
53 Added: const params = new URLSearchParams({ hours: String(hours) });
54 Added: const options = signal ? { signal } : {};
55 Added: return fetchJson(`/api/weather/hourly?${params}`, options);
56 Added: }
57 Added:
58 Added: export async function fetchWeatherForecast(signal = null) {
59 Added: const options = signal ? { signal } : {};
60 Added: return fetchJson("/api/weather/forecast", options);
61 Added: }
62 Added:
63 Added: export async function fetchInsightStability(hours, signal = null) {
64 Added: const params = new URLSearchParams({ hours: String(hours) });
65 Added: const options = signal ? { signal } : {};
66 Added: return fetchJson(`/api/insights/stability?${params}`, options);
67 Added: }
68 Added:
69 Added: export async function fetchInsightDerivatives(hours, signal = null) {
70 Added: const params = new URLSearchParams({ hours: String(hours) });
71 Added: const options = signal ? { signal } : {};
72 Added: return fetchJson(`/api/insights/derivatives?${params}`, options);
73 Added: }
74 Added:
75 Added: export async function fetchInsightDiurnal(probe, days, signal = null) {
76 Added: const params = new URLSearchParams({ days: String(days) });
77 Added: const options = signal ? { signal } : {};
78 Added: return fetchJson(`/api/insights/diurnal/${probe}?${params}`, options);
50 79 }
roles/dashboard/public/js/dashboard/chart-options.js
index 00000000..0f84ced2 000000..100644
@@ -0,0 +1,14 @@
1 Added: export const TOOLTIP_DEFAULTS = {
2 Added: mode: "index",
3 Added: intersect: false,
4 Added: backgroundColor: "rgba(30, 41, 59, 0.7)",
5 Added: titleFont: { size: 11, weight: "normal" },
6 Added: bodyFont: { size: 12 },
7 Added: padding: { x: 8, y: 5 },
8 Added: cornerRadius: 4,
9 Added: caretSize: 0
10 Added: };
11 Added:
12 Added: export function tooltipOptions(callbacks = {}) {
13 Added: return { ...TOOLTIP_DEFAULTS, callbacks };
14 Added: }
roles/dashboard/public/js/dashboard/constants.js
index 2969aec9..e8fae434 100644..100644
@@ -1,4 +1,3 @@
1 Removed: export const OFFLINE_AFTER_MS = 5 * 60 * 1000;
2 1 export const DASHBOARD_REFRESH_MS = 30 * 1000;
3 2 export const DEFAULT_TIMEFRAME = "day";
4 3 export const TIMEFRAMES = {
roles/dashboard/public/js/dashboard/insights.js
index 5ee88af0..0a84d387 100644..100644
@@ -1,7 +1,17 @@
1 Removed: import { fetchProbeData } from "./api.js";
2 Removed: import { READING_RANGES, TIMEFRAMES } from "./constants.js";
1 Added: import {
2 Added: fetchInsightDerivatives,
3 Added: fetchInsightDiurnal,
4 Added: fetchInsightStability,
5 Added: fetchProbeData
6 Added: } from "./api.js";
7 Added: import { tooltipOptions } from "./chart-options.js";
8 Added: import { READING_RANGES } from "./constants.js";
3 9 import { formatChartTime, formatDateTime } from "./format.js";
4 Removed: import { createTimeframeState, timeframeRangeLabel } from "./timeframe.js";
10 Added: import {
11 Added: createTimeframeState,
12 Added: timeframeRangeLabel,
13 Added: timeframeToHours
14 Added: } from "./timeframe.js";
5 15 import { initialiseGraphControls } from "./graph-controls.js";
6 16
7 17 const PROBE_COLORS = {
@@ -21,17 +31,6 @@
21 31 "rgba(236, 72, 153, 0.25)"
22 32 ];
23 33
24 Removed: const TOOLTIP_DEFAULTS = {
25 Removed: mode: "index",
26 Removed: intersect: false,
27 Removed: backgroundColor: "rgba(30, 41, 59, 0.7)",
28 Removed: titleFont: { size: 11, weight: "normal" },
29 Removed: bodyFont: { size: 12 },
30 Removed: padding: { x: 8, y: 5 },
31 Removed: cornerRadius: 4,
32 Removed: caretSize: 0
33 Removed: };
34 Removed:
35 34 // Optimal ranges used to normalise derivatives to % per hour
36 35 const OPTIMAL_SPANS = {
37 36 ph: READING_RANGES.ph.goodMax - READING_RANGES.ph.goodMin,
@@ -44,20 +43,6 @@
44 43
45 44 let charts = {};
46 45
47 Removed: async function fetchJson(url, signal) {
48 Removed: const response = await fetch(url, signal ? { signal } : {});
49 Removed: if (!response.ok) throw new Error(`HTTP ${response.status}`);
50 Removed: return response.json();
51 Removed: }
52 Removed:
53 Removed: /** Convert a timeframe selection to an API hours parameter */
54 Removed: function timeframeToHours(timeframe) {
55 Removed: const tf = timeframe.selected();
56 Removed: const range = timeframe.range();
57 Removed: const ms = TIMEFRAMES[tf]?.ms || TIMEFRAMES.day.ms;
58 Removed: return Math.round((ms * range) / (60 * 60 * 1000));
59 Removed: }
60 Removed:
61 46 // ══════════════════════════════════════════════════════════════════
62 47 // AT A GLANCE — Uses the page-level timeframe
63 48 // ══════════════════════════════════════════════════════════════════
@@ -67,7 +52,7 @@
67 52 if (!canvas) return;
68 53
69 54 const hours = timeframeToHours(timeframe);
70 Removed: const data = await fetchJson(`/api/insights/stability?hours=${hours}`, signal);
55 Added: const data = await fetchInsightStability(hours, signal);
71 56 const labels = data.points.map(p => formatDateTime(p.timestamp));
72 57 const values = data.points.map(p => p.score);
73 58
@@ -97,7 +82,7 @@
97 82 },
98 83 plugins: {
99 84 legend: { display: false },
100 Removed: tooltip: { ...TOOLTIP_DEFAULTS, callbacks: { title: () => "" } }
85 Added: tooltip: tooltipOptions({ title: () => "" })
101 86 }
102 87 }
103 88 });
@@ -113,7 +98,7 @@
113 98 if (!canvas) return;
114 99
115 100 const hours = timeframeToHours(timeframe);
116 Removed: const data = await fetchJson(`/api/insights/derivatives?hours=${hours}`, signal);
101 Added: const data = await fetchInsightDerivatives(hours, signal);
117 102
118 103 // Only use the probes we want (no EC)
119 104 const refProbe = DERIVATIVE_PROBES.find(p => data.probes[p]?.length > 0);
@@ -181,7 +166,7 @@
181 166 const hours = timeframeToHours(timeframe);
182 167 const days = Math.max(1, Math.round(hours / 24));
183 168
184 Removed: const data = await fetchJson(`/api/insights/diurnal/do?days=${days}`, signal);
169 Added: const data = await fetchInsightDiurnal("do", days, signal);
185 170 const labels = data.hours.map(h => `${String(h).padStart(2, "0")}:00`);
186 171
187 172 const datasets = data.traces.map((trace, i) => ({
@@ -220,7 +205,7 @@
220 205 },
221 206 plugins: {
222 207 legend: { display: true, labels: { font: { size: 10 } } },
223 Removed: tooltip: { ...TOOLTIP_DEFAULTS, callbacks: { title: () => "" } }
208 Added: tooltip: tooltipOptions({ title: () => "" })
224 209 }
225 210 }
226 211 });
@@ -342,7 +327,7 @@
342 327 },
343 328 plugins: {
344 329 legend: { display: true },
345 Removed: tooltip: { ...TOOLTIP_DEFAULTS, callbacks: { title: () => "" } }
330 Added: tooltip: tooltipOptions({ title: () => "" })
346 331 }
347 332 }
348 333 });
@@ -421,7 +406,7 @@
421 406 },
422 407 plugins: {
423 408 legend: { display: true },
424 Removed: tooltip: { ...TOOLTIP_DEFAULTS, callbacks: { title: () => "" } }
409 Added: tooltip: tooltipOptions({ title: () => "" })
425 410 }
426 411 }
427 412 });
roles/dashboard/public/js/dashboard/status-state.js
index 00000000..54849398 000000..100644
@@ -0,0 +1,10 @@
1 Added: export function stateForNodeStatus(nodeStatus) {
2 Added: if (!nodeStatus || nodeStatus.status === "unreachable") return "unreachable";
3 Added:
4 Added: const timestamp = nodeStatus.received_at || nodeStatus.timestamp;
5 Added: if (!Number.isFinite(new Date(timestamp).getTime())) return "unreachable";
6 Added: if (nodeStatus.status === "ok") return "healthy";
7 Added: if (nodeStatus.status === "probe_error") return "probe-error";
8 Added:
9 Added: return "unknown";
10 Added: }
roles/dashboard/public/js/dashboard/status.js
index 092c9cfd..d14aa77c 100644..100644
@@ -1,6 +1,6 @@
1 1 import { fetchLatestStatuses } from "./api.js";
2 Removed: import { OFFLINE_AFTER_MS } from "./constants.js";
3 2 import { ageMs, formatAge, formatDateTime } from "./format.js";
3 Added: import { stateForNodeStatus } from "./status-state.js";
4 4
5 5 function statusLabel(state) {
6 6 return {
@@ -12,18 +12,6 @@
12 12 unreachable: "Unreachable",
13 13 unknown: "Unknown"
14 14 }[state] || "Unknown";
15 Removed: }
16 Removed:
17 Removed: function stateForNodeStatus(nodeStatus) {
18 Removed: if (!nodeStatus) return "unreachable";
19 Removed:
20 Removed: const timestamp = nodeStatus.received_at || nodeStatus.timestamp;
21 Removed: const age = ageMs(timestamp);
22 Removed: if (age === null || age > OFFLINE_AFTER_MS) return "unreachable";
23 Removed: if (nodeStatus.status === "ok") return "healthy";
24 Removed: if (nodeStatus.status === "probe_error") return "probe-error";
25 Removed:
26 Removed: return "unknown";
27 15 }
28 16
29 17 function setHeaderStatus(state, text) {
roles/dashboard/public/js/dashboard/timeframe.js
index 7de91b8d..e9025b24 100644..100644
@@ -39,3 +39,10 @@
39 39 const unit = { hour: "hour", day: "day", week: "week", month: "month", year: "year" }[timeframe] || "year";
40 40 return `${range} ${unit}${range === 1 ? "" : "s"}`;
41 41 }
42 Added:
43 Added: export function timeframeToHours(timeframe) {
44 Added: const selected = timeframe.selected();
45 Added: const range = timeframe.range();
46 Added: const milliseconds = TIMEFRAMES[selected]?.ms || TIMEFRAMES[DEFAULT_TIMEFRAME].ms;
47 Added: return Math.round((milliseconds * range) / (60 * 60 * 1000));
48 Added: }
roles/dashboard/public/js/dashboard/weather-forecast.js
index 0f6e256c..58829a97 100644..100644
@@ -1,13 +1,8 @@
1 Added: import { fetchWeatherForecast } from "./api.js";
1 2 import { formatChartTime } from "./format.js";
2 3
3 4 let chart = null;
4 5
5 Removed: async function fetchJson(url) {
6 Removed: const response = await fetch(url);
7 Removed: if (!response.ok) throw new Error(`HTTP ${response.status}`);
8 Removed: return response.json();
9 Removed: }
10 Removed:
11 6 function updateCurrentConditions(points) {
12 7 const summaryEl = document.querySelector("[data-weather-summary]");
13 8 if (!summaryEl) return;
@@ -169,7 +164,7 @@
169 164
170 165 let data;
171 166 try {
172 Removed: data = await fetchJson("/api/weather/forecast");
167 Added: data = await fetchWeatherForecast();
173 168 } catch {
174 169 return;
175 170 }