refactor adopt additional Bootstrap component patterns

- Probe card reading status uses placeholder-glow skeleton loading state until data arrives, then removes the placeholder classes - Download buttons wrapped in d-flex gap-2 for proper spacing - Custom date fields use Bootstrap Collapse component with animated show/hide instead of the hidden attribute - Footer uses bg-body-tertiary and border-top (standard Bootstrap) instead of custom .site-footer class - Probe card dropdown button gets dropdown-toggle class for the standard caret indicator - Range control replaced from btn-group with disabled button to input-group with input-group-text label between the +/- buttons

Commit
75f8f325651f68e41161ec9623d3795a9ae59886
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 65b9e38f..43126479 100644..100644
@@ -285,6 +285,7 @@
285 285 }
286 286
287 287 if (status) {
288 Added: status.classList.remove("placeholder-glow");
288 289 status.textContent = latest
289 290 ? `Latest: ${latest.value} ${latest.unit || ""} at ${formatDateTime(latest.timestamp)} (${formatAge(ageMs(latest.timestamp))})`
290 291 : "No readings found for this probe yet.";
@@ -294,7 +295,10 @@
294 295 } catch (error) {
295 296 if (error.name === "AbortError") return;
296 297
297 Removed: if (status) status.textContent = `Could not load ${probe} readings: ${error.message}`;
298 Added: if (status) {
299 Added: status.classList.remove("placeholder-glow");
300 Added: status.textContent = `Could not load ${probe} readings: ${error.message}`;
301 Added: }
298 302 renderMessageTable(probe, []);
299 303 }
300 304 }
roles/dashboard/public/js/downloads.js
index c1ad5bf7..54c094cb 100644..100644
@@ -1,5 +1,6 @@
1 1 const downloadRangeButtons = Array.from(document.querySelectorAll("[data-download-range]"));
2 2 const customDateFields = document.querySelector("[data-custom-date-fields]");
3 Added: const customDateCollapse = customDateFields ? new bootstrap.Collapse(customDateFields, { toggle: false }) : null;
3 4 const downloadFrom = document.querySelector("#download-from");
4 5 const downloadTo = document.querySelector("#download-to");
5 6 const errorBanner = document.querySelector("[data-downloads-error]");
@@ -10,8 +11,8 @@
10 11 option.addEventListener("change", () => {
11 12 const custom = option.value === "custom";
12 13
13 Removed: if (customDateFields) {
14 Removed: customDateFields.hidden = !custom;
14 Added: if (customDateCollapse) {
15 Added: custom ? customDateCollapse.show() : customDateCollapse.hide();
15 16 }
16 17
17 18 if (custom) {
roles/dashboard/t/06-downloads.t
index 425c1a70..076611d9 100644..100644
@@ -23,9 +23,9 @@
23 23 ->element_exists('[data-downloads-error]')
24 24 ->element_exists('input[name="date_range"][value="week"][checked]')
25 25 ->element_exists(
26 Removed: '[data-custom-date-fields][hidden] input[name="from"][type="date"]')
26 Added: '[data-custom-date-fields].collapse:not(.show) input[name="from"][type="date"]')
27 27 ->element_exists(
28 Removed: '[data-custom-date-fields][hidden] input[name="to"][type="date"]')
28 Added: '[data-custom-date-fields].collapse:not(.show) input[name="to"][type="date"]')
29 29 ->text_like( 'button.downloads-button[type="submit"]', qr/Download CSV/ )
30 30 ->element_exists(
31 31 'button.downloads-button[type="submit"][data-download-button] [data-download-spinner].d-none')
roles/dashboard/templates/dashboard/downloads.html.ep
index 5042127b..a7bf97c8 100644..100644
@@ -45,19 +45,21 @@
45 45 <label class="btn btn-outline-secondary" for="range-custom">Custom</label>
46 46 </div>
47 47
48 Removed: <div class="d-flex flex-wrap gap-3 mt-3" data-custom-date-fields hidden>
49 Removed: <div>
50 Removed: <label class="form-label fw-bold" for="download-from">From</label>
51 Removed: <input class="form-control" id="download-from" name="from" type="date" value="<%= $from %>" required>
48 Added: <div class="collapse mt-3" id="custom-date-fields" data-custom-date-fields>
49 Added: <div class="d-flex flex-wrap gap-3">
50 Added: <div>
51 Added: <label class="form-label fw-bold" for="download-from">From</label>
52 Added: <input class="form-control" id="download-from" name="from" type="date" value="<%= $from %>" required>
53 Added: </div>
54 Added: <div>
55 Added: <label class="form-label fw-bold" for="download-to">To</label>
56 Added: <input class="form-control" id="download-to" name="to" type="date" value="<%= $to %>" required>
57 Added: </div>
52 58 </div>
53 Removed: <div>
54 Removed: <label class="form-label fw-bold" for="download-to">To</label>
55 Removed: <input class="form-control" id="download-to" name="to" type="date" value="<%= $to %>" required>
56 Removed: </div>
57 59 </div>
58 60 </fieldset>
59 61
60 Removed: <div>
62 Added: <div class="d-flex gap-2">
61 63 <button class="btn btn-success downloads-button" type="submit" data-download-button>
62 64 <span class="spinner-border spinner-border-sm d-none" aria-hidden="true" data-download-spinner></span>
63 65 Download CSV
roles/dashboard/templates/dashboard/graph-control.html.ep
index f3493aa4..99df4b76 100644..100644
@@ -2,20 +2,18 @@
2 2
3 3 <div class="graph-control-scroll overflow-x-auto">
4 4 <div class="graph-control d-inline-flex gap-2 flex-nowrap" aria-label="<%= $aria_label %>">
5 Removed: <div class="btn-group range-control" role="group" aria-label="Adjust range">
5 Added: <div class="input-group input-group-sm range-control" aria-label="Adjust range">
6 6 <button
7 Removed: class="btn btn-sm btn-outline-secondary"
7 Added: class="btn btn-outline-secondary"
8 8 type="button"
9 9 data-range-adjustment="decrease"
10 10 aria-label="Decrease range">−</button>
11 Removed: <button
12 Removed: class="btn btn-sm btn-outline-secondary range-label"
13 Removed: type="button"
11 Added: <span
12 Added: class="input-group-text range-label"
14 13 data-range-label
15 Removed: disabled
16 Removed: aria-live="polite">1</button>
14 Added: aria-live="polite">1</span>
17 15 <button
18 Removed: class="btn btn-sm btn-outline-secondary"
16 Added: class="btn btn-outline-secondary"
19 17 type="button"
20 18 data-range-adjustment="increase"
21 19 aria-label="Increase range">+</button>
roles/dashboard/templates/dashboard/probe-card.html.ep
index 0bf0d6f1..d04295b7 100644..100644
@@ -13,7 +13,7 @@
13 13 </div>
14 14 <div class="dropdown flex-shrink-0">
15 15 <button
16 Removed: class="btn btn-sm btn-outline-secondary"
16 Added: class="btn btn-sm btn-outline-secondary dropdown-toggle"
17 17 type="button"
18 18 data-bs-toggle="dropdown"
19 19 aria-expanded="false"
@@ -39,8 +39,8 @@
39 39 <canvas id="chart-<%= $probe->{key} %>"></canvas>
40 40 </div>
41 41 % unless (stash('single_probe')) {
42 Removed: <p class="text-muted small mt-2 mb-0 reading-status" data-status="<%= $probe->{key} %>">
43 Removed: Waiting for data…
42 Added: <p class="text-muted small mt-2 mb-0 reading-status placeholder-glow" data-status="<%= $probe->{key} %>">
43 Added: <span class="placeholder col-8">Waiting for data…</span>
44 44 </p>
45 45 % }
46 46 </section>
roles/dashboard/templates/layouts/default.html.ep
index dc513a59..50089dd0 100644..100644
@@ -78,7 +78,7 @@
78 78 <%= content %>
79 79 </main>
80 80
81 Removed: <footer class="site-footer text-center text-muted small py-3 mt-4">
81 Added: <footer class="text-center text-muted small py-3 mt-4 bg-body-tertiary border-top">
82 82 <p class="mb-0">
83 83 <a class="text-muted" href="https://www.lafermeaquaponique.com/">lafermeaquaponique.com</a>
84 84 </p>