[Perl] DAQ system for the FAPG.
1
use Mojo::Base -strict;
2
3
use Test2::V0;
4
5
use FindBin;
6
use lib "${FindBin::Bin}/../lib/";
7
use lib "${FindBin::Bin}/lib/";
8
use Dashboard::Test qw(test_app test_empty_app);
9
10
my $t = test_app();
11
12
subtest 'insights page renders with all sections' => sub{
13
$t->get_ok('/insights')
14
->status_is(200)
15
->text_is( 'title', 'FAPG DAQ Insights' )
16
->element_exists('.navbar-nav a.nav-link.active[href="/insights"]')
17
->element_exists_not('[data-home-masthead]')
18
->element_exists_not('[data-home-koan-card]')
19
->element_exists(
20
'[data-mobile-navigation] a.active[href="/insights"][aria-current="page"]'
21
)
22
->element_exists(
23
'main section[data-controls-panel][aria-labelledby="insights-controls-title"]'
24
)
25
->text_is( '#insights-controls-title', 'Insights controls' )
26
->element_exists(
27
'#insights-controls .graph-control[data-default-timeframe="day"]')
28
->element_exists('[data-default-timeframe="day"]');
29
};
30
31
subtest 'insights page has all chart canvases' => sub{
32
$t->get_ok('/insights')
33
->status_is(200)
34
->element_exists('canvas#chart-stability')
35
->element_exists('canvas#chart-derivatives')
36
->element_exists('canvas#chart-diurnal')
37
->element_exists('canvas#chart-overlay-do-orp')
38
->element_exists('canvas#chart-ph-do-lag');
39
};
40
41
subtest 'diurnal API returns structured response' => sub{
42
$t->get_ok('/api/insights/diurnal/do')
43
->status_is(200)
44
->json_has('/probe')
45
->json_is( '/probe' => 'do' )
46
->json_has('/hours')
47
->json_has('/traces')
48
->json_has('/mean');
49
};
50
51
subtest 'diurnal API with invalid probe' => sub{
52
$t->get_ok('/api/insights/diurnal/nope')
53
->status_is(404)
54
->json_has('/error');
55
};
56
57
subtest 'diurnal API accepts days param' => sub{
58
$t->get_ok('/api/insights/diurnal/ph?days=3')
59
->status_is(200)
60
->json_is( '/days' => 3 )
61
->json_is( '/probe' => 'ph' );
62
};
63
64
subtest 'derivatives API returns all probes' => sub{
65
$t->get_ok('/api/insights/derivatives')
66
->status_is(200)
67
->json_has('/probes')
68
->json_has('/probes/ph')
69
->json_has('/probes/do')
70
->json_has('/probes/orp')
71
->json_has('/probes/ec')
72
->json_is( '/hours' => 48 );
73
};
74
75
subtest 'derivatives API accepts hours param' => sub{
76
$t->get_ok('/api/insights/derivatives?hours=24')
77
->status_is(200)
78
->json_is( '/hours' => 24 );
79
};
80
81
subtest 'stability API returns points array' => sub{
82
$t->get_ok('/api/insights/stability')
83
->status_is(200)
84
->json_has('/points')
85
->json_is( '/hours' => 168 )
86
->json_is( '/window' => 6 );
87
};
88
89
subtest 'stability API accepts hours param' => sub{
90
$t->get_ok('/api/insights/stability?hours=48')
91
->status_is(200)
92
->json_is( '/hours' => 48 );
93
};
94
95
subtest 'empty database insights page' => sub{
96
my $empty = test_empty_app();
97
$empty->get_ok('/insights')
98
->status_is(200)
99
->element_exists('canvas#chart-stability');
100
};
101
102
subtest 'insights APIs with no recent data' => sub{
103
104
# test_app has data from 2026-07-05, outside default time windows
105
# so these return empty/minimal results gracefully
106
$t->get_ok('/api/insights/stability?hours=1')
107
->status_is(200)
108
->json_is( '/points' => [] );
109
$t->get_ok('/api/insights/derivatives?hours=1')
110
->status_is(200)
111
->json_has('/probes');
112
$t->get_ok('/api/insights/diurnal/do?days=1')
113
->status_is(200)
114
->json_is( '/traces' => [] );
115
};
116
117
subtest 'insights APIs with matching data' => sub {
118
119
# Seed recent hourly rollups to exercise computation paths
120
my $now = time;
121
122
for my $offset ( 1 .. 12 ) {
123
my $epoch = ( int( $now / 3600 ) - $offset ) * 3600;
124
for my $probe (qw(ph do orp ec)) {
125
my %base = ( ph => 7.0, do => 8.0, orp => 350, ec => 800 );
126
my $val = $base{$probe} + ( $offset * 0.1 );
127
$t->app->sqlite->db->query(
128
q{INSERT OR IGNORE INTO reading_rollups
129
(probe, bucket_seconds, bucket_epoch, sample_count,
130
value_total, value_min, value_max,
131
first_sample_epoch, last_sample_epoch, max_gap_seconds)
132
VALUES (?, 3600, ?, 10, ?, ?, ?, ?, ?, 0)},
133
$probe, $epoch, $val * 10, $val - 0.1, $val + 0.1,
134
$epoch, $epoch + 3000,
135
);
136
}
137
}
138
139
$t->get_ok('/api/insights/stability?hours=24')
140
->status_is(200)
141
->json_has('/points');
142
143
$t->get_ok('/api/insights/derivatives?hours=24')
144
->status_is(200)
145
->json_has('/probes/ph');
146
147
my $derivs = $t->tx->res->json->{probes};
148
ok @{ $derivs->{ph} } > 0, 'derivatives returns rate points for ph';
149
150
$t->get_ok('/api/insights/diurnal/do?days=1')
151
->status_is(200)
152
->json_has('/traces');
153
154
my $diurnal = $t->tx->res->json;
155
ok @{ $diurnal->{traces} } > 0, 'diurnal returns traces with recent data';
156
};
157
158
done_testing;
159