[Perl] DAQ system for the FAPG.
refactor rationalise chart card headers
Create a shared chart-card-header.html.ep partial used by insights, weather, and probe cards. All graph cards now have: - A dropdown menu (download data, share placeholder) - An optional ⓘ info toggle for explainer text - Consistent layout across all pages Probe cards keep their custom header (title + variation) but share the same button group pattern.
Changed files
roles/dashboard/t/01-graph-page.t
@@ -24,7 +24,7 @@
24
24
->element_exists(
25
25
'div.toolbar-items div.graph-control .timeframe-control[role="group"]'
26
26
)
27
Removed:
->text_is( 'a[data-raw-download][data-probe="ph"]', 'Download raw data' )
27
Added:
->text_is( 'a[data-raw-download][data-probe="ph"]', 'Download data' )
28
28
->element_exists(
29
29
'button[data-timeframe="day"].timeframe-button.btn-outline-secondary'
30
30
)
roles/dashboard/templates/dashboard/chart-card-header.html.ep
@@ -0,0 +1,56 @@
1
Added:
<%
2
Added:
# Usage: include 'dashboard/chart-card-header',
3
Added:
# title => 'Chart Title',
4
Added:
# info_id => 'info-unique-id', # optional
5
Added:
# info_text => 'Explainer text...', # optional (requires info_id)
6
Added:
# download_href => '/path/to/csv', # optional
7
Added:
my $chart_title = stash('title') // 'Chart';
8
Added:
my $info_id = stash('info_id');
9
Added:
my $info_text = stash('info_text');
10
Added:
my $download_href = stash('download_href');
11
Added:
%>
12
Added:
<div class="card-header d-flex justify-content-between align-items-start gap-2">
13
Added:
<h2 class="h6 mb-0"><%== $chart_title %></h2>
14
Added:
<div class="d-flex align-items-center gap-1 flex-shrink-0">
15
Added:
% if ($info_id && $info_text) {
16
Added:
<button class="btn btn-sm btn-link text-body-secondary p-0 info-toggle"
17
Added:
type="button"
18
Added:
data-bs-toggle="collapse"
19
Added:
data-bs-target="#<%= $info_id %>"
20
Added:
aria-expanded="false"
21
Added:
aria-controls="<%= $info_id %>"
22
Added:
aria-label="About this chart">
23
Added:
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
24
Added:
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
25
Added:
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
26
Added:
</svg>
27
Added:
</button>
28
Added:
% }
29
Added:
<div class="dropdown">
30
Added:
<button
31
Added:
class="btn btn-sm btn-outline-secondary dropdown-toggle"
32
Added:
type="button"
33
Added:
data-bs-toggle="dropdown"
34
Added:
aria-expanded="false"
35
Added:
aria-label="Chart actions">
36
Added:
</button>
37
Added:
<ul class="dropdown-menu dropdown-menu-end">
38
Added:
% if ($download_href) {
39
Added:
<li>
40
Added:
<a class="dropdown-item" href="<%= $download_href %>">Download data</a>
41
Added:
</li>
42
Added:
% }
43
Added:
<li>
44
Added:
<button class="dropdown-item" type="button" disabled>Share (coming soon)</button>
45
Added:
</li>
46
Added:
</ul>
47
Added:
</div>
48
Added:
</div>
49
Added:
</div>
50
Added:
% if ($info_id && $info_text) {
51
Added:
<div class="collapse" id="<%= $info_id %>">
52
Added:
<div class="card-body border-bottom py-2">
53
Added:
<p class="text-body-secondary small mb-0"><%== $info_text %></p>
54
Added:
</div>
55
Added:
</div>
56
Added:
% }
roles/dashboard/templates/dashboard/insights.html.ep
@@ -14,31 +14,7 @@
14
14
<div class="row g-3">
15
15
<div class="col-12" id="stability">
16
16
<div class="card">
17
Removed:
<div class="card-header d-flex align-items-start justify-content-between">
18
Removed:
<h2 class="h6 mb-0">System Stability</h2>
19
Removed:
<button class="btn btn-sm btn-link text-body-secondary p-0 ms-2 info-toggle"
20
Removed:
type="button"
21
Removed:
data-bs-toggle="collapse"
22
Removed:
data-bs-target="#info-stability"
23
Removed:
aria-expanded="false"
24
Removed:
aria-controls="info-stability"
25
Removed:
aria-label="About this chart">
26
Removed:
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
27
Removed:
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
28
Removed:
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
29
Removed:
</svg>
30
Removed:
</button>
31
Removed:
</div>
32
Removed:
<div class="collapse" id="info-stability">
33
Removed:
<div class="card-body border-bottom py-2">
34
Removed:
<p class="text-body-secondary small mb-0">
35
Removed:
A <strong>lower score means more stable</strong>.
36
Removed:
This index is the rolling standard deviation (σ) of all probe values
37
Removed:
over a 6-hour window, normalised to each probe's optimal range.
38
Removed:
Spikes indicate rapid or large deviations from normal operating conditions.
39
Removed:
</p>
40
Removed:
</div>
41
Removed:
</div>
17
Added:
%= include 'dashboard/chart-card-header', title => 'System Stability', info_id => 'info-stability', info_text => 'A <strong>lower score means more stable</strong>. Rolling standard deviation (σ) of all probe values over a 6-hour window, normalised to each optimal range. Spikes indicate rapid or large deviations from normal conditions.'
42
18
<div class="card-body">
43
19
<div class="chart-wrap">
44
20
<canvas id="chart-stability" role="img" aria-label="System stability score — lower is better"></canvas>
@@ -49,29 +25,7 @@
49
25
50
26
<div class="col-12" id="rate-of-change">
51
27
<div class="card">
52
Removed:
<div class="card-header d-flex align-items-start justify-content-between">
53
Removed:
<h2 class="h6 mb-0">Rate of Change</h2>
54
Removed:
<button class="btn btn-sm btn-link text-body-secondary p-0 ms-2 info-toggle"
55
Removed:
type="button"
56
Removed:
data-bs-toggle="collapse"
57
Removed:
data-bs-target="#info-derivatives"
58
Removed:
aria-expanded="false"
59
Removed:
aria-controls="info-derivatives"
60
Removed:
aria-label="About this chart">
61
Removed:
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
62
Removed:
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
63
Removed:
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
64
Removed:
</svg>
65
Removed:
</button>
66
Removed:
</div>
67
Removed:
<div class="collapse" id="info-derivatives">
68
Removed:
<div class="card-body border-bottom py-2">
69
Removed:
<p class="text-body-secondary small mb-0">
70
Removed:
Normalised rate of change per probe (% of optimal range per hour).
71
Removed:
Values near zero mean the parameter is steady.
72
Removed:
</p>
73
Removed:
</div>
74
Removed:
</div>
28
Added:
%= include 'dashboard/chart-card-header', title => 'Rate of Change', info_id => 'info-derivatives', info_text => 'Normalised rate of change per probe (% of optimal range per hour). Values near zero mean the parameter is steady.'
75
29
<div class="card-body">
76
30
<div class="chart-wrap">
77
31
<canvas id="chart-derivatives" role="img" aria-label="Normalised rate of change per probe"></canvas>
@@ -82,28 +36,7 @@
82
36
83
37
<div class="col-12" id="diurnal">
84
38
<div class="card">
85
Removed:
<div class="card-header d-flex align-items-start justify-content-between">
86
Removed:
<h2 class="h6 mb-0">DO Diurnal Pattern</h2>
87
Removed:
<button class="btn btn-sm btn-link text-body-secondary p-0 ms-2 info-toggle"
88
Removed:
type="button"
89
Removed:
data-bs-toggle="collapse"
90
Removed:
data-bs-target="#info-diurnal"
91
Removed:
aria-expanded="false"
92
Removed:
aria-controls="info-diurnal"
93
Removed:
aria-label="About this chart">
94
Removed:
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
95
Removed:
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
96
Removed:
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
97
Removed:
</svg>
98
Removed:
</button>
99
Removed:
</div>
100
Removed:
<div class="collapse" id="info-diurnal">
101
Removed:
<div class="card-body border-bottom py-2">
102
Removed:
<p class="text-body-secondary small mb-0">
103
Removed:
Day/night DO cycle — each line is one day, bold is the average.
104
Removed:
</p>
105
Removed:
</div>
106
Removed:
</div>
39
Added:
%= include 'dashboard/chart-card-header', title => 'DO Diurnal Pattern', info_id => 'info-diurnal', info_text => 'Day/night DO cycle — each line is one day, bold is the 7-day average. Shows photosynthesis-driven oxygen production during daylight hours.'
107
40
<div class="card-body">
108
41
<div class="chart-wrap">
109
42
<canvas id="chart-diurnal" role="img" aria-label="Dissolved oxygen diurnal overlay"></canvas>
@@ -114,28 +47,7 @@
114
47
115
48
<div class="col-12" id="do-orp-overlay">
116
49
<div class="card">
117
Removed:
<div class="card-header d-flex align-items-start justify-content-between">
118
Removed:
<h2 class="h6 mb-0">DO & ORP Over Time</h2>
119
Removed:
<button class="btn btn-sm btn-link text-body-secondary p-0 ms-2 info-toggle"
120
Removed:
type="button"
121
Removed:
data-bs-toggle="collapse"
122
Removed:
data-bs-target="#info-do-orp"
123
Removed:
aria-expanded="false"
124
Removed:
aria-controls="info-do-orp"
125
Removed:
aria-label="About this chart">
126
Removed:
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
127
Removed:
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
128
Removed:
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
129
Removed:
</svg>
130
Removed:
</button>
131
Removed:
</div>
132
Removed:
<div class="collapse" id="info-do-orp">
133
Removed:
<div class="card-body border-bottom py-2">
134
Removed:
<p class="text-body-secondary small mb-0">
135
Removed:
Dual-axis overlay — correlated drops indicate oxygen-driven redox shifts.
136
Removed:
</p>
137
Removed:
</div>
138
Removed:
</div>
50
Added:
%= include 'dashboard/chart-card-header', title => 'DO & ORP Over Time', info_id => 'info-do-orp', info_text => 'Dual-axis overlay with outdoor temperature. Correlated DO/ORP drops indicate oxygen-driven redox shifts. The grey dashed line shows outdoor temperature for correlation.'
139
51
<div class="card-body">
140
52
<div class="chart-wrap">
141
53
<canvas id="chart-overlay-do-orp" role="img" aria-label="DO and ORP overlay chart"></canvas>
@@ -146,28 +58,7 @@
146
58
147
59
<div class="col-12" id="ph-do-lag">
148
60
<div class="card">
149
Removed:
<div class="card-header d-flex align-items-start justify-content-between">
150
Removed:
<h2 class="h6 mb-0">pH ↔ DO Relationship</h2>
151
Removed:
<button class="btn btn-sm btn-link text-body-secondary p-0 ms-2 info-toggle"
152
Removed:
type="button"
153
Removed:
data-bs-toggle="collapse"
154
Removed:
data-bs-target="#info-ph-do"
155
Removed:
aria-expanded="false"
156
Removed:
aria-controls="info-ph-do"
157
Removed:
aria-label="About this chart">
158
Removed:
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
159
Removed:
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
160
Removed:
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
161
Removed:
</svg>
162
Removed:
</button>
163
Removed:
</div>
164
Removed:
<div class="collapse" id="info-ph-do">
165
Removed:
<div class="card-body border-bottom py-2">
166
Removed:
<p class="text-body-secondary small mb-0">
167
Removed:
Nitrification link — DO drops often precede pH drops.
168
Removed:
</p>
169
Removed:
</div>
170
Removed:
</div>
61
Added:
%= include 'dashboard/chart-card-header', title => 'pH ↔ DO Relationship', info_id => 'info-ph-do', info_text => 'Nitrification link — DO drops often precede pH drops. When bacteria consume oxygen for nitrification, they produce acid which lowers pH.'
171
62
<div class="card-body">
172
63
<div class="chart-wrap">
173
64
<canvas id="chart-ph-do-lag" role="img" aria-label="pH and DO relationship overlay"></canvas>
roles/dashboard/templates/dashboard/probe-card.html.ep
@@ -5,6 +5,7 @@
5
5
% ec => 'Electrical conductivity reflects dissolved nutrient concentration. 300–1500 µS/cm is typical. Rising EC means nutrient accumulation; drops after water changes.',
6
6
% );
7
7
% my $info_id = "info-probe-$probe->{key}";
8
Added:
% my $dl_href = "/downloads/readings.csv?probe=$probe->{key}&timeframe=day&range=1";
8
9
<div class="card">
9
10
<div class="card-header d-flex justify-content-between align-items-start gap-2">
10
11
<div class="probe-card-title min-w-0">
@@ -13,7 +14,7 @@
13
14
</h1>
14
15
<p class="small mb-0 mt-1" data-variation="<%= $probe->{key} %>"></p>
15
16
</div>
16
Removed:
<div class="d-flex align-items-center gap-2 flex-shrink-0">
17
Added:
<div class="d-flex align-items-center gap-1 flex-shrink-0">
17
18
<button class="btn btn-sm btn-link text-body-secondary p-0 info-toggle"
18
19
type="button"
19
20
data-bs-toggle="collapse"
@@ -21,7 +22,7 @@
21
22
aria-expanded="false"
22
23
aria-controls="<%= $info_id %>"
23
24
aria-label="About this probe">
24
Removed:
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
25
Added:
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
25
26
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
26
27
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
27
28
</svg>
@@ -38,9 +39,9 @@
38
39
<li>
39
40
<a
40
41
class="dropdown-item graph-download-button"
41
Removed:
href="/downloads/readings.csv?probe=<%= $probe->{key} %>&timeframe=day&range=1"
42
Added:
href="<%= $dl_href %>"
42
43
data-raw-download
43
Removed:
data-probe="<%= $probe->{key} %>">Download raw data</a>
44
Added:
data-probe="<%= $probe->{key} %>">Download data</a>
44
45
</li>
45
46
</ul>
46
47
</div>
roles/dashboard/templates/dashboard/weather.html.ep
@@ -14,9 +14,7 @@
14
14
<div class="row g-3">
15
15
<div class="col-12" id="weather-temperature">
16
16
<div class="card">
17
Removed:
<div class="card-header">
18
Removed:
<h2 class="h6 mb-0">Temperature</h2>
19
Removed:
</div>
17
Added:
%= include 'dashboard/chart-card-header', title => 'Temperature', info_id => 'info-weather-temp', info_text => 'Outdoor air temperature at 2m above ground from the Météo France AROME model (1.5 km resolution). Relevant for estimating greenhouse and water temperature trends.'
20
18
<div class="card-body">
21
19
<div class="chart-wrap">
22
20
<canvas id="chart-weather-temperature" role="img" aria-label="Outdoor temperature over time"></canvas>
@@ -27,9 +25,7 @@
27
25
28
26
<div class="col-12" id="weather-humidity">
29
27
<div class="card">
30
Removed:
<div class="card-header">
31
Removed:
<h2 class="h6 mb-0">Humidity</h2>
32
Removed:
</div>
28
Added:
%= include 'dashboard/chart-card-header', title => 'Humidity', info_id => 'info-weather-humid', info_text => 'Outdoor relative humidity at 2m. High humidity reduces evaporation and plant transpiration; low humidity increases water loss from open systems.'
33
29
<div class="card-body">
34
30
<div class="chart-wrap">
35
31
<canvas id="chart-weather-humidity" role="img" aria-label="Outdoor relative humidity over time"></canvas>
@@ -40,9 +36,7 @@
40
36
41
37
<div class="col-12" id="weather-rain">
42
38
<div class="card">
43
Removed:
<div class="card-header">
44
Removed:
<h2 class="h6 mb-0">Rainfall</h2>
45
Removed:
</div>
39
Added:
%= include 'dashboard/chart-card-header', title => 'Rainfall', info_id => 'info-weather-rain', info_text => 'Hourly rainfall in mm. Heavy rain can dilute outdoor systems, cause overflow, and affect nutrient balance. Indoor systems are less affected but ambient humidity rises.'
46
40
<div class="card-body">
47
41
<div class="chart-wrap">
48
42
<canvas id="chart-weather-rain" role="img" aria-label="Rainfall over time"></canvas>