[Perl] DAQ system for the FAPG.
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.
Changed files
- roles/dashboard/public/js/dashboard/api.js
- roles/dashboard/public/js/dashboard/chart-options.js
- roles/dashboard/public/js/dashboard/constants.js
- roles/dashboard/public/js/dashboard/insights.js
- roles/dashboard/public/js/dashboard/status-state.js
- roles/dashboard/public/js/dashboard/status.js
- roles/dashboard/public/js/dashboard/timeframe.js
- roles/dashboard/public/js/dashboard/weather-forecast.js
roles/dashboard/public/js/dashboard/api.js
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
}