[Perl] DAQ system for the FAPG.
perf consolidate status requests
- expose the latest configured statuses through one bulk API query - render all dashboard status components from the shared response - retain and test the existing single-status endpoint
Changed files
roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
@@ -81,6 +81,7 @@
81
81
82
82
$r->get('/api/readings/:probe/series')->to('Reading#series');
83
83
$r->get('/api/readings/:probe')->to('Reading#list');
84
Added:
$r->get('/api/status')->to('Reading#status_list');
84
85
$r->get('/api/status/:probe')->to('Reading#status');
85
86
}
86
87
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Reading.pm
@@ -233,6 +233,49 @@
233
233
);
234
234
}
235
235
236
Added:
sub status_list ($self) {
237
Added:
my @probes = map { $_->{key} } $self->status_items->@*;
238
Added:
my $placeholders = join ', ', ('?') x @probes;
239
Added:
my %statuses = map { $_ => undef } @probes;
240
Added:
241
Added:
my $rows = eval {
242
Added:
$self->sqlite->db->query(
243
Added:
qq{
244
Added:
WITH ranked AS (
245
Added:
SELECT
246
Added:
timestamp,
247
Added:
received_at,
248
Added:
node,
249
Added:
probe,
250
Added:
status,
251
Added:
message,
252
Added:
error,
253
Added:
device,
254
Added:
ROW_NUMBER() OVER (
255
Added:
PARTITION BY probe
256
Added:
ORDER BY COALESCE(timestamp, received_at) DESC, id DESC
257
Added:
) AS status_rank
258
Added:
FROM node_status
259
Added:
WHERE valid = 1
260
Added:
AND probe IN ($placeholders)
261
Added:
)
262
Added:
SELECT timestamp, received_at, node, probe, status, message, error, device
263
Added:
FROM ranked
264
Added:
WHERE status_rank = 1
265
Added:
},
266
Added:
@probes,
267
Added:
)->hashes->to_array;
268
Added:
};
269
Added:
270
Added:
$rows = [] if $@;
271
Added:
for my $row (@$rows) {
272
Added:
mark_unreachable_if_stale($row);
273
Added:
$statuses{ $row->{probe} } = $row;
274
Added:
}
275
Added:
276
Added:
$self->render( json => { statuses => \%statuses, }, );
277
Added:
}
278
Added:
236
279
sub mark_unreachable_if_stale ($row) {
237
280
my $received_at = $row->{received_at} // $row->{timestamp};
238
281
return if !defined $received_at || $received_at ge reachable_since();
roles/dashboard/public/js/dashboard/api.js
@@ -8,9 +8,9 @@
8
8
return response.json();
9
9
}
10
10
11
Removed:
export async function fetchLatestStatus(probe) {
12
Removed:
const payload = await fetchJson(`/api/status/${probe}`);
13
Removed:
return payload.status || null;
11
Added:
export async function fetchLatestStatuses() {
12
Added:
const payload = await fetchJson("/api/status");
13
Added:
return payload.statuses || {};
14
14
}
15
15
16
16
export async function fetchLatestReading(probe) {
roles/dashboard/public/js/dashboard/status.js
@@ -1,4 +1,4 @@
1
Removed:
import { fetchLatestStatus } from "./api.js";
1
Added:
import { fetchLatestStatuses } from "./api.js";
2
2
import { OFFLINE_AFTER_MS } from "./constants.js";
3
3
import { ageMs, formatAge, formatDateTime } from "./format.js";
4
4
@@ -47,17 +47,13 @@
47
47
return [detail, `Status received at ${when}`].filter(Boolean).join(" ");
48
48
}
49
49
50
Removed:
async function loadOverview() {
50
Added:
function loadOverview(statuses) {
51
51
const overview = document.querySelector("[data-overview-status]");
52
52
if (!overview) return;
53
53
54
54
const rows = Array.from(document.querySelectorAll("[data-sensor-status]"));
55
Removed:
const results = await Promise.allSettled(
56
Removed:
rows.map(row => fetchLatestStatus(row.dataset.sensorStatus))
57
Removed:
);
58
Removed:
const items = rows.map((row, index) => {
59
Removed:
const result = results[index];
60
Removed:
const status = result.status === "fulfilled" ? result.value : null;
55
Added:
const items = rows.map(row => {
56
Added:
const status = statuses[row.dataset.sensorStatus] || null;
61
57
62
58
return { status, state: stateForNodeStatus(status) };
63
59
});
@@ -89,7 +85,7 @@
89
85
element.title = text;
90
86
}
91
87
92
Removed:
async function loadHeaderStatus() {
88
Added:
function loadHeaderStatus(statuses) {
93
89
if (!document.querySelector("[data-header-status]")) return;
94
90
95
91
const rows = Array.from(document.querySelectorAll("[data-header-status-item]"));
@@ -98,12 +94,9 @@
98
94
return;
99
95
}
100
96
101
Removed:
const results = await Promise.allSettled(
102
Removed:
rows.map(row => fetchLatestStatus(row.dataset.headerStatusItem))
97
Added:
const warnings = rows.filter(row =>
98
Added:
stateForNodeStatus(statuses[row.dataset.headerStatusItem] || null) !== "healthy"
103
99
);
104
Removed:
const warnings = rows.filter((row, index) =>
105
Removed:
stateForNodeStatus(results[index].status === "fulfilled" ? results[index].value : null) !== "healthy"
106
Removed:
);
107
100
108
101
if (!warnings.length) {
109
102
setHeaderStatus("healthy", "All DAQ status checks are healthy");
@@ -132,20 +125,31 @@
132
125
: "No recent status received";
133
126
}
134
127
135
Removed:
async function loadStatusPage() {
128
Added:
function loadStatusPage(statuses) {
136
129
const rows = Array.from(document.querySelectorAll("[data-status-page-item]"));
137
130
if (!rows.length) return;
138
131
139
Removed:
const results = await Promise.allSettled(
140
Removed:
rows.map(row => fetchLatestStatus(row.dataset.statusPageItem))
141
Removed:
);
142
Removed:
rows.forEach((row, index) => {
143
Removed:
updateStatusPageRow(row, results[index].status === "fulfilled" ? results[index].value : null);
132
Added:
rows.forEach(row => {
133
Added:
updateStatusPageRow(row, statuses[row.dataset.statusPageItem] || null);
144
134
});
145
135
}
146
136
147
137
export function createStatusLoader() {
148
138
return async function loadStatuses() {
149
Removed:
await Promise.all([loadOverview(), loadHeaderStatus(), loadStatusPage()]);
139
Added:
const hasStatusElements = document.querySelector(
140
Added:
"[data-overview-status], [data-header-status], [data-status-page-item]"
141
Added:
);
142
Added:
if (!hasStatusElements) return;
143
Added:
144
Added:
let statuses = {};
145
Added:
try {
146
Added:
statuses = await fetchLatestStatuses();
147
Added:
} catch (_error) {
148
Added:
// Render every configured item as unreachable when the bulk request fails.
149
Added:
}
150
Added:
151
Added:
loadOverview(statuses);
152
Added:
loadHeaderStatus(statuses);
153
Added:
loadStatusPage(statuses);
150
154
};
151
155
}
roles/dashboard/t/03-status-api.t
@@ -11,6 +11,14 @@
11
11
12
12
my $t = test_app();
13
13
14
Added:
$t->get_ok('/api/status')
15
Added:
->status_is(200)
16
Added:
->json_is( '/statuses/ph/status' => 'ok' )
17
Added:
->json_is( '/statuses/hub/node' => 'fapg-daq-five-01' )
18
Added:
->json_is( '/statuses/do/status' => 'unreachable' )
19
Added:
->json_is( '/statuses/orp' => undef )
20
Added:
->json_is( '/statuses/ec' => undef );
21
Added:
14
22
$t->get_ok('/api/status/ph')
15
23
->status_is(200)
16
24
->json_is( '/probe' => 'ph' )