[Perl] DAQ system for the FAPG.
Add combined probes dashboard page
Changed files
- roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
- roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Dashboard.pm
- roles/dashboard/public/css/app.css
- roles/dashboard/public/js/dashboard.js
- roles/dashboard/t/00-homepage.t
- roles/dashboard/t/01-graph-page.t
- roles/dashboard/templates/dashboard/probes.html.ep
- roles/dashboard/templates/layouts/default.html.ep
roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
@@ -73,6 +73,7 @@
73
73
74
74
$r->get('/')->to('dashboard#index');
75
75
$r->get('/status')->to('dashboard#status');
76
Added:
$r->get('/probes')->to('dashboard#probe_list');
76
77
$r->get('/graphs/:probe')->to('dashboard#graph');
77
78
78
79
$r->get('/api/readings/:probe/series')->to('Reading#series');
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Dashboard.pm
@@ -17,6 +17,13 @@
17
17
);
18
18
}
19
19
20
Added:
sub probe_list ($self) {
21
Added:
$self->render(
22
Added:
template => 'dashboard/probes',
23
Added:
probes => $self->probes,
24
Added:
);
25
Added:
}
26
Added:
20
27
sub graph ($self) {
21
28
my $probe_key = $self->stash('probe');
22
29
my ($probe) = grep { $_->{key} eq $probe_key } $self->probes->@*;
roles/dashboard/public/css/app.css
@@ -76,7 +76,7 @@
76
76
text-decoration: underline;
77
77
}
78
78
79
Removed:
.probe-nav {
79
Added:
.nav-links {
80
80
display: flex;
81
81
flex-wrap: wrap;
82
82
justify-content: flex-end;
@@ -84,7 +84,7 @@
84
84
margin-left: auto;
85
85
}
86
86
87
Removed:
.probe-nav a {
87
Added:
.nav-links a {
88
88
border-radius: 999px;
89
89
padding: 0.5rem 0.75rem;
90
90
color: var(--text);
@@ -93,7 +93,7 @@
93
93
text-decoration: none;
94
94
}
95
95
96
Removed:
.probe-nav a:hover {
96
Added:
.nav-links a:hover {
97
97
background: var(--accent-soft);
98
98
color: var(--accent);
99
99
}
@@ -661,7 +661,7 @@
661
661
background: var(--text);
662
662
}
663
663
664
Removed:
.probe-nav {
664
Added:
.nav-links {
665
665
position: absolute;
666
666
top: calc(100% + 0.35rem);
667
667
right: 0;
@@ -674,14 +674,14 @@
674
674
box-shadow: 0 10px 30px rgba(23, 33, 27, 0.12);
675
675
}
676
676
677
Removed:
.probe-nav a {
677
Added:
.nav-links a {
678
678
display: block;
679
679
width: 100%;
680
680
border-radius: 0.4rem;
681
681
padding: 0.75rem;
682
682
}
683
683
684
Removed:
.nav-toggle:checked ~ .probe-nav {
684
Added:
.nav-toggle:checked ~ .nav-links {
685
685
display: block;
686
686
}
687
687
roles/dashboard/public/js/dashboard.js
@@ -86,6 +86,11 @@
86
86
return activePanel ? activePanel.dataset.panel : null;
87
87
}
88
88
89
Added:
function displayedProbes() {
90
Added:
return Array.from(document.querySelectorAll(".tab-panel[data-panel]"))
91
Added:
.map(panel => panel.dataset.panel);
92
Added:
}
93
Added:
89
94
function statusElement(probe) {
90
95
return document.querySelector(`[data-status="${probe}"]`);
91
96
}
@@ -753,7 +758,7 @@
753
758
button.classList.toggle("is-active", button.dataset.timeframe === activeTimeframe);
754
759
});
755
760
756
Removed:
loadProbe(activeProbe());
761
Added:
displayedProbes().forEach(loadProbe);
757
762
}
758
763
759
764
function setSmoothing(enabled) {
@@ -763,7 +768,7 @@
763
768
toggle.checked = smoothingEnabled;
764
769
});
765
770
766
Removed:
loadProbe(activeProbe());
771
Added:
displayedProbes().forEach(loadProbe);
767
772
}
768
773
769
774
function probeFromHash() {
@@ -816,7 +821,7 @@
816
821
activateTab(initialProbe);
817
822
document.getElementById(`probe-${initialProbe}`).scrollIntoView();
818
823
} else if (activeProbe()) {
819
Removed:
loadProbe(activeProbe());
824
Added:
displayedProbes().forEach(loadProbe);
820
825
}
821
826
822
827
loadOverview();
@@ -825,11 +830,7 @@
825
830
loadStatusPage();
826
831
827
832
setInterval(() => {
828
Removed:
const probe = activeProbe();
829
Removed:
830
Removed:
if (probe) {
831
Removed:
loadProbe(probe);
832
Removed:
}
833
Added:
displayedProbes().forEach(loadProbe);
833
834
834
835
loadOverview();
835
836
loadQuickStatus();
roles/dashboard/t/00-homepage.t
@@ -25,14 +25,8 @@
25
25
->element_exists('[data-header-status-item="do"]')
26
26
->element_exists('[data-header-status-item="orp"]')
27
27
->element_exists('[data-header-status-item="ec"]')
28
Removed:
->element_exists(
29
Removed:
'nav.probe-nav a[href="/graphs/ph"][data-nav-probe="ph"]')
30
Removed:
->element_exists(
31
Removed:
'nav.probe-nav a[href="/graphs/do"][data-nav-probe="do"]')
32
Removed:
->element_exists(
33
Removed:
'nav.probe-nav a[href="/graphs/orp"][data-nav-probe="orp"]')
34
Removed:
->element_exists(
35
Removed:
'nav.probe-nav a[href="/graphs/ec"][data-nav-probe="ec"]')
28
Added:
->text_is( 'div.nav-links a[href="/probes"]', 'Probes' )
29
Added:
->element_exists_not('div.nav-links a[href^="/graphs/"]')
36
30
->element_exists(
37
31
'footer.site-footer a[href="https://www.lafermeaquaponique.com/"]')
38
32
->content_like(qr/Copyright © 2026 Marius Peter/)
roles/dashboard/t/01-graph-page.t
@@ -21,15 +21,9 @@
21
21
->element_exists('[data-header-status-item="do"]')
22
22
->element_exists('[data-header-status-item="orp"]')
23
23
->element_exists('[data-header-status-item="ec"]')
24
Added:
->text_is( 'div.nav-links a[href="/probes"]', 'Probes' )
25
Added:
->element_exists_not('div.nav-links a[href^="/graphs/"]')
24
26
->element_exists(
25
Removed:
'nav.probe-nav a[href="/graphs/ph"][data-nav-probe="ph"]')
26
Removed:
->element_exists(
27
Removed:
'nav.probe-nav a[href="/graphs/do"][data-nav-probe="do"]')
28
Removed:
->element_exists(
29
Removed:
'nav.probe-nav a[href="/graphs/orp"][data-nav-probe="orp"]')
30
Removed:
->element_exists(
31
Removed:
'nav.probe-nav a[href="/graphs/ec"][data-nav-probe="ec"]')
32
Removed:
->element_exists(
33
27
'footer.site-footer a[href="https://www.lafermeaquaponique.com/"]')
34
28
->element_exists('section#probe-ph[data-panel="ph"]')
35
29
->element_exists('canvas#chart-ph')
@@ -43,5 +37,17 @@
43
37
->content_like(qr/Latest pH Reading Messages/);
44
38
45
39
$t->get_ok('/graphs/unknown')->status_is(404);
40
Added:
41
Added:
$t->get_ok('/probes')
42
Added:
->status_is(200)
43
Added:
->text_is('h1', 'pH')
44
Added:
->element_exists('section#probe-ph[data-panel="ph"]')
45
Added:
->element_exists('section#probe-do[data-panel="do"]')
46
Added:
->element_exists('section#probe-orp[data-panel="orp"]')
47
Added:
->element_exists('section#probe-ec[data-panel="ec"]')
48
Added:
->element_exists('canvas#chart-ph')
49
Added:
->element_exists('canvas#chart-do')
50
Added:
->element_exists('canvas#chart-orp')
51
Added:
->element_exists('canvas#chart-ec');
46
52
47
53
done_testing;
roles/dashboard/templates/dashboard/probes.html.ep
@@ -0,0 +1,67 @@
1
Added:
<!-- -*- mode: web; -*- -->
2
Added:
3
Added:
% layout 'default';
4
Added:
% title 'FAPG DAQ Probes';
5
Added:
6
Added:
% for my $probe ($probes->@*) {
7
Added:
<section class="dashboard-card">
8
Added:
<section
9
Added:
id="probe-<%= $probe->{key} %>"
10
Added:
class="tab-panel is-active"
11
Added:
data-panel="<%= $probe->{key} %>">
12
Added:
13
Added:
<div class="panel-heading">
14
Added:
<h1><%= $probe->{label} %></h1>
15
Added:
<div class="panel-tools">
16
Added:
<p>
17
Added:
Unit:
18
Added:
<strong><%= $probe->{unit} %></strong>
19
Added:
</p>
20
Added:
<div class="timeframe-control" role="group" aria-label="<%= $probe->{label} %> chart timeframe">
21
Added:
<button class="timeframe-button" type="button" data-timeframe="hour">Hourly</button>
22
Added:
<button class="timeframe-button is-active" type="button" data-timeframe="day">Daily</button>
23
Added:
<button class="timeframe-button" type="button" data-timeframe="week">Weekly</button>
24
Added:
<button class="timeframe-button" type="button" data-timeframe="year">Yearly</button>
25
Added:
</div>
26
Added:
<label class="smoothing-control">
27
Added:
<input type="checkbox" data-smoothing-toggle checked>
28
Added:
<span>Smoothed</span>
29
Added:
</label>
30
Added:
</div>
31
Added:
</div>
32
Added:
33
Added:
<div class="chart-wrap">
34
Added:
<canvas id="chart-<%= $probe->{key} %>"></canvas>
35
Added:
</div>
36
Added:
37
Added:
<p class="reading-status" data-status="<%= $probe->{key} %>">
38
Added:
Waiting for data…
39
Added:
</p>
40
Added:
41
Added:
<section class="message-log" aria-label="Last ten reading messages for <%= $probe->{label} %>">
42
Added:
<h2>Latest <%= $probe->{label} %> Reading Messages</h2>
43
Added:
<div class="message-table-wrap">
44
Added:
<table class="message-table">
45
Added:
<thead>
46
Added:
<tr>
47
Added:
<th scope="col">Received</th>
48
Added:
<th scope="col">Age</th>
49
Added:
<th scope="col">Node</th>
50
Added:
<th scope="col">Value</th>
51
Added:
<th scope="col">Unit</th>
52
Added:
</tr>
53
Added:
</thead>
54
Added:
<tbody data-reading-messages="<%= $probe->{key} %>">
55
Added:
<tr>
56
Added:
<td colspan="5">Waiting for messages</td>
57
Added:
</tr>
58
Added:
</tbody>
59
Added:
</table>
60
Added:
</div>
61
Added:
</section>
62
Added:
</section>
63
Added:
</section>
64
Added:
% }
65
Added:
66
Added:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
67
Added:
<script src="/js/dashboard.js"></script>
roles/dashboard/templates/layouts/default.html.ep
@@ -14,17 +14,15 @@
14
14
<body>
15
15
<header class="page-header" aria-label="Dashboard navigation">
16
16
<a class="page-brand" href="/">FAPG</a>
17
Removed:
<input class="nav-toggle" id="probe-nav-toggle" type="checkbox" aria-label="Toggle graph navigation">
18
Removed:
<label class="nav-toggle-button" for="probe-nav-toggle" aria-hidden="true">
17
Added:
<input class="nav-toggle" id="nav-links-toggle" type="checkbox" aria-label="Toggle navigation">
18
Added:
<label class="nav-toggle-button" for="nav-links-toggle" aria-hidden="true">
19
19
<span></span>
20
20
<span></span>
21
21
<span></span>
22
22
</label>
23
Removed:
<nav class="probe-nav" aria-label="Probe graphs">
24
Removed:
% for my $probe (probes()->@*) {
25
Removed:
<a href="/graphs/<%= $probe->{key} %>" data-nav-probe="<%= $probe->{key} %>"><%= $probe->{label} %></a>
26
Removed:
% }
27
Removed:
</nav>
23
Added:
<div class="nav-links">
24
Added:
<a href="/probes">Probes</a>
25
Added:
</div>
28
26
<a
29
27
class="header-status-link is-unknown"
30
28
href="/status"