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.

Commit
56f1baf63d2a0fcff63670da8df8bbda6c181ab7
Author
OpenAI Codex <codex@openai.com>
Author date
Committer
OpenAI Codex <codex@openai.com>
Committer date
Changed files
roles/dashboard/lib/FAPG/DAQ/Dashboard.pm
index 00000000..fa798e93 000000..100644
@@ -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
index 00000000..f594884f 000000..100644
@@ -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
index 00000000..32c82fd6 000000..100644
@@ -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
index 6f433366..00000000 100644..000000
@@ -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
index 2203f350..00000000 100644..000000
@@ -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
index 00cf0506..00000000 100644..000000
@@ -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
index 73179739..e8c39649 100755..100755
@@ -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
index 5125b66a..810e6296 100644..100644
@@ -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