[Perl] DAQ system for the FAPG.
Namespace dashboard under FAPG::DAQ
Move the Mojolicious dashboard app and controllers into the FAPG::DAQ::Dashboard namespace. Update the dashboard startup script and test helper to load the namespaced app while preserving the existing dashboard moniker and configuration file behavior.
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/Reading.pm
- roles/dashboard/lib/dashboard.pm
- roles/dashboard/lib/dashboard/Controller/Dashboard.pm
- roles/dashboard/lib/dashboard/Controller/Reading.pm
- roles/dashboard/script/dashboard
- roles/dashboard/t/lib/Dashboard/Test.pm
roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
@@ -0,0 +1,85 @@
1
Added:
# -*- mode: cperl; -*-
2
Added:
3
Added:
package FAPG::DAQ::Dashboard;
4
Added:
use Mojo::Base 'Mojolicious', -signatures;
5
Added:
6
Added:
use Mojo::SQLite;
7
Added:
8
Added:
# This method will run once at server start
9
Added:
sub startup ($self) {
10
Added:
$self->moniker('dashboard');
11
Added:
12
Added:
# Load configuration from config file
13
Added:
my $config = $self->plugin('NotYAMLConfig');
14
Added:
15
Added:
# Configure the application
16
Added:
$self->secrets( $config->{secrets} );
17
Added:
18
Added:
$self->config(
19
Added:
hypnotoad => {
20
Added:
listen => ['http://127.0.0.1:3000'],
21
Added:
pid_file => '/run/fapg-daq-dashboard/hypnotoad.pid',
22
Added:
workers => 2,
23
Added:
proxy => 1,
24
Added:
},
25
Added:
);
26
Added:
27
Added:
my $db_path = $ENV{FAPG_DAQ_DB} // $self->home->rel_file('fapg-daq.sqlite3');
28
Added:
29
Added:
my $sqlite = Mojo::SQLite->new("sqlite:$db_path");
30
Added:
31
Added:
$self->helper( sqlite => sub { $sqlite } );
32
Added:
33
Added:
$self->helper(
34
Added:
probes => sub {
35
Added:
return [
36
Added:
{
37
Added:
key => 'ph',
38
Added:
label => 'pH',
39
Added:
unit => 'pH',
40
Added:
node => 'Pi Zero 01',
41
Added:
},
42
Added:
{
43
Added:
key => 'do',
44
Added:
label => 'Dissolved Oxygen',
45
Added:
unit => 'mg/L',
46
Added:
node => 'Pi Zero 03',
47
Added:
},
48
Added:
{
49
Added:
key => 'orp',
50
Added:
label => 'ORP',
51
Added:
unit => 'mV',
52
Added:
node => 'Pi Zero 04',
53
Added:
},
54
Added:
{
55
Added:
key => 'ec',
56
Added:
label => 'Electrical Conductivity',
57
Added:
unit => 'µS/cm',
58
Added:
node => 'Pi Zero 02',
59
Added:
},
60
Added:
];
61
Added:
}
62
Added:
);
63
Added:
64
Added:
$self->helper(
65
Added:
status_items => sub {
66
Added:
return [
67
Added:
{
68
Added:
key => 'hub',
69
Added:
label => 'DAQ Hub',
70
Added:
},
71
Added:
$self->probes->@*,
72
Added:
];
73
Added:
}
74
Added:
);
75
Added:
76
Added:
my $r = $self->routes;
77
Added:
78
Added:
$r->get('/')->to('dashboard#index');
79
Added:
$r->get('/graphs/:probe')->to('dashboard#graph');
80
Added:
81
Added:
$r->get('/api/readings/:probe')->to('Reading#list');
82
Added:
$r->get('/api/status/:probe')->to('Reading#status');
83
Added:
}
84
Added:
85
Added:
1;
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Dashboard.pm
@@ -0,0 +1,25 @@
1
Added:
# -*- mode: cperl; -*-
2
Added:
3
Added:
package FAPG::DAQ::Dashboard::Controller::Dashboard;
4
Added:
use Mojo::Base 'Mojolicious::Controller', -signatures;
5
Added:
6
Added:
sub index ($self) {
7
Added:
$self->render(
8
Added:
template => 'dashboard/index',
9
Added:
probes => $self->probes,
10
Added:
);
11
Added:
}
12
Added:
13
Added:
sub graph ($self) {
14
Added:
my $probe_key = $self->stash('probe');
15
Added:
my ($probe) = grep { $_->{key} eq $probe_key } $self->probes->@*;
16
Added:
17
Added:
return $self->reply->not_found unless defined $probe;
18
Added:
19
Added:
$self->render(
20
Added:
template => 'dashboard/graph',
21
Added:
probe => $probe,
22
Added:
);
23
Added:
}
24
Added:
25
Added:
1;
roles/dashboard/lib/FAPG/DAQ/Dashboard/Controller/Reading.pm
@@ -0,0 +1,109 @@
1
Added:
# -*- mode: cperl; -*-
2
Added:
3
Added:
package FAPG::DAQ::Dashboard::Controller::Reading;
4
Added:
use Mojo::Base 'Mojolicious::Controller', -signatures;
5
Added:
6
Added:
use POSIX qw(strftime);
7
Added:
8
Added:
my $STATUS_REACHABLE_SECONDS = 120;
9
Added:
10
Added:
sub list ($self) {
11
Added:
my $probe = $self->param('probe') // '';
12
Added:
13
Added:
my %known = map { $_->{key} => 1 } $self->probes->@*;
14
Added:
15
Added:
return $self->render(
16
Added:
status => 404,
17
Added:
json => {
18
Added:
error => "Unknown probe type: $probe",
19
Added:
},
20
Added:
) unless $known{$probe};
21
Added:
22
Added:
my $limit = $self->param('limit') // 300;
23
Added:
24
Added:
$limit = 300 unless $limit =~ /^\d+$/;
25
Added:
$limit = 2_000 if $limit > 2_000;
26
Added:
27
Added:
my @where = ('probe = ?');
28
Added:
my @bind = ($probe);
29
Added:
my $since = $self->param('since');
30
Added:
31
Added:
if ( defined $since && $since =~ /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ ) {
32
Added:
push @where, 'COALESCE(received_at, timestamp) >= ?';
33
Added:
push @bind, $since;
34
Added:
}
35
Added:
36
Added:
my $rows = $self->sqlite->db->query(
37
Added:
q{
38
Added:
SELECT timestamp, received_at, node, probe, value, unit
39
Added:
FROM readings
40
Added:
WHERE
41
Added:
}
42
Added:
. join( "\n AND ", @where ) . q{
43
Added:
ORDER BY COALESCE(received_at, timestamp) DESC, id DESC
44
Added:
LIMIT ?
45
Added:
},
46
Added:
@bind,
47
Added:
$limit,
48
Added:
)->hashes->to_array;
49
Added:
50
Added:
$rows = [ reverse $rows->@* ];
51
Added:
52
Added:
$self->render(
53
Added:
json => {
54
Added:
probe => $probe,
55
Added:
readings => $rows,
56
Added:
},
57
Added:
);
58
Added:
}
59
Added:
60
Added:
sub status ($self) {
61
Added:
my $probe = $self->param('probe') // '';
62
Added:
63
Added:
my %known = map { $_->{key} => 1 } $self->status_items->@*;
64
Added:
65
Added:
return $self->render(
66
Added:
status => 404,
67
Added:
json => {
68
Added:
error => "Unknown status item: $probe",
69
Added:
},
70
Added:
) unless $known{$probe};
71
Added:
72
Added:
my $row = eval {
73
Added:
$self->sqlite->db->query(
74
Added:
q{
75
Added:
SELECT timestamp, received_at, node, probe, status, message, error, device
76
Added:
FROM node_status
77
Added:
WHERE probe = ?
78
Added:
AND valid = 1
79
Added:
ORDER BY COALESCE(timestamp, received_at) DESC, id DESC
80
Added:
LIMIT 1
81
Added:
},
82
Added:
$probe,
83
Added:
)->hash;
84
Added:
};
85
Added:
86
Added:
$row = undef if $@;
87
Added:
mark_unreachable_if_stale($row) if $row;
88
Added:
89
Added:
$self->render(
90
Added:
json => {
91
Added:
probe => $probe,
92
Added:
status => $row,
93
Added:
},
94
Added:
);
95
Added:
}
96
Added:
97
Added:
sub mark_unreachable_if_stale ($row) {
98
Added:
my $received_at = $row->{received_at} // $row->{timestamp};
99
Added:
return if !defined $received_at || $received_at ge reachable_since();
100
Added:
101
Added:
$row->{status} = 'unreachable';
102
Added:
$row->{message} = "No status received in the last $STATUS_REACHABLE_SECONDS seconds";
103
Added:
}
104
Added:
105
Added:
sub reachable_since {
106
Added:
return strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime( time - $STATUS_REACHABLE_SECONDS ) );
107
Added:
}
108
Added:
109
Added:
1;
roles/dashboard/lib/dashboard.pm
@@ -1,84 +0,0 @@
1
Removed:
# -*- mode: cperl; -*-
2
Removed:
3
Removed:
package dashboard;
4
Removed:
use Mojo::Base 'Mojolicious', -signatures;
5
Removed:
6
Removed:
use Mojo::SQLite;
7
Removed:
8
Removed:
# This method will run once at server start
9
Removed:
sub startup ($self) {
10
Removed:
11
Removed:
# Load configuration from config file
12
Removed:
my $config = $self->plugin('NotYAMLConfig');
13
Removed:
14
Removed:
# Configure the application
15
Removed:
$self->secrets( $config->{secrets} );
16
Removed:
17
Removed:
$self->config(
18
Removed:
hypnotoad => {
19
Removed:
listen => ['http://127.0.0.1:3000'],
20
Removed:
pid_file => '/run/fapg-daq-dashboard/hypnotoad.pid',
21
Removed:
workers => 2,
22
Removed:
proxy => 1,
23
Removed:
},
24
Removed:
);
25
Removed:
26
Removed:
my $db_path = $ENV{FAPG_DAQ_DB} // $self->home->rel_file('fapg-daq.sqlite3');
27
Removed:
28
Removed:
my $sqlite = Mojo::SQLite->new("sqlite:$db_path");
29
Removed:
30
Removed:
$self->helper( sqlite => sub { $sqlite } );
31
Removed:
32
Removed:
$self->helper(
33
Removed:
probes => sub {
34
Removed:
return [
35
Removed:
{
36
Removed:
key => 'ph',
37
Removed:
label => 'pH',
38
Removed:
unit => 'pH',
39
Removed:
node => 'Pi Zero 01',
40
Removed:
},
41
Removed:
{
42
Removed:
key => 'do',
43
Removed:
label => 'Dissolved Oxygen',
44
Removed:
unit => 'mg/L',
45
Removed:
node => 'Pi Zero 03',
46
Removed:
},
47
Removed:
{
48
Removed:
key => 'orp',
49
Removed:
label => 'ORP',
50
Removed:
unit => 'mV',
51
Removed:
node => 'Pi Zero 04',
52
Removed:
},
53
Removed:
{
54
Removed:
key => 'ec',
55
Removed:
label => 'Electrical Conductivity',
56
Removed:
unit => 'µS/cm',
57
Removed:
node => 'Pi Zero 02',
58
Removed:
},
59
Removed:
];
60
Removed:
}
61
Removed:
);
62
Removed:
63
Removed:
$self->helper(
64
Removed:
status_items => sub {
65
Removed:
return [
66
Removed:
{
67
Removed:
key => 'hub',
68
Removed:
label => 'DAQ Hub',
69
Removed:
},
70
Removed:
$self->probes->@*,
71
Removed:
];
72
Removed:
}
73
Removed:
);
74
Removed:
75
Removed:
my $r = $self->routes;
76
Removed:
77
Removed:
$r->get('/')->to('dashboard#index');
78
Removed:
$r->get('/graphs/:probe')->to('dashboard#graph');
79
Removed:
80
Removed:
$r->get('/api/readings/:probe')->to('Reading#list');
81
Removed:
$r->get('/api/status/:probe')->to('Reading#status');
82
Removed:
}
83
Removed:
84
Removed:
1;
roles/dashboard/lib/dashboard/Controller/Dashboard.pm
@@ -1,25 +0,0 @@
1
Removed:
# -*- mode: cperl; -*-
2
Removed:
3
Removed:
package dashboard::Controller::Dashboard;
4
Removed:
use Mojo::Base 'Mojolicious::Controller', -signatures;
5
Removed:
6
Removed:
sub index ($self) {
7
Removed:
$self->render(
8
Removed:
template => 'dashboard/index',
9
Removed:
probes => $self->probes,
10
Removed:
);
11
Removed:
}
12
Removed:
13
Removed:
sub graph ($self) {
14
Removed:
my $probe_key = $self->stash('probe');
15
Removed:
my ($probe) = grep { $_->{key} eq $probe_key } $self->probes->@*;
16
Removed:
17
Removed:
return $self->reply->not_found unless defined $probe;
18
Removed:
19
Removed:
$self->render(
20
Removed:
template => 'dashboard/graph',
21
Removed:
probe => $probe,
22
Removed:
);
23
Removed:
}
24
Removed:
25
Removed:
1;
roles/dashboard/lib/dashboard/Controller/Reading.pm
@@ -1,109 +0,0 @@
1
Removed:
# -*- mode: cperl; -*-
2
Removed:
3
Removed:
package dashboard::Controller::Reading;
4
Removed:
use Mojo::Base 'Mojolicious::Controller', -signatures;
5
Removed:
6
Removed:
use POSIX qw(strftime);
7
Removed:
8
Removed:
my $STATUS_REACHABLE_SECONDS = 120;
9
Removed:
10
Removed:
sub list ($self) {
11
Removed:
my $probe = $self->param('probe') // '';
12
Removed:
13
Removed:
my %known = map { $_->{key} => 1 } $self->probes->@*;
14
Removed:
15
Removed:
return $self->render(
16
Removed:
status => 404,
17
Removed:
json => {
18
Removed:
error => "Unknown probe type: $probe",
19
Removed:
},
20
Removed:
) unless $known{$probe};
21
Removed:
22
Removed:
my $limit = $self->param('limit') // 300;
23
Removed:
24
Removed:
$limit = 300 unless $limit =~ /^\d+$/;
25
Removed:
$limit = 2_000 if $limit > 2_000;
26
Removed:
27
Removed:
my @where = ('probe = ?');
28
Removed:
my @bind = ($probe);
29
Removed:
my $since = $self->param('since');
30
Removed:
31
Removed:
if ( defined $since && $since =~ /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ ) {
32
Removed:
push @where, 'COALESCE(received_at, timestamp) >= ?';
33
Removed:
push @bind, $since;
34
Removed:
}
35
Removed:
36
Removed:
my $rows = $self->sqlite->db->query(
37
Removed:
q{
38
Removed:
SELECT timestamp, received_at, node, probe, value, unit
39
Removed:
FROM readings
40
Removed:
WHERE
41
Removed:
}
42
Removed:
. join( "\n AND ", @where ) . q{
43
Removed:
ORDER BY COALESCE(received_at, timestamp) DESC, id DESC
44
Removed:
LIMIT ?
45
Removed:
},
46
Removed:
@bind,
47
Removed:
$limit,
48
Removed:
)->hashes->to_array;
49
Removed:
50
Removed:
$rows = [ reverse $rows->@* ];
51
Removed:
52
Removed:
$self->render(
53
Removed:
json => {
54
Removed:
probe => $probe,
55
Removed:
readings => $rows,
56
Removed:
},
57
Removed:
);
58
Removed:
}
59
Removed:
60
Removed:
sub status ($self) {
61
Removed:
my $probe = $self->param('probe') // '';
62
Removed:
63
Removed:
my %known = map { $_->{key} => 1 } $self->status_items->@*;
64
Removed:
65
Removed:
return $self->render(
66
Removed:
status => 404,
67
Removed:
json => {
68
Removed:
error => "Unknown status item: $probe",
69
Removed:
},
70
Removed:
) unless $known{$probe};
71
Removed:
72
Removed:
my $row = eval {
73
Removed:
$self->sqlite->db->query(
74
Removed:
q{
75
Removed:
SELECT timestamp, received_at, node, probe, status, message, error, device
76
Removed:
FROM node_status
77
Removed:
WHERE probe = ?
78
Removed:
AND valid = 1
79
Removed:
ORDER BY COALESCE(timestamp, received_at) DESC, id DESC
80
Removed:
LIMIT 1
81
Removed:
},
82
Removed:
$probe,
83
Removed:
)->hash;
84
Removed:
};
85
Removed:
86
Removed:
$row = undef if $@;
87
Removed:
mark_unreachable_if_stale($row) if $row;
88
Removed:
89
Removed:
$self->render(
90
Removed:
json => {
91
Removed:
probe => $probe,
92
Removed:
status => $row,
93
Removed:
},
94
Removed:
);
95
Removed:
}
96
Removed:
97
Removed:
sub mark_unreachable_if_stale ($row) {
98
Removed:
my $received_at = $row->{received_at} // $row->{timestamp};
99
Removed:
return if !defined $received_at || $received_at ge reachable_since();
100
Removed:
101
Removed:
$row->{status} = 'unreachable';
102
Removed:
$row->{message} = "No status received in the last $STATUS_REACHABLE_SECONDS seconds";
103
Removed:
}
104
Removed:
105
Removed:
sub reachable_since {
106
Removed:
return strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime( time - $STATUS_REACHABLE_SECONDS ) );
107
Removed:
}
108
Removed:
109
Removed:
1;
roles/dashboard/script/dashboard
@@ -8,4 +8,4 @@
8
8
use Mojolicious::Commands;
9
9
10
10
# Start command line interface for application
11
Removed:
Mojolicious::Commands->start_app('dashboard');
11
Added:
Mojolicious::Commands->start_app('FAPG::DAQ::Dashboard');
roles/dashboard/t/lib/Dashboard/Test.pm
@@ -7,14 +7,14 @@
7
7
use POSIX qw(strftime);
8
8
use Test::Mojo;
9
9
10
Removed:
use dashboard;
10
Added:
use FAPG::DAQ::Dashboard;
11
11
12
12
our @EXPORT_OK = qw(test_app);
13
13
14
14
sub test_app {
15
15
$ENV{FAPG_DAQ_DB} = ':memory:';
16
16
17
Removed:
my $t = Test::Mojo->new('dashboard');
17
Added:
my $t = Test::Mojo->new('FAPG::DAQ::Dashboard');
18
18
my $fresh_status_at = utc_timestamp(time);
19
19
my $stale_status_at = utc_timestamp( time - 121 );
20
20