[Perl] DAQ system for the FAPG.
Add DAQ readings CSV downloads
Changed files
- roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
- roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Dashboard.pm
- roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Download.pm
- roles/dashboard/public/css/app.css
- roles/dashboard/public/js/downloads.js
- roles/dashboard/t/00-homepage.t
- roles/dashboard/t/01-graph-page.t
- roles/dashboard/t/05-status-page.t
- roles/dashboard/t/06-downloads.t
- roles/dashboard/t/lib/Dashboard/Test.pm
- roles/dashboard/templates/dashboard/downloads.html.ep
- roles/dashboard/templates/dashboard/graph.html.ep
- roles/dashboard/templates/dashboard/probes.html.ep
- roles/dashboard/templates/dashboard/status.html.ep
- roles/dashboard/templates/layouts/default.html.ep
roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
@@ -74,6 +74,8 @@
74
74
$r->get('/')->to('dashboard#index');
75
75
$r->get('/status')->to('dashboard#status');
76
76
$r->get('/probes')->to('dashboard#probe_list');
77
Added:
$r->get('/downloads')->to('dashboard#downloads');
78
Added:
$r->get('/downloads/readings.csv')->to('Download#readings');
77
79
$r->get('/graphs/:probe')->to('dashboard#graph');
78
80
79
81
$r->get('/api/readings/:probe/series')->to('Reading#series');
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Dashboard.pm
@@ -3,6 +3,8 @@
3
3
package FAPG::DAQ::Dashboard::Controller::Dashboard;
4
4
use Mojo::Base 'Mojolicious::Controller', -signatures;
5
5
6
Added:
use POSIX qw(strftime);
7
Added:
6
8
sub index ($self) {
7
9
$self->render(
8
10
template => 'dashboard/index',
@@ -21,6 +23,32 @@
21
23
$self->render(
22
24
template => 'dashboard/probes',
23
25
probes => $self->probes,
26
Added:
);
27
Added:
}
28
Added:
29
Added:
sub downloads ($self) {
30
Added:
my $from = strftime( '%Y-%m-%d', gmtime( time - ( 7 * 24 * 60 * 60 ) ) );
31
Added:
my $to = strftime( '%Y-%m-%d', gmtime(time) );
32
Added:
my $database = $self->sqlite->db;
33
Added:
my $table = $database->query(
34
Added:
q{SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'readings'}
35
Added:
)->hash;
36
Added:
my $oldest;
37
Added:
38
Added:
if ($table) {
39
Added:
$oldest = $database->query(
40
Added:
'SELECT MIN(COALESCE(received_at, timestamp)) AS timestamp FROM readings'
41
Added:
)->hash->{timestamp};
42
Added:
}
43
Added:
44
Added:
$oldest = $from if !defined $oldest;
45
Added:
$oldest =~ s/T.*\z//;
46
Added:
47
Added:
$self->render(
48
Added:
template => 'dashboard/downloads',
49
Added:
from => $from,
50
Added:
to => $to,
51
Added:
oldest => $oldest,
24
52
);
25
53
}
26
54
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Download.pm
@@ -0,0 +1,84 @@
1
Added:
# -*- mode: cperl; -*-
2
Added:
3
Added:
package FAPG::DAQ::Dashboard::Controller::Download;
4
Added:
use Mojo::Base 'Mojolicious::Controller', -signatures;
5
Added:
6
Added:
use POSIX qw(strftime);
7
Added:
use Time::Local qw(timegm);
8
Added:
9
Added:
sub readings ($self) {
10
Added:
my $from = $self->param('from') // '';
11
Added:
my $to = $self->param('to') // '';
12
Added:
my $from_epoch = date_epoch($from);
13
Added:
my $to_epoch = date_epoch($to);
14
Added:
15
Added:
return $self->render(
16
Added:
status => 400,
17
Added:
text => 'The from and to dates must be valid calendar dates.',
18
Added:
) unless defined $from_epoch && defined $to_epoch;
19
Added:
20
Added:
return $self->render(
21
Added:
status => 400,
22
Added:
text => 'The from date must not be after the to date.',
23
Added:
) if $from_epoch > $to_epoch;
24
Added:
25
Added:
my $database = $self->sqlite->db;
26
Added:
my $table = $database->query(
27
Added:
q{SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'readings'}
28
Added:
)->hash;
29
Added:
my $rows = [];
30
Added:
31
Added:
if ($table) {
32
Added:
$rows = $database->query(
33
Added:
q{
34
Added:
SELECT timestamp, received_at, node, probe, value, unit
35
Added:
FROM readings
36
Added:
WHERE COALESCE(received_at, timestamp) >= ?
37
Added:
AND COALESCE(received_at, timestamp) < ?
38
Added:
ORDER BY COALESCE(received_at, timestamp), id
39
Added:
},
40
Added:
utc_timestamp($from_epoch),
41
Added:
utc_timestamp( $to_epoch + ( 24 * 60 * 60 ) ),
42
Added:
)->hashes->to_array;
43
Added:
}
44
Added:
45
Added:
my @lines = ('timestamp,received_at,node,probe,value,unit');
46
Added:
47
Added:
for my $row ( $rows->@* ) {
48
Added:
push @lines, join ',', map { csv_value( $row->{$_} ) }
49
Added:
qw(timestamp received_at node probe value unit);
50
Added:
}
51
Added:
52
Added:
my $filename = "fapg-daq-readings-$from-to-$to.csv";
53
Added:
54
Added:
$self->res->headers->content_type('text/csv; charset=utf-8');
55
Added:
$self->res->headers->content_disposition(
56
Added:
qq{attachment; filename="$filename"} );
57
Added:
$self->render( data => join( "\n", @lines ) . "\n" );
58
Added:
}
59
Added:
60
Added:
sub csv_value ($value) {
61
Added:
$value //= '';
62
Added:
$value =~ s/"/""/g;
63
Added:
64
Added:
return qq{"$value"};
65
Added:
}
66
Added:
67
Added:
sub date_epoch ($date) {
68
Added:
return undef unless $date =~ /\A(\d{4})-(\d{2})-(\d{2})\z/;
69
Added:
70
Added:
my ( $year, $month, $day ) = ( $1, $2, $3 );
71
Added:
my $epoch = eval { timegm( 0, 0, 0, $day, $month - 1, $year ) };
72
Added:
73
Added:
return undef if !defined $epoch || $@;
74
Added:
75
Added:
return undef unless strftime( '%Y-%m-%d', gmtime($epoch) ) eq $date;
76
Added:
77
Added:
return $epoch;
78
Added:
}
79
Added:
80
Added:
sub utc_timestamp ($epoch) {
81
Added:
return strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime $epoch );
82
Added:
}
83
Added:
84
Added:
1;
roles/dashboard/public/css/app.css
@@ -98,6 +98,80 @@
98
98
color: var(--accent);
99
99
}
100
100
101
Added:
.downloads-form {
102
Added:
display: flex;
103
Added:
flex: 1;
104
Added:
flex-direction: column;
105
Added:
gap: 1rem;
106
Added:
}
107
Added:
108
Added:
.downloads-card {
109
Added:
display: flex;
110
Added:
flex-direction: column;
111
Added:
min-height: 20rem;
112
Added:
}
113
Added:
114
Added:
.downloads-card .card-body {
115
Added:
display: flex;
116
Added:
flex: 1;
117
Added:
}
118
Added:
119
Added:
.download-date-fields {
120
Added:
display: flex;
121
Added:
flex-wrap: wrap;
122
Added:
gap: 1rem;
123
Added:
}
124
Added:
125
Added:
.download-date-field {
126
Added:
display: grid;
127
Added:
gap: 0.35rem;
128
Added:
}
129
Added:
130
Added:
.download-date-field label {
131
Added:
font-weight: 700;
132
Added:
}
133
Added:
134
Added:
.date-input-control {
135
Added:
display: flex;
136
Added:
gap: 0.5rem;
137
Added:
}
138
Added:
139
Added:
.date-input-control input,
140
Added:
.downloads-button,
141
Added:
.secondary-button {
142
Added:
min-height: 2.75rem;
143
Added:
border: 1px solid var(--border);
144
Added:
border-radius: 0.4rem;
145
Added:
padding: 0.5rem 0.65rem;
146
Added:
font: inherit;
147
Added:
}
148
Added:
149
Added:
.downloads-button {
150
Added:
align-self: flex-end;
151
Added:
margin-top: auto;
152
Added:
border-color: var(--accent);
153
Added:
background: var(--accent);
154
Added:
color: white;
155
Added:
font-weight: 700;
156
Added:
cursor: pointer;
157
Added:
}
158
Added:
159
Added:
.downloads-button:hover {
160
Added:
filter: brightness(0.9);
161
Added:
}
162
Added:
163
Added:
.secondary-button {
164
Added:
background: white;
165
Added:
color: var(--accent);
166
Added:
font-weight: 700;
167
Added:
cursor: pointer;
168
Added:
}
169
Added:
170
Added:
.secondary-button:hover {
171
Added:
border-color: var(--accent);
172
Added:
background: var(--accent-soft);
173
Added:
}
174
Added:
101
175
.header-status-link {
102
176
position: relative;
103
177
display: inline-grid;
@@ -394,7 +468,7 @@
394
468
text-align: right;
395
469
}
396
470
397
Removed:
.dashboard-card {
471
Added:
.card {
398
472
background: var(--card);
399
473
border: 1px solid var(--border);
400
474
border-radius: 0.5rem;
@@ -402,6 +476,10 @@
402
476
overflow: hidden;
403
477
}
404
478
479
Added:
.card + .card {
480
Added:
margin-top: 1rem;
481
Added:
}
482
Added:
405
483
.tabs {
406
484
display: flex;
407
485
flex-wrap: wrap;
@@ -432,35 +510,39 @@
432
510
color: white;
433
511
}
434
512
435
Removed:
.tab-panel {
436
Removed:
display: none;
437
Removed:
padding: 1.25rem;
438
Removed:
scroll-margin-top: 5.5rem;
439
Removed:
}
440
Removed:
441
Removed:
.tab-panel.is-active {
442
Removed:
display: block;
443
Removed:
}
444
Removed:
445
Removed:
.panel-heading {
513
Added:
.card-heading {
446
514
display: flex;
447
515
justify-content: space-between;
448
516
gap: 1rem;
449
517
align-items: flex-start;
450
Removed:
margin-bottom: 1rem;
518
Added:
padding: 1.25rem;
519
Added:
border-bottom: 1px solid var(--border);
451
520
}
452
521
453
Removed:
.panel-heading h1,
454
Removed:
.panel-heading h2 {
522
Added:
.card-heading h1,
523
Added:
.card-heading h2 {
455
524
margin: 0;
456
525
font-size: 1.35rem;
457
526
}
458
527
459
Removed:
.panel-heading p {
528
Added:
.card-heading p {
460
529
margin: 0;
461
530
color: var(--muted);
462
531
}
463
532
533
Added:
.card-body {
534
Added:
padding: 1.25rem;
535
Added:
}
536
Added:
537
Added:
.tab-panel {
538
Added:
display: none;
539
Added:
scroll-margin-top: 5.5rem;
540
Added:
}
541
Added:
542
Added:
.tab-panel.is-active {
543
Added:
display: block;
544
Added:
}
545
Added:
464
546
.panel-tools {
465
547
display: flex;
466
548
flex-wrap: wrap;
@@ -574,14 +656,6 @@
574
656
border-bottom: 0;
575
657
}
576
658
577
Removed:
.status-page-card {
578
Removed:
padding: 1.25rem;
579
Removed:
}
580
Removed:
581
Removed:
.status-page-card .panel-heading {
582
Removed:
margin-bottom: 1rem;
583
Removed:
}
584
Removed:
585
659
.status-table {
586
660
min-width: 760px;
587
661
}
@@ -724,11 +798,12 @@
724
798
padding-inline: 0.65rem;
725
799
}
726
800
727
Removed:
.panel-heading {
801
Added:
.card-heading {
728
802
display: block;
729
803
}
730
804
731
Removed:
.tab-panel {
805
Added:
.card-heading,
806
Added:
.card-body {
732
807
padding: 1rem;
733
808
}
734
809
roles/dashboard/public/js/downloads.js
@@ -0,0 +1,9 @@
1
Added:
document.querySelectorAll('[data-date-shortcut]').forEach(button => {
2
Added:
button.addEventListener('click', () => {
3
Added:
const input = document.querySelector(button.dataset.dateShortcut);
4
Added:
5
Added:
if (input) {
6
Added:
input.value = button.dataset.dateValue;
7
Added:
}
8
Added:
});
9
Added:
});
roles/dashboard/t/00-homepage.t
@@ -31,7 +31,7 @@
31
31
'footer.site-footer a[href="https://www.lafermeaquaponique.com/"]')
32
32
->content_like(qr/Copyright © 2026 Marius Peter/)
33
33
->element_exists_not('section#probe-ph[data-panel="ph"]')
34
Removed:
->element_exists_not('section.dashboard-card')
34
Added:
->element_exists_not('div.card')
35
35
->content_like(qr/Checking sensors/)
36
36
->content_like(qr/data-overview-status/)
37
37
->content_like(qr/data-sensor-status="ph"/)
roles/dashboard/t/01-graph-page.t
@@ -25,6 +25,8 @@
25
25
->element_exists_not('div.nav-links a[href^="/graphs/"]')
26
26
->element_exists(
27
27
'footer.site-footer a[href="https://www.lafermeaquaponique.com/"]')
28
Added:
->element_exists('div.card > div.card-heading')
29
Added:
->element_exists('div.card > div.card-body')
28
30
->element_exists('section#probe-ph[data-panel="ph"]')
29
31
->element_exists('canvas#chart-ph')
30
32
->element_exists_not('section#probe-do[data-panel="do"]')
@@ -41,6 +43,8 @@
41
43
$t->get_ok('/probes')
42
44
->status_is(200)
43
45
->text_is('h1', 'pH')
46
Added:
->element_exists('div.card > div.card-heading')
47
Added:
->element_exists('div.card > div.card-body')
44
48
->element_exists('section#probe-ph[data-panel="ph"]')
45
49
->element_exists('section#probe-do[data-panel="do"]')
46
50
->element_exists('section#probe-orp[data-panel="orp"]')
roles/dashboard/t/05-status-page.t
@@ -14,6 +14,8 @@
14
14
$t->get_ok('/status')
15
15
->status_is(200)
16
16
->text_is('h1', 'DAQ Status')
17
Added:
->element_exists('div.card.status-page-card > div.card-heading')
18
Added:
->element_exists('div.card.status-page-card > div.card-body')
17
19
->element_exists('a.header-status-link[href="/status"][data-header-status]')
18
20
->element_exists('table.status-table')
19
21
->element_exists('tr[data-status-page-item="hub"]')
roles/dashboard/t/06-downloads.t
@@ -0,0 +1,51 @@
1
Added:
# -*- mode: cperl; -*-
2
Added:
3
Added:
use Mojo::Base -strict;
4
Added:
5
Added:
use Test2::V0;
6
Added:
7
Added:
use FindBin;
8
Added:
use lib "${FindBin::Bin}/../lib/";
9
Added:
use lib "${FindBin::Bin}/lib/";
10
Added:
use Dashboard::Test qw(test_app test_empty_app);
11
Added:
12
Added:
my $t = test_app();
13
Added:
14
Added:
$t->get_ok('/downloads')
15
Added:
->status_is(200)
16
Added:
->text_is( 'div.nav-links a[href="/probes"]', 'Probes' )
17
Added:
->text_is( 'div.nav-links a[href="/downloads"]', 'Downloads' )
18
Added:
->element_exists('a.header-status-link[href="/status"][data-header-status]')
19
Added:
->element_exists('a.header-status-link span.header-status-led')
20
Added:
->element_exists('div.card.downloads-card > div.card-heading')
21
Added:
->element_exists('div.card.downloads-card > div.card-body')
22
Added:
->element_exists('form.downloads-form[action="/downloads/readings.csv"]')
23
Added:
->element_exists('input[name="from"][type="date"][required]')
24
Added:
->element_exists('input[name="to"][type="date"][required]')
25
Added:
->text_is('button.secondary-button[data-date-shortcut="#download-from"]', 'Oldest')
26
Added:
->text_is('button.secondary-button[data-date-shortcut="#download-to"]', 'Today')
27
Added:
->text_is('button.downloads-button[type="submit"]', 'Download CSV');
28
Added:
29
Added:
$t->get_ok('/downloads/readings.csv?from=2026-07-05&to=2026-07-05')
30
Added:
->status_is(200)
31
Added:
->header_like( 'Content-Type' => qr{text/csv} )
32
Added:
->header_like( 'Content-Disposition' => qr{attachment} )
33
Added:
->content_like(qr/^timestamp,received_at,node,probe,value,unit/m)
34
Added:
->content_like(qr/2026-07-05T12:00:00Z/)
35
Added:
->content_like(qr/2026-07-05T12:00:15Z/);
36
Added:
37
Added:
$t->get_ok('/downloads/readings.csv?from=2026-07-06&to=2026-07-05')
38
Added:
->status_is(400)
39
Added:
->content_like(qr/from date must not be after the to date/);
40
Added:
41
Added:
my $empty_t = test_empty_app();
42
Added:
43
Added:
$empty_t->get_ok('/downloads')
44
Added:
->status_is(200)
45
Added:
->element_exists('button.secondary-button[data-date-shortcut="#download-from"]');
46
Added:
47
Added:
$empty_t->get_ok('/downloads/readings.csv?from=2026-07-05&to=2026-07-05')
48
Added:
->status_is(200)
49
Added:
->content_is("timestamp,received_at,node,probe,value,unit\n");
50
Added:
51
Added:
done_testing;
roles/dashboard/t/lib/Dashboard/Test.pm
@@ -9,7 +9,7 @@
9
9
10
10
use FAPG::DAQ::Dashboard;
11
11
12
Removed:
our @EXPORT_OK = qw(test_app);
12
Added:
our @EXPORT_OK = qw(test_app test_empty_app);
13
13
14
14
sub test_app {
15
15
$ENV{FAPG_DAQ_DB} = ':memory:';
@@ -23,6 +23,12 @@
23
23
seed_statuses( $t, $fresh_status_at, $stale_status_at );
24
24
25
25
return $t;
26
Added:
}
27
Added:
28
Added:
sub test_empty_app {
29
Added:
$ENV{FAPG_DAQ_DB} = ':memory:';
30
Added:
31
Added:
return Test::Mojo->new('FAPG::DAQ::Dashboard');
26
32
}
27
33
28
34
sub create_schema {
roles/dashboard/templates/dashboard/downloads.html.ep
@@ -0,0 +1,38 @@
1
Added:
<!-- -*- mode: web; -*- -->
2
Added:
3
Added:
% layout 'default';
4
Added:
% title 'FAPG DAQ Downloads';
5
Added:
6
Added:
<div class="card downloads-card">
7
Added:
<div class="card-heading">
8
Added:
<div>
9
Added:
<h1>Downloads</h1>
10
Added:
<p>Download recorded DAQ readings as a CSV file.</p>
11
Added:
</div>
12
Added:
</div>
13
Added:
14
Added:
<div class="card-body">
15
Added:
<form class="downloads-form" action="/downloads/readings.csv" method="get">
16
Added:
<div class="download-date-fields">
17
Added:
<div class="download-date-field">
18
Added:
<label for="download-from">From</label>
19
Added:
<div class="date-input-control">
20
Added:
<input id="download-from" name="from" type="date" value="<%= $from %>" required>
21
Added:
<button class="secondary-button" type="button" data-date-shortcut="#download-from" data-date-value="<%= $oldest %>">Oldest</button>
22
Added:
</div>
23
Added:
</div>
24
Added:
<div class="download-date-field">
25
Added:
<label for="download-to">To</label>
26
Added:
<div class="date-input-control">
27
Added:
<input id="download-to" name="to" type="date" value="<%= $to %>" required>
28
Added:
<button class="secondary-button" type="button" data-date-shortcut="#download-to" data-date-value="<%= $to %>">Today</button>
29
Added:
</div>
30
Added:
</div>
31
Added:
</div>
32
Added:
<button class="downloads-button" type="submit">Download CSV</button>
33
Added:
</form>
34
Added:
</div>
35
Added:
</div>
36
Added:
37
Added:
<script src="/js/dashboard.js"></script>
38
Added:
<script src="/js/downloads.js"></script>
roles/dashboard/templates/dashboard/graph.html.ep
@@ -3,31 +3,28 @@
3
3
% layout 'default';
4
4
% title "FAPG DAQ $probe->{label}";
5
5
6
Removed:
<section class="dashboard-card">
7
Removed:
<section
8
Removed:
id="probe-<%= $probe->{key} %>"
9
Removed:
class="tab-panel is-active"
10
Removed:
data-panel="<%= $probe->{key} %>">
11
Removed:
12
Removed:
<div class="panel-heading">
13
Removed:
<h1><%= $probe->{label} %></h1>
14
Removed:
<div class="panel-tools">
15
Removed:
<p>
16
Removed:
Unit:
17
Removed:
<strong><%= $probe->{unit} %></strong>
18
Removed:
</p>
19
Removed:
<div class="timeframe-control" role="group" aria-label="<%= $probe->{label} %> chart timeframe">
20
Removed:
<button class="timeframe-button" type="button" data-timeframe="hour">Hourly</button>
21
Removed:
<button class="timeframe-button is-active" type="button" data-timeframe="day">Daily</button>
22
Removed:
<button class="timeframe-button" type="button" data-timeframe="week">Weekly</button>
23
Removed:
<button class="timeframe-button" type="button" data-timeframe="year">Yearly</button>
24
Removed:
</div>
25
Removed:
<label class="smoothing-control">
26
Removed:
<input type="checkbox" data-smoothing-toggle checked>
27
Removed:
<span>Smoothed</span>
28
Removed:
</label>
6
Added:
<div class="card">
7
Added:
<div class="card-heading">
8
Added:
<h1><%= $probe->{label} %></h1>
9
Added:
<div class="panel-tools">
10
Added:
<p>
11
Added:
Unit:
12
Added:
<strong><%= $probe->{unit} %></strong>
13
Added:
</p>
14
Added:
<div class="timeframe-control" role="group" aria-label="<%= $probe->{label} %> chart timeframe">
15
Added:
<button class="timeframe-button" type="button" data-timeframe="hour">Hourly</button>
16
Added:
<button class="timeframe-button is-active" type="button" data-timeframe="day">Daily</button>
17
Added:
<button class="timeframe-button" type="button" data-timeframe="week">Weekly</button>
18
Added:
<button class="timeframe-button" type="button" data-timeframe="year">Yearly</button>
29
19
</div>
20
Added:
<label class="smoothing-control">
21
Added:
<input type="checkbox" data-smoothing-toggle checked>
22
Added:
<span>Smoothed</span>
23
Added:
</label>
30
24
</div>
25
Added:
</div>
26
Added:
<div class="card-body">
27
Added:
<section id="probe-<%= $probe->{key} %>" class="tab-panel is-active" data-panel="<%= $probe->{key} %>">
31
28
32
29
<div class="chart-wrap">
33
30
<canvas id="chart-<%= $probe->{key} %>"></canvas>
@@ -58,8 +55,9 @@
58
55
</table>
59
56
</div>
60
57
</section>
61
Removed:
</section>
62
Removed:
</section>
58
Added:
</section>
59
Added:
</div>
60
Added:
</div>
63
61
64
62
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
65
63
<script src="/js/dashboard.js"></script>
roles/dashboard/templates/dashboard/probes.html.ep
@@ -4,31 +4,28 @@
4
4
% title 'FAPG DAQ Probes';
5
5
6
6
% for my $probe ($probes->@*) {
7
Removed:
<section class="dashboard-card">
8
Removed:
<section
9
Removed:
id="probe-<%= $probe->{key} %>"
10
Removed:
class="tab-panel is-active"
11
Removed:
data-panel="<%= $probe->{key} %>">
12
Removed:
13
Removed:
<div class="panel-heading">
14
Removed:
<h1><%= $probe->{label} %></h1>
15
Removed:
<div class="panel-tools">
16
Removed:
<p>
17
Removed:
Unit:
18
Removed:
<strong><%= $probe->{unit} %></strong>
19
Removed:
</p>
20
Removed:
<div class="timeframe-control" role="group" aria-label="<%= $probe->{label} %> chart timeframe">
21
Removed:
<button class="timeframe-button" type="button" data-timeframe="hour">Hourly</button>
22
Removed:
<button class="timeframe-button is-active" type="button" data-timeframe="day">Daily</button>
23
Removed:
<button class="timeframe-button" type="button" data-timeframe="week">Weekly</button>
24
Removed:
<button class="timeframe-button" type="button" data-timeframe="year">Yearly</button>
25
Removed:
</div>
26
Removed:
<label class="smoothing-control">
27
Removed:
<input type="checkbox" data-smoothing-toggle checked>
28
Removed:
<span>Smoothed</span>
29
Removed:
</label>
7
Added:
<div class="card">
8
Added:
<div class="card-heading">
9
Added:
<h1><%= $probe->{label} %></h1>
10
Added:
<div class="panel-tools">
11
Added:
<p>
12
Added:
Unit:
13
Added:
<strong><%= $probe->{unit} %></strong>
14
Added:
</p>
15
Added:
<div class="timeframe-control" role="group" aria-label="<%= $probe->{label} %> chart timeframe">
16
Added:
<button class="timeframe-button" type="button" data-timeframe="hour">Hourly</button>
17
Added:
<button class="timeframe-button is-active" type="button" data-timeframe="day">Daily</button>
18
Added:
<button class="timeframe-button" type="button" data-timeframe="week">Weekly</button>
19
Added:
<button class="timeframe-button" type="button" data-timeframe="year">Yearly</button>
30
20
</div>
21
Added:
<label class="smoothing-control">
22
Added:
<input type="checkbox" data-smoothing-toggle checked>
23
Added:
<span>Smoothed</span>
24
Added:
</label>
31
25
</div>
26
Added:
</div>
27
Added:
<div class="card-body">
28
Added:
<section id="probe-<%= $probe->{key} %>" class="tab-panel is-active" data-panel="<%= $probe->{key} %>">
32
29
33
30
<div class="chart-wrap">
34
31
<canvas id="chart-<%= $probe->{key} %>"></canvas>
@@ -59,8 +56,9 @@
59
56
</table>
60
57
</div>
61
58
</section>
62
Removed:
</section>
63
Removed:
</section>
59
Added:
</section>
60
Added:
</div>
61
Added:
</div>
64
62
% }
65
63
66
64
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
roles/dashboard/templates/dashboard/status.html.ep
@@ -3,16 +3,17 @@
3
3
% layout 'default';
4
4
% title 'FAPG DAQ Status';
5
5
6
Removed:
<section class="dashboard-card status-page-card">
7
Removed:
<div class="panel-heading">
6
Added:
<div class="card status-page-card">
7
Added:
<div class="card-heading">
8
8
<div>
9
9
<h1>DAQ Status</h1>
10
10
<p>Current connection status for the hub and sensor probes.</p>
11
11
</div>
12
12
</div>
13
13
14
Removed:
<div class="message-table-wrap">
15
Removed:
<table class="message-table status-table">
14
Added:
<div class="card-body">
15
Added:
<div class="message-table-wrap">
16
Added:
<table class="message-table status-table">
16
17
<thead>
17
18
<tr>
18
19
<th scope="col">Probe</th>
@@ -33,8 +34,9 @@
33
34
</tr>
34
35
% }
35
36
</tbody>
36
Removed:
</table>
37
Added:
</table>
38
Added:
</div>
37
39
</div>
38
Removed:
</section>
40
Added:
</div>
39
41
40
42
<script src="/js/dashboard.js"></script>
roles/dashboard/templates/layouts/default.html.ep
@@ -22,6 +22,7 @@
22
22
</label>
23
23
<div class="nav-links">
24
24
<a href="/probes">Probes</a>
25
Added:
<a href="/downloads">Downloads</a>
25
26
</div>
26
27
<a
27
28
class="header-status-link is-unknown"